관찰자 패턴과 EventEmitter

2020. 2. 20. 11:42TypeScript&JavaScript/Node.js

관찰자 패턴은 상태 변화가 일어날 때 관찰자(또는 Listner)에게 알릴 수 있는 객체(Subject)를 정의하는 것.

전통적인 연속 전달 스타일 콜백은 일반적으로 그 결과를 하나의 Listner인 콜백에만 전파하지만 관찰자 패턴은 Subject가 실제로 여러 관찰자(Observer)들에게 알릴 수 있다는 것이 큰 차이다.

순수 자바스크립트는 동기적으로 작동하는데, 이 의미는 한 번에 하나의 프로세스만 실행시킨다는 것이다. 따라서 이벤트 루프를 활용하여 비동기적으로 작용하는 것은 정확히는 Node.js의 libuv라는 라이브러리를 통해 구현된다. 대표적인 비동기작업인 setTimeout이나 I/O 활동, HTTP 요청등이 libuv를 통해 스케쥴링되고 쓰레드 풀이나 커널에 인계한 콜백 작업을 실행하는 셈이다.

관찰자 패턴은 Node.js 코어에 내장되어 있으며, EventEmitter 클래스를 통해 사용할 수 있다. EventEmitter는 프로토타입이며 코어 모듈로부터 익스포트 된다. 인터넷이 연결되거나 파일을 열고 닫는 작업등은 libuv의 C++코어에서 처리하지만, 보다 상위 단계의 이벤트는 Node.js의 EventEmitter에서 관리한다. 즉, 자바스크립트 자체는 이벤트와 관련한 객체가 없고, Node.js가 그와 관련된 컨셉을 EventEmitter에 구현하는 것이다.

const EventEmitter = require('events').EventEmitter;
const eeInstance = new EventEmitter();

EventEmitter 의 필수 메소드는 on(event, listener), emit(event, [arg1]. [..]), once(event, listner), removeListner(event, listener) 등이 있다. 위 메소드들은 EventEmitter 객체를 반환한다. on 메소드는 특정 상황에 실행시킬 리스너 함수를 Emitter 안에 등록한다는 의미를 갖는다. emit 메소드는 새 이벤트를 생성하고 리스너에게 전달할 추가적인 인자들을 지원한다. once 메소드는 첫 이벤트가 전달된 후 제거되는 새로운 리스터를 등록한다. removeListner는 지정된 이벤트 유형에 대한 리스너를 제거한다.

const EventEmitter = require('events').EventEmitter;
const fs = require('fs');

function findPattern(files, regex) {
	const emitter = new EventEmitter();
    files.forEach(function(file) {
    	fs.readFile(file, 'utf8', (err, content) => {
        	if(err)
            	return emitter.emit('error', err);
            
            emitter.emit('fileread', file);
            let match;
            if(match = content.match(regex))
            	match.forEach(elem => emitter.emit('found', file, elem));
        });
    });
    return emitter;
}

위 코드의 EventEmitter는 세 가지 이벤트를 생성한다. (fileread, found, error)

findPattern(
	['fileA.txt', 'fileB.json'],
    /hello \w+/g
)
.on('fileread', file => console.log(file + ' was read'))
.on('found', (file, match) => console.log('Matched "' + match + '" in file ' + file))
.on('error', err => console.log('Error emitted: ' + err.message));

findPattern() 함수에서 만들어진 EventEmitter에 의해 생성된 세 가지 유형의 이벤트 각각에 리스너를 등록하는 예제이다.

이벤트는 콜백과 마찬가지로 동기식, 또는 비동기식으로 생성될 수 있다. 동일한 EventEmitter에서 두 가지 접근 방식을 섞지 않는 것이 좋다. 동기 이벤트와 비동기 이벤트를 발생시키는 주된 차이점은 리스너를 등록할 수 있는 방법에 있다. 이벤트가 비동기적으로 발생하면 EventEmitter가 초기화 된 후에도 프로그램은 새로운 리스너를 등록할 수 있다. 왜냐면 이벤트가 이벤트 루프의 다음 사이클이 될 때까지는 실행되지 않을 것이기 때문이다. 

반대로 이벤트를 동기적으로 발생시키려면 EventEmitter 함수가 이벤트를 방출하기 전에 모든 리스너가 등록되어 있어야 한다.

const EventEmitter = require('events').EventEmitter;

class SyncEmit extends EventEmitter {
	constructor() {
    	super();
        this.emit('ready');
    }
}

const syncEmit = new SyncEmit();
syncEmit.on('ready', () => console.log('Object is ready to be used'));

ready 이벤트가 비동기적으로 발생할 경우 위 코드는 잘 동작한다. 그러나 이벤트가 동기적으로 생성되면 이벤트가 이미 전송된 후 리스너가 등록되므로 결과적으로 리스너가 호출되지 않는다. 따라서 EventEmitter의 동작을 명확하게 구분짓는 것이 매우 중요하다.

오류 전파에 관해 EventEmitter는 이벤트가 비동기적으로 발생할 경우, 이벤트 루프에서 손실될 수 있기 때문에 콜백에서와 같이 예외가 발생해도 예외를 바로 throw 할 수 없다. 대신, 규약에 의해 error라는 특수한 이벤트를 발생시키고, Error 객체를 인자로 전달한다. Node.js는 에러 이벤트를 처리하고 예외를 자동으로 throw하며 이에 연결된 리스너가 없는 경우 프로그램을 종료하므로 항상 에러 이벤트에 대한 리스너를 등록하는 것이 권장된다.

EventEmitter는 콜백과 결합해서도 사용 가능하다.

const glob = require('glob');
glob('data/*.txt', (error, files) => 
	console.log(`All files found: ${JSON.stringify(files)}`))
    .on('match', match => console.log(`Match found: ${match}`));

콜백을 받아들이고 EventEmitter를 반환하는 함수를 만듦으로써, EventEmitter를 통해 보다 세분화된 이벤트를 방출하면서 주요 기능에 대한 간단하고 명확한 진입점을 제공할 수 있다.

 

'TypeScript&JavaScript > Node.js' 카테고리의 다른 글

Reactor 패턴  (0) 2020.02.09
Map, Set Collection, WeakMap, WeakSet Collection  (0) 2020.02.09
Node.js의 철학  (0) 2020.02.09