1. 옵저버(Observer) 패턴
옵저버 패턴을 이용하면 객체 간의 '일대다' 의존 관계를 쉽게 구축할 수 있습니다. 옵저버 패턴의 메커니즘을 이용하면 이러한 의존 관계의 동적 유지보수를 매우 쉽게 구현할 수 있습니다.
(출처: AnYuQingYang: 디자인 패턴 - 옵저버 패턴(Observer Pattern))
옵저버 패턴은 주제(Subject)와 옵저버(Observer)를 분리합니다. 주제는 옵저버의 세부 사항을 알지 못하며, 모든 옵저버가 update 인터페이스를 가지고 있다는 것만 압니다. 옵저버는 주제에 대해 더 적은 정보를 가지며, 주제의 업데이트를 수동적으로 기다릴 뿐입니다. 각 옵저버들은 서로의 존재조차 모르며, 동일한 주제를 구독하고 있는지조차 알지 못합니다.
느슨한 결합은 유연성(탄력성)을 의미합니다. 정해진 인터페이스를 구현하기만 하면 주제와 옵저버를 서로 독립적으로 수정할 수 있으며, 이들의 상호작용 관계에 영향을 주지 않습니다.
2. 고전적인 옵저버 패턴
옵저버 패턴의 개념은 비교적 간단합니다. 고전적인 구현에는 한 단계의 추상화(Observer, Subject의 인터페이스 정의)가 필요합니다. JavaScript로 구현한 간단한 옵저버 패턴은 다음과 같습니다.
/* 고전적인 옵저버 패턴
* Observer, Subject
* 한 단계의 추상화를 추가할 수도 있지만, JS 상속은 번거로우므로 여기서는 간략하게 진행합니다.
* 리스트를 직접 관리합니다.
*/
// Subject
function Subject(name) {
this.name = name;
this.list = [];
}
// 구독
Subject.prototype.regist = function(obj) {
this.list.push(obj);
};
// 구독 취소
Subject.prototype.unregist = function(obj) {
var list = this.list;
for(var i = 0, len = list.length; i < len; i++) {
if (list[i] === obj) {
list.splice(i, 1);
}
}
};
// 모든 구독자에게 알림
Subject.prototype.notify = function() {
var list = this.list;
for(var i = 0, len = list.length; i < len; i++) {
list[i].update({data: this.name});
}
};
// Observer
function Observer(id) {
this.id = id;
// Subject에서 호출할 업데이트 메서드
this.update = function(dataObj) {
// ...
console.log(this.id + ' received ' + dataObj.data + '\'s update');
};
}
// 테스트
var subject = new Subject('My Topic');
var observer1 = new Observer(1);
var observer2 = new Observer(2);
var observer3 = new Observer(3);
subject.regist(observer1);
subject.regist(observer2);
subject.notify(); // 주제 업데이트, 모든 옵저버에게 알림
subject.regist(observer3); // observer3 새로 추가
subject.notify();
// observer1 이탈
subject.unregist(observer1);
subject.notify();
실행 결과는 다음과 같습니다.
[caption id="attachment_663" align="alignnone" width="239"]
옵저버 패턴[/caption]
P.S. 1단계 추상화에 관심이 있다면 AnYuQingYang: JS의 6가지 상속 방식 다시 이해하기를 참고하여 직접 구현해 보세요.
3. 발행/구독(Publish/Subscribe) 패턴
옵저버 패턴은 주제와 옵저버를 분리하지만, 상호작용 관계는 양측 내부에 내장되어 있습니다(정해진 인터페이스를 구현해야 함). 이 상호작용 관계마저 분리해낸다면 그것을 발행/구독 패턴이라고 부릅니다.
발행/구독 패턴은 주제와 옵저버 사이에 별도의 이벤트 메커니즘을 추가합니다. 이벤트 메커니즘이 양측에 인터페이스를 제공하고 상호작용 관계를 유지합니다. 이렇게 계층을 하나 더 두면 이벤트 큐, 이벤트 버블링(DOM에만 있는 게 아니랍니다!) 등 더 많은 제어 기능을 도입할 수 있다는 장점이 있습니다.
여기까지 오면 거의 완벽하게 결합을 분리한 셈입니다. 엄청난 유연성을 제공하지만 피할 수 없는 단점도 존재합니다. 예를 들어 옵저버 하나가 충돌했을 때, 결합이 분리되어 있기 때문에 이 소식이 밖으로 전달되지 않습니다. 주제도 모르고, 주변 옵저버도 모르며, 이 옵저버에 의존하는 다른 모듈도 알 수 없게 됩니다. (이론적으로 이벤트 관리 메커니즘을 강화하여 제어할 수는 있지만, 이는 이벤트 계층을 점점 더 복잡하고 비대하게 만듭니다.)
4. 사용자 정의 이벤트 메커니즘을 통한 발행/구독 패턴 구현
주의: '이벤트' 자체가 아니라 '이벤트 메커니즘'입니다. 사용자 정의 이벤트는 DOM API이므로 DOM 요소에만 바인딩할 수 있기 때문입니다. 자세한 내용은 Document.createEvent() - Web API Interfaces | MDN을 확인하세요.
따라서 '이벤트 메커니즘'을 직접 정의해야 합니다. 구현 세부 사항은 AnYuQingYang: JS 학습 노트 11_고급 기교 하단을 확인하세요.
DOM 레벨 3 API는 사용자 정의 이벤트 생성을 지원합니다. 내용은 다음과 같습니다.
// 사용자 정의 이벤트 객체 생성
var event = document.createEvent('CustomEvent');
// 이벤트 초기화
event.initCustomEvent('myeve', true, true);
// initCustomEvent 인터페이스 정의는 다음과 같습니다.
/*
void initCustomEvent(
in DOMString type,
in boolean canBubble,
in boolean cancelable,
in any detail
);
*/
document.createEvent와 initCustomEvent는 이미 구식이 되었습니다. 이제는 생성자를 사용하여 사용자 정의 이벤트 객체를 직접 만들 수 있습니다 (JavaScript CustomEvent 예제 참고). 물론 현재(2015-07-13)로서는 구식 방식의 호환성이 가장 좋습니다. 자세한 내용은 Document.createEvent() - Web API Interfaces | MDN을 확인하세요.
[IE8-]와의 호환성이 필요하다면 다른 수단을 강구해야 합니다. 여기서는 자세히 다루지 않으니 JavaScript 사용자 정의 이벤트를 확인하세요.
P.S. 프로젝트에서 주요 JS 라이브러리(프레임워크)를 사용 중이라면 보통 사용자 정의 이벤트를 지원하지만, 대부분 DOM 요소와 바인딩됩니다. 비 DOM 요소 객체의 이벤트 메커니즘은 직접 구현해야 합니다. 아주 가벼운 발행/구독 구현체인 addyosmani/pubsubz를 참고해 보세요. 코드가 매우 정교하여 살펴볼 가치가 있습니다.
5. 옵저버 패턴의 구체적인 활용
옵저버 패턴의 매우 중요한 활용 사례 중 하나는 Ajax 요청의 콜백 로직입니다. 주제는 Ajax 요청이 반환하는 데이터이고, 옵저버는 콜백 함수 내의 각 로직 블록입니다(리스트를 통해 실행 순서를 보장할 수 있습니다). 콜백 함수에서 모든 옵저버에게 알림(notify)만 주면 됩니다. 예제 코드는 다음과 같습니다.
// 발행/구독 패턴 구현 출처: https://github.com/addyosmani/pubsubz
(function(window) {
var topics = {},
subUid = -1,
pubsubz = {};
pubsubz.publish = function(topic, args) {
if (!topics[topic]) {
return false;
}
var subscribers = topics[topic],
len = subscribers ? subscribers.length : 0;
while (len--) {
subscribers[len].func(topic, args);
}
return true;
};
pubsubz.subscribe = function(topic, func) {
if (!topics[topic]) {
topics[topic] = [];
}
var token = (++subUid).toString();
topics[topic].push({
token: token,
func: func
});
return token;
};
pubsubz.unsubscribe = function(token) {
for (var m in topics) {
if (topics[m]) {
for (var i = 0, j = topics[m].length; i < j; i++) {
if (topics[m][i].token === token) {
topics[m].splice(i, 1);
return token;
}
}
}
}
return false;
};
window.pubsubz = pubsubz;
}(this));
// 구체적인 활용
var psz = pubsubz;
var eventName = 'DataUpdate';
var obs1 = psz.subscribe(eventName, function(data) {
console.log('obs1 received ' + data);
console.log('무효 데이터 삭제');
});
var obs2 = psz.subscribe(eventName, function(data) {
console.log('obs2 received ' + data);
console.log('새 데이터 표시');
});
var obs3 = psz.subscribe(eventName, function(data) {
console.log('obs3 received ' + data);
console.log('아이콘 리스트 업데이트');
});
// Ajax 시뮬레이션
function ajax(url, callback) {
// 50ms 후 콜백 실행 (데이터를 이미 받았다고 가정)
setTimeout(function(data) {
data = 'data'; // 시뮬레이션 데이터
callback(data);
}, 50);
}
ajax('emptyUrl', function(data) {
psz.publish(eventName, data);
});
// 실행 결과는 다음과 같습니다.
// obs3 received DataUpdate
// 아이콘 리스트 업데이트
// obs2 received DataUpdate
// 새 데이터 표시
// obs1 received DataUpdate
// 무효 데이터 삭제
// (역순인 이유는 pubsubz.publish가 감소 반복문을 사용하기 때문입니다)
물론 이 방식도 로직이 여러 옵저버로 분산되어 코딩 난이도가 높아지는 등의 문제가 생길 수 있습니다(이벤트 기반 방식의 단점과 유사). 사용 시 구체적인 상황에 맞춰 장단점을 따져보아야 합니다.
참고 자료
-
《JavaScript 디자인 패턴》
-
관련 블로그 포스트 다수
아직 댓글이 없습니다