Observer Pattern

박세영·2022년 8월 14일
0

Observer 패턴, 줄여서 Observer 패턴,은 여러개의 모듈을 직접적으로 의존하지 않고도 소통할 수 있도록 고안되었다. Observer 패턴은 서비스의 결합도를 낮추는 좋은 방법이고 JS에서 흔히 사용된다.

다만, 디커플링이 무조건 추구해야할 것은 아니다. 잘 커플링된 모듈은 가독성이 좋고, 오히려 관리하기 수월하다.

The Observer Pattern In Action

코드를 직접 작성해가며 Observer 패턴에 대해 학습해보자. Observer.js라는 파일을 생성하고 아래와 같이 코드를 작성해 보자.

let subscribers = {};
module.exports = {
    publish() {
        // method to publish an update
    },
    subscribe() {
        // method to subscribe to an update
    }
};

먼저, subscirbers라는 객체를 쌩성한다. subscirbers 객체에서는 등록된 subscriber 콜백들을 관리한다. 이 객체 안에서, 우리는 key-value 형식의 이벤트들을 저장하고 관리할 것이다. 각 이벤트는 이벤트 이름과 값으로 이루어진 배열을 key로 갖는다. 우리는 이 배열에 subscriber 콜백을 등록하고 저장할 것이다. 이 콜백들은 이벤트가 트리거 될 때마다 발동된다.

다음으로 Observer 모듈은 두개의 함수를 export한다. publish는 업데이트를 발행하고, subscribe는 업데이트를 구독한다.

먼저 subscribe 메서드에 집중해보자. 이 메서드는 다른 모듈에서 등록과 구독 콜백에 사용된다.

subscribe 메서드는 두개의 인수를 받는다. 첫 번째는 구독될 이벤트의 이름이고 두 번째는 이벤트가 발행될 때 동작될 콜백함수이다.

subscribe(event, callback) {
    if (!subscribers[event]) {
        subscribers[event] = [];
    }
    subscribers[event].push(callback);
}

위 코드에서, 우리는 우선 요청된 문의된 이벤트가 subscribers 객체에 존재하는지 체크한다. 만약 없다면, 해당 이벤트에 대해 아직 등록이되지 않았다는 의미이며, 따라서 먼저 이벤트 이름을 subscribers의 키로 등록을 해준 다음 빈 배열을 값으로 넣어주어야 한다. 마지막으로 subscriber callback을 이벤트의 배열에 추가한다.

다음으로 publish 매서드이다.

publish(event, data) {
    if (!subscribers[event]) return;
    subscribers[event].forEach(subscriberCallback =>
        subscriberCallback(data));
}

먼저, subscribers에 현재 발행된 이벤트가 존재하는지 확인한다. 만약 없다면 return해준다. 만약 발행한 이벤트가 subscribers에 존재한다면, 그 이벤트에 해당하는 subscribe callback들을 loop문을 통해 실행시켜 준다. subscribe callback을 실행할때 인수로 받은 data 역시 함께 넘겨준다. data 인수는 optional이다.

이제 우리는 pubsub 모듈을 사용할 모듈을 만들 수 있다. 두 개의 파일을 생성한 후 이름을 각각 moduleA.js, moduleB.js라고 지어보자.

이 예시에서 moduleA.js은 publisher가 되고, moduleB.js는 subscriber가 된다. 곧 알게되겠지만, 두 개의 모듈은 서로의 존재를 인지하지 못하며, pubsub모듈을 통해 소통할 것이다.

// ModuleA
const pubSub = require("./pubsub");
module.exports = {
    publishEvent() {
        const data = {
            msg: "TOP SECRET DATA"
        };
        
        pubSub.publish("anEvent", data);
    }
};

MouleA에서 pubsub모듈을 import한 후 publishEvent 메서드를 포함한 객체를 export했다. publishEvent 메서드는 data를 특정하고 pubsub객체의 publish 메서드에 event 이름과 data를 담아 호출했다.

// ModuleB
const pubSub = require("./pubsub");
pubSub.subscribe("anEvent", data => {
    console.log(
        `"anEvent", was published with this data: "${data.msg}"`
    );
});

subscriber 코드는 더 함축적이다. 여기서 우리는 pubsub객체를 import한 이벤트를 구독하기 위해 다음 subscribe 메서드를 호출한다. 이 때, 구독하려는 event의 이름과 subscribe callback을 인자로 전달한다. 이때 callback은 event 배열에 저장되어 이벤트가 발행될 때, 발동된다. 앞서 살펴봤듯이 optional로 제공된 data를 통해 메시지를 콘솔에 찍는다.

이제 마지막으로 엔트리가 될 index.js 파일을 생성한다.

const moduleA = require("./moduleA");
const moduleB = require("./moduleB");
// We use moduleA's publishEvent() method
moduleA.publishEvent();
moduleA.publishEvent();

이곳에서 앞서 구현했던 두 개의 모듈을 import한 다음 moduleApublishEvent()메서드를 호출한다.

콘솔에 찍힌 결과는 아래와 같다.

"anEvent", was published with this data: "TOP SECRET DATA"
"anEvent", was published with this data: "TOP SECRET DATA"

이 코드를 위에서부터 살펴보면, moduleB를 import했을 때, subscribe 메서드가 호출되고 ‘myEvent’라는 이름을 가진 이벤트에 callback을 추가하게 된다.

다음으로 moduleA에서 publishEvent 메서드를 실행하게 되면 미리 등록한 콜백이 발동하게 된다. publishEvent 메서드를 두 번 호출했기 때문에 콘솔에도 data.msg에 대한 로그가 두번 찍히게 된다.

아직 Observer 모듈에서 빠진 로직이 하나 있다. 바로 기존 구독을 취소하는 unsubscribe 메서드이다. 이를 처리하는 좋은 방법 중 하나가 바로 subscribe 메서드가 unsubscribe()함수를 담은 객체를 리턴해주는 것이다.

제거할 callback 함수를 찾아가기 위해 배열의 몇 번째 인덱스에 존재하는지를 알고 있어야 한다. 따라서 Observer.js 파일의 subscribe 메서드를 아래와 같이 리펙터링 해보자.

subscribe(event, callback) {
    let index;
    if (!subscribers[event]) {
        subscribers[event] = [];
    }
    index = subscribers[event].push(callback) - 1;
    
    return {
        unsubscribe() {
            subscribers[event].splice(index, 1);
        }
    };
}

우리는 index라는 새로운 변수를 사용하게 되었다. 다행이도 JS의 push() 메서드는 아이템을 삽입한 후 배열의 길이를 리턴해준다. 우리는 이 리턴 값을 받아 index를 저장해서 관리할 수 있게 되었다.

index를 저장한 후 unsubscribe 메서드를 담은 객체를 리턴해준다. 이 메서드는 subscribers[event] 배열을 받아서 해당 콜백을 삭제한다.

이제 moduleB에서 아래와 같이 unsubscribe 메서드를 사용할 수 있다.

const Observer = require("./Observer");
let subscription;
subscription = Observer.subscribe("anEvent", data => {
    console.log(
        `"anEvent", was published with this data: "${data.msg}"`
    );
    subscription.unsubscribe();
});

먼저, subscription이라는 변수를 추가한다음, subscribe() 메서드의 반환값을 저장해둔다. 이 때 반환된 객체는 unsubscribe메서드를 포함하고 있다.

이제 콘솔에 로그를 찍은 후 필요 없어진 콜백 함수들을 subscription변수에 저장된 객체 안에 있는 subscribe메서드를 호출해서 구독을 취소할 수 있다.

다시 프로그램을 실행 해보면, 이제 하나의 메시지만 콘솔에 찍히는 것을 알 수 있다. 그 이유는 subscriber 모듈이 subscribe를 호출할때 콘솔을 찍은 후 바로 unsubscribe 메서드를 호출하기 때문이다.

여기까지가 JS로 구현한 Observer 패턴이다.

Observer 패턴과 Observer 패턴에 대해 공부하고 나서 다시 이 포스트를 보니 이 포스팅에서 설명한 것은 PubSub이 아닌 Observer 패턴이였다..
진짜 PubSub 구조에대 대해 알아보고 싶다면 Challenge Day16과제를 다시 살펴보자

0개의 댓글