관찰자 패턴 ( Observer Pattern )

kakasoo·2021년 9월 25일
0
post-thumbnail

이 글은 발표한 내용을 정리한 것입니다.

Node.js에서 기본적으로 사용되고 중요한 또 다른 패턴은 관찰자 패턴입니다. 리액터 ( Reactor )그리고 콜백 ( Callback ) 과 함께 관찰자 패턴은 비동기적인 Node.js 세계를 숙달하는 데 필수적인 조건입니다. 관찰자 패턴은 Node.js의 반응적 (reactive) 특성을 모델링하고 콜백을 완벽하게 보완하는 이상적인 해결책입니다. 다음과 같이 공식적인 정의를 내릴 수 있습니다.

관찰자 패턴은 상태 변화가 일어날 때 관찰자 ( 또는 listener )에게 통지할 수 있는 객체를 정의하는 것입니다.

설명만 들어도 벅차오르네요.

요약하면 콜백 패턴을 보완하는 이상적인 형태라는 것입니다. 콜백은 하나의 리스너를 갖는 반면, 관찰자 패턴은 실질적으로 여러 관찰자를 지정할 수 있으니깐요.

const { EventEmitter } = require("events");

const emitter = new EventEmitter();

이것만으로도 바로 Observer를 만들 수 있습니다!

EventEmitter의 필수 메서드를 보도록 하겠습니다.

  • on(event, listener) : 이 메서드는 이벤트를 리스너의 함수로 등록합니다.
  • once(event, listenr) : 이 메서드는 첫 이벤트 후 제거되는 리스너를 등록합니다.
  • emit(event, [arg1], [...]) : 이 메서드는 새 이벤트를 생성하고 리스너에게 전달할 추가적인 인자 공간을 제공합니다.
  • removeListener(event, listener) : 이 메서드는 지정된 이벤트 유형에 대한 리스너를 제거합니다.
  • setMaxListeners : 리스너가 10개면 경고 메시지를 뱉는데, 이 제한을 늘릴 수 있게 해준다.
    • 하지만 가장 좋은 것은 사용이 끝난 리스너는 구독을 해제해주거나,
    • once 메서드를 사용하는 것입니다.

활용 예시

const { EventEmitter } = require("events");
const { readFile } = require("fs");

function findRegex(files, regex) {
    const emitter = new EventEmitter();

    for (const file of files) {
        readFile(file, "utf8", (err, content) => {
            if (err) {
                return emitter.emit("error", err);
            }

            emitter.emit("fileread", file, content);
            const match = content.match(regex);
            if (match) {
                match.forEach((el) => emitter.emit("found", file, el));
            }
        });
    }
    return emitter;
}

findRegex(["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}`))
    .on("error", (err) => console.error(`Error emitted ${err.message}`));
const { EventEmitter } = require("events");
const { readFile } = require("fs");
const log = console.log;
class FindRegex extends EventEmitter {
    constructor(regex) {
        super();
        this.regex = regex;
        this.files = [];
    }

    addFile(file) {
        this.files.push(file);
        return this;
    }

    find() {
        for (const file of this.files) {
            readFile(file, "utf8", (err, content) => {
                if (err) {
                    return this.emit("error", err);
                }

                this.emit("fileread", file);

                const match = content.match(this.regex);
                if (match) {
                    match.forEach((el) => this.emit("found", file, el));
                }
            });
            return this;
        }
    }
}

const findRegexInstance = new FindRegex(/hello \w+/g)
    .addFile("fileA.txt")
    .addFile("fileB.json")
    .find()
    .on("found", (file, match) => log(`Matched "${match}" in file ${file}.`))
    .on("error", (err) => console.error(`Error emitted ${err.message}`));

같은 코드인데, 하나는 클래스 형태로 작성된 예제에요.

사실 이 코드는, 동기적이라고 가정할 경우에는 제대로 동작할 거라고 볼 수 없어요.

왜냐하면 find를 실행한 다음에, 이벤트를 등록하고 있기 때문이에요.

하지만 비동기적이라고 한다면,

지금 현 스택의 코드가 동작한 다음에, 이벤트 루프가 넘어가면서 동작할 거란 걸 알 수 있죠.

그래서 이 코드들은 정상적인 동작을 보장받고 있습니다.

findRegexInstance
	.addFile(...)
	.find() // find 이후에 이벤트를 등록함에도 정상적으로 동작하는 이유를 위에 설명했다.
	.on('found', ...)

EventEmitter와 콜백 중 어느 걸 써야 하는가?

사실 둘은 기능적으로는 같습니다.

둘의 사용 시점을 구분하기 위해서는 가독성, 의미, 코드의 양을 생각하는 게 낫습니다.

콜백은 여러 유형의 결과를 전달할 수 없고, 단 한 번만 실행됩니다.

또 여러 개의 콜백을 둬서 각 이벤트마다 따로 처리해야 한다면 EventEmitter가 낫습니다.

여러 번 발생하거나 혹은 발생하지 않을 수도 있는 이벤트인 경우에도 EventEmitter가 낫고요.

profile
자바스크립트를 좋아하는 "백엔드" 개발자

0개의 댓글