관련지식
node.js, javascript, promise, async

오늘 쓸 내용에 대한 별도의 정의가 있는지 몰라서 제 마음대로 이름을 붙이다보니 뭔가 거창한 이름이 되었네요. 오늘은 오랜만에 코딩 스타일에 대해 적어봅니다.

문제

얼마전 친한 동생으로부터 node.js 개발에 대한 질문이 하나 있었습니다. 상황은 아래와 같았습니다.

어떤 비동기함수 A를 호출해서 응답받은 결과값에 따라 동기함수 B와 비동기함수 C와 D가 하나만 실행되거나 같이 실행됨. 그리고 모든 실행이 끝나면 E 함수를 호출해야함. 그런데 결과값에 따른 함수 B/C/D 호출을 열거하기가 싫음. B/C/D 함수간에 상관관계는 없으므로 실행 우선순위는 없음

일단 비동기, 동기 함수 여부를 떠나서 수도코드로 정리하면 대충 아래와 같은 상황이겠죠.

  1. ret = A()
  2. if(ret == 1)
  3. B()
  4. else if(ret == 2)
  5. B()
  6. C()
  7. else if(ret == 3)
  8. C()
  9. D()
  10. else if(ret == 4)
  11. C()
  12. else
  13. ...
  14. E()

함수 호출에 대한 모든 케이스를 열거해야 하지만 케이스별로 실행할 로직이 명확하게 구분되기 때문에 나쁜 방법은 아닙니다. 그런데 C와 D함수는 비동기함수라는 것이 걸리네요. 노드에서 비동기함수를 실행할때 주로 사용하는것은 콜백함수, Promise, async/await 가 있는데 여기선 어떤 방법이 좋을까요? 아마 동생은 이런 구조가 떠올라서 질문한것 같습니다.

  1. ret = A()
  2. if(ret == 1)
  3. B()
  4. E()
  5. else if(ret == 2)
  6. B()
  7. C(function() {
  8. E()
  9. })
  10. else if(ret == 3)
  11. C(function() {
  12. D(function() {
  13. E()
  14. })
  15. })
  16. ...

await 를 썼다면 좀 나았겠네요.

  1. ret = A()
  2. if(ret == 1)
  3. B()
  4. else if(ret == 2)
  5. B()
  6. await C()
  7. else if(ret == 3)
  8. await C()
  9. await D()
  10. ...
  11. E()

문제파악

위 상황에서 중요한 포인트를 뽑아볼까요?

  1. A함수의 결과값에 따라 B/C/D 가 실행되어야 하고 마지막에 E가 실행되야함
  2. B/C/D 함수끼리 실행 순서 상관없음

C와 D함수가 비동기함수라는 것은 사실 별로 중요하지 않습니다. 가장 중요한것은 위의 두가지 조건이죠.

A함수의 결과값에 따라 호출해야 할 함수가 정해지죠. 결과값에 따른 호출 케이스가 굉장히 단순하거나 간단하면 if…else 나 switch…case 같은 조건식 없이 만들수도 있지만 조건이 다양하다면 조건식은 반드시 필요하게 됩니다. 그리고 조건식은 하드코딩을 요구하죠. 물론 조건식 이용해서 아래처럼 코딩해도 됩니다.

  1. ret = A()
  2. if(ret == 1)
  3. enableB = true;
  4. else if(ret == 2)
  5. enableB = true;
  6. enableC = true;
  7. else if(ret == 3)
  8. enableC = true;
  9. enableD = true;
  10. ...
  11. if(enableB)
  12. B()
  13. if(enableC)
  14. await C()
  15. if(enableD)
  16. await D()
  17. E()

함수 호출부는 한곳에 몰려 있으므로 이전 코딩보다는 조금 발전한것 같지만, 비동기함수의 장점을 살리지 못했고 전체 소스는 길어졌습니다. 가독성이 떨어지죠. 따라서 다른 방법으로 해결해야 하는데, 먼저 1번 포인트를 좀 바꿔보겠습니다. A함수의 결과값에 따라 B/C/D 함수를 호출하는것이 아니라 무조건 함수를 호출하면 어떨까요? 함수는 호출하지만 함수 안에서 실제적인 로직 실행을 하지 않는것입니다.

  1. A함수의 실행후 B/C/D 함수 호출. 파라미터로 전달된 결과값에 따라 각 함수내 로직이 실행/미실행됨. 마지막에 E함수 호출

처음에 만들려고 했던 소스는 결과값을 기준으로 어떤 함수를 호출할지 결정하는것이지만, 지금 제시하는것은 일단 함수를 호출하고 호출된 함수에서 실제 로직을 수행할지 판단하는 것입니다. 비유하자면 결혼식때 회사 직원을 부를까 말까 고민하는 것이 아니라, 일단 청첩장을 주면 받은 사람이 갈까 말까 고민하는것이죠ㅎㅎ 그럼 호출하는쪽의 수도코드는 아래와 같은 형태가 될것입니다.

  1. ret = A()
  2. B(ret)
  3. C(ret)
  4. D(ret)
  5. E()

그리고 2번 항목에서 실행 순서는 상관이 없다고 되어있습니다. 게다가 C와 D는 비동기함수다. 즉 두 함수를 한번에 실행해도 상관없다. 뭐 떠오르는거 없으신가요? Promise.all() 은 모든 Promise를 같이 실행하고, 모든 Promise가 끝나면 이벤트가 발생하죠. 이번 상황에서 매우 적절한 기능입니다.

소스구현

먼저 비동기함수로 동작하는 C함수를 구현해보겠습니다. Promise.all() 을 사용할 것이기 때문에 당연히 Promise 객체로 만들것입니다.

  1. function C(a_ret) {
  2. return new Promise(function(resolve, reject) {
  3. setTimeout(function() {
  4. resolve();
  5. }, 200);
  6. });
  7. }

그런데 이렇게 만들면 C함수를 호출하는 족족 실행이 되겠죠? 파라미터에 따라 로직이 수행되지 않도록 수정해야 합니다. 2와 3일때만 실행하고 그외엔 실행하지 않게 만들어보겠습니다.

  1. function C(a_ret) {
  2. return new Promise(function(resolve, reject) {
  3. if(a_ret != 2 && a_ret != 3) { //2와 3이 아닐땐 로직을 수행하지 않는다. reject 아님
  4. resolve();
  5. return;
  6. }
  7. setTimeout(function() {
  8. resolve();
  9. }, 200);
  10. });
  11. }

별로 어려운건 없습니다. 단지 if 구문을 하나 추가하면 되죠. 결국 조건값에 따라 로직을 수행하고 resolve 할것이냐, 수행하지 않고 resolve 할것이냐의 차이만 있을뿐입니다. D함수도 마찬가지겠죠?

B함수의 로직은 동기 로직이라고 했지만 상관없습니다. Promise에 동기 로직을 못쓸 이유는 없으니까요.

  1. function B(a_ret) {
  2. return new Promise(function(resolve, reject) {
  3. if(a_ret != 1 && a_ret != 2) { //1과 2가 아닐땐 로직을 수행하지 않는다.
  4. resolve();
  5. return;
  6. }
  7. console.log('call B function');
  8. resolve();
  9. });
  10. }

이렇게 B, C, D 함수가 만들어지면 아래와 같이 실행할수가 있습니다.

  1. Promise.all(B(ret), C(ret), D(ret)).then(function(rets) {
  2. E();
  3. });

처음에 생각했던 수도코드와 비교해볼까요?

  1. ret = A()
  2. if(ret == 1)
  3. B()
  4. E()
  5. else if(ret == 2)
  6. B()
  7. C(function() {
  8. E()
  9. })
  10. else if(ret == 3)
  11. C(function() {
  12. D(function() {
  13. E()
  14. })
  15. })
  16. ...

훨씬 간단하게 구현되었죠. 이렇게 실행 조건의 판단을 호출하는 쪽에서 하지 않고 호출 당하는 함수에서 하게 되면 로직이 간단하게 정리될수 있습니다. 공통으로 사용하는 공통함수를 만들때는 이런 형태로 많이 만드는데, 막상 비즈니스 로직을 구현할땐 생각하지 못하는것 같네요.

최종샘플

A와 E 함수는 생략되어있는데 크게 중요한 부분은 아니므로 A함수대신 정수값을, E함수 호출부분 대신 console.log() 를 사용하셔도 됩니다.

  1. function B(a_ret) {
  2. return new Promise(function(resolve, reject) {
  3. if(a_ret != 1 && a_ret != 2) { //1과 2가 아닐땐 로직을 수행하지 않는다.
  4. resolve();
  5. return;
  6. }
  7. console.log('call B function');
  8. resolve();
  9. });
  10. }
  11. function C(a_ret) {
  12. return new Promise(function(resolve, reject) {
  13. if(a_ret != 2 && a_ret != 3) { //2와 3이 아닐땐 로직을 수행하지 않는다. reject 아님
  14. resolve();
  15. return;
  16. }
  17. setTimeout(function() {
  18. resolve();
  19. }, 200);
  20. });
  21. }
  22. function D(a_ret) {
  23. return new Promise(function(resolve, reject) {
  24. if(a_ret != 1 && a_ret != 3) { //1과 3이 아닐땐 로직을 수행하지 않는다. reject 아님
  25. resolve();
  26. return;
  27. }
  28. setTimeout(function() {
  29. resolve();
  30. }, 100);
  31. });
  32. }
  33. (async () => {
  34. const ret = A();
  35. Promise.all(B(ret), C(ret), D(ret)).then(function(rets) {
  36. E();
  37. });
  38. })