본문 바로가기

Backend/Node.js

Observer Pattern in Node.js

관찰자 패턴(Observer Pattern)은 Node.js 에서 중요하고 기본적인 패턴이며, reactive 특성을 모델링하면서 콜백을 보완하는 패턴이라고 할 수 있습니다.

관찰자 패턴은 Node.js 에서만 사용되는 패턴은 아닌데, 일반적으로 객체의 상태 변화를 메서드등을 통해 관찰자에게 알릴 수 있도록 객체를 정의하는 것입니다. pub/sub 모델에 쓰이기도 하며 최근엔 분산 이벤트 시스템에도 많이 쓰이는 것 같습니다.

 

Node.js 에서 일반적인 콜백 스타일은 결과를 하나의 대상에게만 전파할 수 있으나 관찰자 패턴에선 여러 관찰자들에게 알릴 수 있어 콜백을 보완한다고 할 수 있습니다.

 

- EventEmitter

 

다른 언어들과 다르게 Node.js 의 코어에는 이미 관찰자 패턴이 내장되어 있고, EventEmitter 클래스를 통해 이를 사용할 수 있습니다.

예시 코드를 보겠습니다.

 

// tmp.txt
test file
file file file
test okokokok
ppppp test


// test-emitter.js
const { EventEmitter } = require('events');
const fs = require('fs');

const testEmitter = (file, regex) => {
  const emitter = new EventEmitter();

  fs.readFile(file, 'utf8', (err, data) => {
    if (err) {
      return emitter.emit('error', err);
    }

    emitter.emit('read', file);

    let match = data.match(regex);

    if (match) {
      match.forEach(elem => emitter.emit('match', file, elem));
    }
  });

  return emitter;
};

testEmitter('tmp.txt', /test \w+/g)
  .on('read', file => console.info(file + '- file read'))
  .on('match', (file, match) => console.info(match + ' - matched'))
  .on('error', err => console.error(err));
  
/*
tmp.txt - file read
test file - matched
test okokokok - matched
*/

 

위에서 적은대로 EventEmitter 클래스를 통해 emitter 를 사용할 수 있고, 아래는 EventEmitter 의 메서드들입니다.

 

  • on(event, listener)
  • once(event, listener)
  • emit(event, ...)
  • removeListener(event, listener)

emit 부터 보자면 이 메서드를 사용해 특정 이벤트를 생성하고, listener 에게 전달할 수 있는 인자들을 지원합니다. 위 코드에선 파일을 읽을때 발생할 수 있는 에러 이벤트에 대한 error, 파일을 읽는 이벤트인 read, 파일 내용 중 일치하는 항목이 발견되었을 때의 이벤트 match를 생성했고 각각 err, file, (file, elem) 의 인자들을 listener 에게 전달하고 있습니다.

 

on 메서드를 사용하면 특정 이벤트에 대해 listener 를 등록할 수 있습니다. 예시 코드에선 read / match / error 이벤트에 대해 각각 listener 를 등록했죠. emit 메서드가 전달해준 인자들을 여기서 받아 console 로 stdout 을 찍고 있습니다.

출력 결과를 보면 쉽게 이해가 되리라 생각합니다. 

 

위 예시 코드에 나와있진 않지만 once 메서드는 이름 그대로 첫 이벤트 이후 제거되는 listener 를 등록할 수 있습니다.

removeListener 메서드는 특정 이벤트에 대한 listener 를 명시적으로 제거할 수 있습니다.

 

일반적으로 EventEmitter 는 콜백에서처럼 예외가 발생해도 예외를 바로 throw 할 수 없습니다.

대신 위에서 볼 수 있듯이 error 라는 특수한 이벤트를 발생시키고, Error 객체를 인자로 전달하는 규약이 정해져있습니다.

Node.js 에선 이렇게 발생한 error 이벤트에 대해 listener 가 없는 경우 프로그램을 종료하므로 에러 이벤트에 대한 listener 는 항상 등록하는 것이 좋습니다.

 

express().on('error', (error: NodeJS.ErrnoException) => {
  const errno = String(error.errno || 0);
  ...
});

 

뭔가 많은 것이 생략된 코드지만 express 서버를 띄웠을 때 error 이벤트에 대한 listener 를 등록하는 코드입니다.

실제 이러한 형태로 listener 를 등록해 express 가 error 이벤트를 발생시켰을 경우 특정 횟수만큼 express 앱을 재부팅하는 등의 처리를 하고 있습니다.

 

- 관찰 확장

 

EventEmitter 클래스를 활용해 관찰 대상을 직접 만들어 관찰자 패턴을 사용할 수 있지만, 조금은 제약이 많은 사용법입니다.

실제로 그렇게 구현이 안되어있을 수 있지만, 위에서 예시를 든 것처럼 express 는 어떻게 on 메서드를 사용해 error 이벤트에 대한 listener 를 등록할 수 있게 해놨을까요?

 

우리는 일반 객체를 아주 쉽게 관찰 가능하게 만들 수 있습니다. 예시를 보죠.

 

const { EventEmitter } = require('events');
const fs = require('fs');

class TestEmitter extends EventEmitter {
  constructor(file, regex) {
    super();
    this.file = file;
    this.regex = regex;
  }

  match() {
    const file = this.file;
    fs.readFile(file, 'utf8', (err, data) => {
      if (err) {
        return this.emit('error', err);
      }

      this.emit('read', file);

      let match = data.match(this.regex);

      if (match) {
        match.forEach(elem => this.emit('match', file, elem));
      }
    });

    return this;
  }
}

const testEmitter = new TestEmitter('tmp.txt', /test \w+/g);
testEmitter
  .match()
  .on('read', file => console.info(file + '- file read'))
  .on('match', (file, match) => console.info(match + ' - matched'))
  .on('error', err => console.error('error!', err));
  
/*
error! [Error: ENOENT: no such file or directory, open 'tmp.txt'] {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: 'tmp.txt'
}
*/

 

파일을 읽어 match 된 내용을 출력하는 처음 사용한 예시를 비슷하게 변경해봤습니다.

확장한다는 이름처럼 EventEmitter 를 extends 하여 우리는 일반 객체를 관찰 가능한 객체로 만들 수 있습니다.

이 코드에 별다른 추가 설명은 필요 없을 것입니다. 일부러 이번엔 error 이벤트에 대해 등록한 listener 의 동작을 보기 위해 tmp.txt 파일을 만들지 않은 상태로 코드를 실행해봤고, 의도한대로 listener 가 동작하고 있습니다.

 

- vs Callback

 

딱히 대립시킬 필요는 없지만, 앞으로 업무를 하면서 API 를 정의할 때 Callback 을 사용할지 EventEmitter 를 사용하는게 좋을지
애매할 때가 간혹 있습니다.

일반적으론 결과를 비동기 방식으로 반환하는 경우엔 Callback 을 사용하고, 이벤트는 발생한 무엇인가를 전달할 때 사용합니다.

하지만 이런 구분으론 역시나 반대상황에서도 서로를 사용할 수 있죠.

 

관점에 따라 나누자면 Callback 은 호출자에게 직접 전달하고, EventEmitter 는 특정 이벤트가 발생했음을 알립니다.

EventEmitter 는 이벤트가 여러번 발생할 수도, 아예 발생하지 않을 수도 있지만 Callback 은 정확히 한번 호출되어야 합니다.

반복적인 상황에서 무언가를 처리하는게 대다수의 케이스이기 때문에, EventEmitter 가 대체로 깔끔한 API 를 제공할 수 있는 선택지가 될 수 있습니다.

 

하지만 결국 중요한건 필요한 스펙을 정확히 이해하고 용도에 맞게 사용하는 개발자의 판단입니다.

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

제네레이터 (Generator)  (0) 2022.08.27
Promise & Promisify  (0) 2022.08.20
모듈 시스템  (0) 2022.08.07
Callback 패턴  (0) 2022.07.23
lockfile 주의점  (0) 2022.07.16