Observer 패턴, 줄여서 Observer 패턴,은 여러개의 모듈을 직접적으로 의존하지 않고도 소통할 수 있도록 고안되었다. Observer 패턴은 서비스의 결합도를 낮추는 좋은 방법이고 JS에서 흔히 사용된다.
다만, 디커플링이 무조건 추구해야할 것은 아니다. 잘 커플링된 모듈은 가독성이 좋고, 오히려 관리하기 수월하다.
코드를 직접 작성해가며 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한 다음 moduleA
의 publishEvent()
메서드를 호출한다.
콘솔에 찍힌 결과는 아래와 같다.
"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과제를 다시 살펴보자