3장: CPS 패턴 사용 시의 Tip

Node.js 환경에서 CPS 패턴을 사용할 때 시도할 만한 Tip들을 정리했다.


1. Callback Hell을 조금 해결하는 방법

본인은 Promise 세대여서 Callback Hell을 제대로 경험해 본 적이 없고, 웬만한 개발 환경이라면 Callback Hell을 겪기 어려울 것으로 예상돼 짧게 요약했다.

들여 쓰기 때문에 가독성이 매우 떨어지게 되고, 변수 이름도 중첩되는 문제가 있다. 만약 Blocking API를 사용해 동일한 내용을 구현했다면 잘 못 이해할 가능성은 거의 없을 것이다.

Pattern:

  • 중첩 수준을 낮게 유지하기 위해, else 문을 사용하지 않는다.
  • 인라인 함수의 이름을 지정하면, 함수 이름을 통해 더 쉽게 디버깅이 가능하다.
  • 함수를 쪼갠다.

자주 하는 실수:

  • Callback을 호출한 뒤에도 함수는 계속 실행됨을 잊는다.

    1
    2
    if (err) callback(err);
    // 여기서도 함수는 계속 실행된다.
    • return callback(err) 혹은 return을 callback 호출 이후 수행하여 함수 실행을 종료한다.

2. 순차적으로 실행시키는 방법

Callback Hell을 겪지 않고 비동기 API를 순차적으로 실행하는 방법:

  • 재귀 함수로 실행한다.
  • 재밌는 점은, StackOverFlow가 날 일은 없다는 점이다. 비동기 함수여서 매 번 스택이 초기화되니까.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const length = N;
const tasks = [
/* ... */
];
const data = [
/* ... */
];
const callback = (f) => f;
const iterate = (idx) => {
if (idx === length) return callback();

const task = tasks[idx];
task(data[idx], (err) => {
if (err) return callback(err);
iterate(idx + 1);
});
};
iterate(0); // Callback이 재귀적으로 수행돼, N 만큼 수행된다.

이 방식의 한계:

  • 실행될 작업의 숫자를 알아야 한다.

3. JS 경쟁 조건 해결하기

  • Javascript는 단일 스레드로 실행된다.

  • 리소스 동기화는 필요 없지만, 비동기 API 타이밍 문제는 아직 남아있다.

  • Javascript 역시 호출 시점과 I/O 수행 시점 차이로 중복 작업 등의 예기치 않은 동작을 할 수 있다.

  • 상호 배제로 해결 가능하다.

1
2
3
4
5
6
7
8
9
10
// 실행 중인 job을 등록한다. 공유 리소스 동기화는 필요 없다.
const jobs = new Map();
const fn = (id, data, callback) => {
// 이 코드로 타이밍 문제를 해결할 수 있다.
if (jobs.has(id))
return process.nextTick(callback);

jobs.set(id, true);
// 정상 분기.
};

4. 동시에 수행되는 작업 개수 제한 하기

한 번에 너무 많은 파일을 열려고 하는 등의 경우 리소스 부족으로 뻗어버릴 수 있다. 동시에 실행하는 작업의 수를 제한해 이를 상황을 방지하는 아이디어를 소개한다.

알고리즘:

  • 처음에 동시 실행 제한 개수만큼의 작업을 실행
  • 각 작업이 끝날 때, 동시 실행 제한 개수 - 현재 실행 개수 만큼의 작업을 실행
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const tasks = [
/* ... */
];
const limit = 2; // 동시 실행 제한 개수
let running = 0,
completed = 0,
idx = 0;

const next = () => {
// 여유 작업 개수만큼 반복
while (running < limit && idx < tasks.length) {
const task = tasks[idx++];
task(() => {
// 새 작업을 할 수 없음
if (completed === tasks.length)
return finish();
completed++;
running--;
next(); // 새 작업을 할 여유가 있음
});
running++;
}
};

// 동시 실행 제한 개수를 채우며 계속 실행함.
next();

큐로 구현하는 방법:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Queue로 구현하는 방식
// 로직은 같은데 Queue를 사용하는 것만 다르다.
class TaskQueue {
constructor(limit) {
this.limit = limit;
this.running = 0;
this.queue = [];
}

// task :: callback => void; (must call callback)
// task를 tasks에서 가져오는 게 아니라, Queue에 넣은 것이 나온다.
// => 새 작업을 큐에 동적으로 추가할 수 있다.
pushTask(task) {
this.queue.push(task);
this.next();
}

next() {
while (
this.running < this.limit &&
this.queue.length
) {
const task = this.queue.shift();
task(() => {
this.running--;
this.next();
});
this.running++;
}
}
}

5. Async 라이브러리 사용

복잡한 비동기 제어 흐름을 선언적인 방식으로 처리할 수 있게 헬퍼 함수들을 제공하는 라이브러리이다.

  • 순차적인 반복
  • 제한된 동시 실행

등을 헬퍼 함수를 통해 쉽게 구현 가능하다. CPS 패턴은 주로 사용할 것 같진 않아 따로 정리하진 않았다.


TODO:

  • 여러 번 이해하여 좋은 예제를 만들어 이 글 내용 보강하기

3장: CPS 패턴 사용 시의 Tip

https://jsqna.com/ndp-3-cps-tips/

Author

Seongbin Kim

Posted on

21-01-08

Updated on

21-01-19

Licensed under

댓글