본문 바로가기

Backend/Node.js

Callback 패턴

Node.JS 에서 callback 은 Reactor 패턴의 핸들러를 구현한 것입니다. (관련 포스팅)

callback 은 작업 결과를 전달하기 위해 호출되는 함수이며, 비동기 작업을 처리할 때 반드시 필요합니다.

Javascript 에선 함수가 일급 객체여서 변수에 할당허간, 인수로 전달되거나, 다른 함수 호출에서 반환되거나 자료구조에 저장될 수 있기 때문에 callback 을 아주 잘 표현할 수 있습니다.

 

- 직접 스타일

 

function add(x, y) {
  return x + y;
}

 

동기 함수에서 return 을 통해 결과를 전달하는 아주 흔한 방식이며, 이를 직접 스타일이라고 합니다.

하지만 callback 은 이러한 형태와 약간 다릅니다.

 

- 연속 전달 방식(Continuation-Passing Style)

 

function add(x, y, callback) {
  callback(x + y);
}

 

위의 직접 스타일을 연속 전달 방식으로 바꾼 코드입니다. Javascript 에서 callback 은 다른 함수에 인자로 전달되는 함수이며,

작업이 완료되면 결과로 호출합니다. 결과를 전달하는 이러한 방식을 연속 전달 방식 이라고 합니다.

 

function add(x, y, callback) {
  setTimeout(() => {
    callback(x + y);
  }, 100);
}

console.info('before');
add(1, 2, result => console.info('result: ', result));
console.info('after');

/*
before
after
result: 3
*/

 

add 가 비동기 함수여도 전달 방식은 달라지지 않습니다. 다만 호출 결과에 약간의 변화가 있죠.

Node.JS 에서 setTimeout 의 비동기 요청은 전달된 후 즉시 제어를 이벤트 루프에 돌려줘서 큐에 있는 다른 이벤트를 처리할 수 있게
하기 때문에, before -> after -> result 순서로 출력이 된 것입니다.

 

- 비연속 전달 방식

 

Javascript 에서 모든 callback 이 연속 전달 방식인것은 아닙니다.

 

const result = [1, 2, 3].map(item => item + 1);
console.info(result); // [2, 3, 4]

 

함수에 callback 인자가 있으면 비동기식이거나 연속 전달 방식을 사용한다고 가정할 수 있지만,

위 코드에서 callback 은 배열 내의 요소를 반복하는데 사용될 뿐 연산 결과를 전달하진 않습니다.

 

- callback 규칙

 

Node.JS 에서 callback 을 사용할 때의 규칙이자, 연속 전달 방식에서 따르는 규칙이라고 할 수 있습니다.

바로 위에 적은 비연속 전달 방식에서는 이러한 규칙이 바로 적용되지 않았다는 것을 바로 볼 수 있습니다.

 

fs.readFile(filename, [options], callback)

 

Node.JS 에서 제공하는 fs 모듈의 readFile 함수입니다. 표준 규칙은 함수가 입력에 callback 을 허용한다면 이는 맨 마지막 인자

전달되어야 한다는 점 입니다. 일반적으로 함수의 인자에 함수가 들어간다고 할 때, 가장 마지막에 위치하는 것이 당연히 가독성에 더

도움이 될 것입니다.

 

fs.readFile('tmp.txt', 'utf8', (err, data) => {
  if (err) reject(err);
  else resolve(data);
});

 

Node.JS 에선 연속 전달 방식 함수에 의해 생성된 오류는 항상 callback 의 첫번째 인자로 전달되며, 실제 결과는 두번째 인자부터

전달됩니다. 실제로 fs 뿐만 아니라 callback 을 지원하는 많은 API 들을 사용하다보면 이러한 표준 규칙을 따르고 있고, 에러 없이 성공했을 경우 err 에는 대체로 null 이 전달되는 것을 볼 수 있습니다(간혹 undefined 도)

 

const fs = require('fs');

function readSomething(filename, callback) {
  fs.readFile(filename, 'utf8', (err, data) => {
    if (err) return callback(err);

    let result;

    try {
      result = JSON.parse(data);
    } catch (e) {
      return callback(e);
    }

    callback(null, result);
  });
}

 

callback 을 사용시 에러를 전파할 땐 return 문을 사용해야 한다는 점은 은근히 놓치기 쉬운 포인트입니다.

callback 을 통해 에러를 전달할 때는 return 을 같이 사용해 이후의 코드 라인이 실행되지 않도록 해야 합니다.

 

const fs = require('fs');

function readSomething(filename, callback) {
  fs.readFile(filename, 'utf8', (err, data) => {
    if (err) return callback(err);

    callback(null, JSON.parse(data));
  });
}

 

위와 비슷하지만 JSON.parse 부분에서 try - catch 를 제거하고 데이터를 callback 으로 전달하는 코드입니다.

여기서 문제는 JSON.parse 에서 발생하는 에러를 잡을 방법이 없습니다.

 

readSomething('test.txt', (err, result) => {
  if (err) console.error(err);
  else console.info(result);
});

SyntaxError: Unexpected token c in JSON at position 0
    at JSON.parse (<anonymous>)
    at .../callback-pattern.js:17:25
    at FSReqCallback.readFileAfterClose [as oncomplete] (internal/fs/read_file_context.js:63:3)

 

JSON.parse 가 실패할 형식의 내용을 test.txt 에 넣어놓고 코드를 실행한다면, 위처럼 SyntaxError 가 발생하면서 앱이 종료됩니다.

 

try {
  readSomething('test.txt', (err, result) => {
    if (err) console.error(err);
    else console.info(result);
  });
} catch (e) {
  console.error('use try - catch ', e);
}

 

이는 위처럼 readSomething 을 실행하는 부분에 try - catch 를 사용해도, 에러가 발생한 스택과 실행 스택이 다르기 때문에 JSON.parse 의 에러를 캐치하지 못합니다. 이러한 경우엔 Node.JS 어플리케이션 전역 컨텍스트에 uncaughtException 이벤트에 대한 핸들러를 추가해 핸들링할 수 있긴 합니다만, 별로 추천되는 방법은 아니며 애초에 캐치되지 않는 에러가 발생하는 형태의 코드를

작성하지 않아야 합니다.

 

callback 스타일은 callback hell 이라는 안티 패턴과도 엮여 있고 근래엔 callback 스타일이 쓰이는 코드를 보기 힘들지만,

특정 모듈을 다루거나 혹은 역으로 모듈을 작성할 때 은근히 사용하게 되는 패턴인 것 같습니다.

예를 들어 fs 모듈을 사용해 파일을 읽을때 fs.readFileSync 라는 동기 API 를 제공해주긴 하지만, fs.readFile 비동기 API 사용을 권장하는 것처럼 Node.JS 의 특징을 살리기 위해선 callback 은 앞으로도 알아두어야 할 패턴입니다.

'Backend > Node.js' 카테고리의 다른 글

Observer Pattern in Node.js  (0) 2022.08.13
모듈 시스템  (0) 2022.08.07
lockfile 주의점  (0) 2022.07.16
Node.JS - Reactor 패턴  (0) 2022.07.09
Cluster Module in Node.JS  (0) 2022.04.09