🔎

[JS] 옵저버 패턴 (Observer Pattern)


옵저버 패턴?

옵저버 패턴은 객체의 상태 변화를 관찰하는 옵저버들의 목록을 객체에 등록해서 상태 변화가 발생할 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 즉, 어떤 객체의 상태가 변하면 연관된 객체들에게 알림을 보내는 디자인 패턴이다.

이 패턴의 핵심은 상태를 가진 객체(subject)인 '발행기관(publisher)'에 이 객체를 관찰하는 옵저버들인 '구독자(subscriber)'들을 등록시키는 것이다. 그리고 각각의 구독자들은 발행기관이 발생시키는 이벤트를 받아 처리한다. 이 때문에 '발행/구독 모델'이라고도 한다.

옵저버 패턴은 MVC 패러다임과 자주 결합되어 사용된다. 옵저버 패턴을 사용함으로써 MVC에서 모델과 뷰 사이를 느슨하게 연결할 수 있다.

React의 'Redux'나 Vue의 'Vuex' 같은 중앙 집중식 저장소가 옵저버 패턴에 기반을 둔다.

구현

위키에 있는 UML 다이어그램을 참고해서 상태를 가지는 발행기관을 구현해보자.

발행기관(Publisher)

발행기관은 구독자들을 등록(register), 제거(unregister)하는 기능, 이벤트가 발생했을 때 구독자들에게 알려주는 기능을 가진다.

또한 state에 접근할 수 있도록 Proxy를 통해 get 트랩을 설정했다.

1class Publisher {
2 constructor(state) {
3 this.state = state;
4 this.subscriber = new Set();
5 return new Proxy(this, {
6 get(target, prop) {
7 if(prop in target.state) return target.state[prop];
8 else return target[prop];
9 }
10 });
11 }
12 registerSubscriber(subscriber) {
13 this.subscriber.add(subscriber);
14 }
15 unregisterSubscriber(subscriber) {
16 this.subscriber.delete(subscriber);
17 }
18 notifySubscribers() {
19 this.subscriber.forEach(subscriber => subscriber.notify());
20 }
21 setState(newState) {
22 this.state = { ...this.state, ...newState };
23 this.notifySubscribers();
24 }
25}

구독자(Subscriber)

구독자는 발행기관에서 변화가 생겼을 때 하는 일인 notify를 정의해야 한다.

1class Subscriber {
2 constructor(notify) {
3 this.notify = notify;
4 }
5}

사용

정의한 발행기관과 구독자를 사용보자.

1let state = new Publisher({ a: 1, b: 2 });
2
3let adder = new Subscriber(() => console.log(`a + b = ${state.a + state.b}`));
4let multiplier = new Subscriber(() => console.log(`a * b = ${state.a * state.b}`));
5
6state.registerSubscriber(adder);
7state.registerSubscriber(multiplier);
8
9state.notifySubscribers();
10// -> a + b = 3
11// -> a * b = 2
12state.setState({ a: 3 });
13// -> a + b = 5
14// -> a * b = 6
15state.unregisterSubscriber(multiplier);
16
17state.setState({ b: 7 });
18// -> a + b = 10

발행기관에 구독자를 구독시킨 후 상태를 바꿨을 때, 각 구독자는 변화한 상태에 대해 처리를 해주는 것을 볼 수 있다.

위에 구현한 발행기관과 구독자를 그대로 사용하기는 문제가 있다.

발행기관의 state를 직접 접근해 변경할 경우 구독자들이 상태가 변경됐다는 것을 알 수 없다. 이를 방지하기 위해 state를 private 필드로 두거나 클로저를 사용해 숨겨두어야 한다. subscriber도 마찬가지로 숨기는 것이 좋겠다.

또 다른 문제점도 존재한다. 만약 10명의 구독자가 100개의 발행기관에 구독을 해야한다면, 구독 관련 코드가 무려 10 * 100 = 1000개가 필요하다.

개선

문제점을 개선하기 위해 앞서 작성한 코드를 observableobserve의 관계로 단순화 해보자.

observableobserve에서 사용되고, observable에 변화가 생기면 observe에 등록된 함수가 실행되는 것이다.

1const state = observable({ a: 1, b: 3 });
2observe(() => console.log(`a + b = ${state.a + state.b}`));
3observe(() => console.log(`a * b = ${state.a * state.b}`));
4state.a = 3;
5// -> a + b = 5
6// -> a * b = 6

구현

1let requestingObserver = null;
2
3const observable = (object) => {
4 const observersPerProps = new Map();
5 return new Proxy(object, {
6 get(target, prop) {
7 if (!observersPerProps.has(prop)) observersPerProps.set(prop, new Set());
8 if (requestingObserver) observersPerProps.get(prop).add(requestingObserver);
9 return target[prop];
10 },
11 set(target, prop, val) {
12 if (target[prop] === val) return true;
13 if (JSON.stringify(target[prop]) === JSON.stringify(val)) return true;
14 target[prop] = val;
15 observersPerProps.get(prop).forEach((notify) => notify());
16 return true;
17 }
18 });
19};
20
21const observe = (notify) => {
22 requestingObserver = notify;
23 notify();
24 requestingObserver = null;
25};
26
27const state = observable({ a: 3, b: 3 });
28
29observe(() => console.log(`a + b = ${state.a + state.b}`));
30// -> a + b = 6
31observe(() => console.log(`a * b = ${state.a * state.b}`));
32// -> a * b = 9
33state.a = 5;
34// -> a + b = 8
35// -> a * b = 15
36state.a = 5;
37// 아무것도 출력되지 않는다.

이전과 다른 점은 옵저버가 등록되는 방식이다. observe를 통해 notify를 등록하면 우선 requestingObservernotify를 가리키게 한다. 그 다음 notify를 실행해 observable의 get 트랩에서 requestingObserver를 통해 notify를 옵저버에 추가하게 된다. requestingObserver를 'observablenotify를 등록하기 위한 중계소'라고 생각하면 된다.

옵저버를 추가할 때에는 notify에서 사용되는 프로퍼티에만 알람이 갈 수 있도록 프로퍼티 별로 옵저버를 등록해준다.

state가 변경되었을 때는 변경된 프로퍼티에 등록된 옵저버들에게 변경을 알린다. 만약 변경된 값이 이전 값과 같다면 알림이 가지 않게 방어 로직을 작성해줬다.


참조:
Vanilla Javascript로 상태관리 시스템 만들기
https://ko.wikipedia.org/wiki/%EC%98%B5%EC%84%9C%EB%B2%84_%ED%8C%A8%ED%84%B4
https://lihano.tistory.com/19