옵저버 패턴은 객체의 상태 변화를 관찰하는 디자인 패턴 중 하나입니다.
이 패턴에서는 객체 간의 일대다 의존 관계를 정의하며, 어떤 객체의 상태가 변할 때 해당 객체에 의존하는 다른 객체들에게 자동으로 알림을 보내는 방식을 사용합니다.
이때 상태를 보내는 객체를 주체(subject), 상태를 받는 객체를 옵저버(observer)라고 합니다.
주체 객체의 상태가 변하면 옵저버 객체에게 변화를 알리게됩니다.
주체와 옵저버는 서로 독립적으로 변경될 수 있으며 따라서 느슨한 결합을 유지할 수 있습니다.
느슨한 결합이 주는 이점은 싱글톤 패턴에서 살펴보았습니다.
역시 코드를 보고 살펴봅시다.
// Observable(Subject) class
class NewsAgency {
constructor() {
this.news = '';
this.observers = [];
}
// method to register observers
addObserver(observer) {
this.observers.push(observer);
}
// method to remove observers
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
// method to notify all observers
notifyObservers() {
for (const observer of this.observers) {
observer.update(this.news);
}
}
// method to set new news and notify all observers
setNews(news) {
this.news = news;
this.notifyObservers();
}
}
// Observer interface
class Observer {
update(news) {}
}
// Concrete Observer classes
class NewsChannel1 extends Observer {
update(news) {
console.log(`News Channel 1: ${news}`);
}
}
class NewsChannel2 extends Observer {
update(news) {
console.log(`News Channel 2: ${news}`);
}
}
// Create a news agency object
const agency = new NewsAgency();
// Create two news channel observer objects and register them to the news agency object
const channel1 = new NewsChannel1();
const channel2 = new NewsChannel2();
agency.addObserver(channel1);
agency.addObserver(channel2);
// Set a new news on the news agency object
agency.setNews('Breaking News: Earthquake hits the city');
// Output:
// News Channel 1: Breaking News: Earthquake hits the city
// News Channel 2: Breaking News: Earthquake hits the city
위의 코드에서 NewsAgency클래스가 Observer입니다.
addObserver(), removeObserver(), notifyObservers() 메소드는 관측자 객체(Observer)를 등록, 제거, 알림을 위한 메소드입니다.
channel1과 channel2객체는 agency객체에 의존해 있습니다.
(news를 인자로 받고 있기 때문)
따라서
agency.setNews('Breaking News: Earthquake hits the city');
이렇게 setNews()메소드를 통해 새로운 상태를 알려주게 되면 의존하고 있는 모든 객체(channel1, channel2)들이 변화를 인지하고 바뀌게 됩니다.
proxy 객체란 어떠한 대상의 기본적인 동작(속성 접근, 할당, 순호, 열거, 함수 호출 등)의 작업을 가로챌 수 있는 객체를 뜻합니다.
프록시 객체는 두 개의 매겨변수를 가집니다.
- target: 프록시할 대상
- handler: 프록시 객체의 target 동작을 가로채서 정의할 동작들이 정해져 있는 함수
javaScript에서는 프록시 객체를 활용하여 옵저버 패턴을 구현할 수 있습니다.
class Observable {
constructor() {
this.observers = new Set()
return new Proxy(this, {
get: function(target, key, receiver) {
if (key === 'subscribe') {
return function(observer) {
target.observers.add(observer)
return {
unsubscribe: function() {
target.observers.delete(observer)
}
}
}
} else {
return Reflect.get(target, key, receiver)
}
}
})
}
notify(data) {
this.observers.forEach(observer => observer(data))
}
}
// Example usage
const observable = new Observable()
const subscription1 = observable.subscribe(data => console.log(`Subscription 1 received data: ${data}`))
const subscription2 = observable.subscribe(data => console.log(`Subscription 2 received data: ${data}`))
observable.notify('Hello, observers!')
subscription1.unsubscribe()
observable.notify('Goodbye, subscription 1!')
위의 코드에서 Proxy객체의 handler가 'get'임을 확인할 수 있습니다.
따라서 해당 Proxy객체는 Observable클래스의 객체의 속성에 접근할 때마다 호출됩니다.
'key'인자가 'subscribe'라면 생성된 'observes' set에 추가합니다. 그리고 'unsubscribe()' 메소드를 가진 객체가 반환됩니다.
즉, subscribe를 하면 unsubscribe를 할 수 있게 된다는 뜻입니다.
따라서 .subscribe()로 생성된 객체를 .unsubscribe()로 의존성을 지울수 있게됩니다.
마지막으로 observable.notify('something')을 하게 되면 .subscribe()로 생성된 객체에 직접 접근하지 않아도 해당 객체들에 알림을 보낼 수 있게 되는 것입니다.
const subscription1 = observable.subscribe(data => console.log(`Subscription 1 received data: ${data}`))
위 처럼 객체를 생성할 때 안에 써주는 콜백 함수는 notify() 메소드가 실행될 때 실행되게 됩니다.
그렇다면 Proxy 객체를 사용한 옵저버 패턴이 갖는 장점과 단점을 살펴봅시다.
1. 코드의 가독성 및 유지보수성이 향상됩니다.
Proxy객체를 사용하면 구독 및 구독 해지와 같은 관찰자 패턴의 핵심 기능을 간단하게 구현할 수 있습니다. 이것은 Observer Pattern의 코드를 더 간결하고 읽기 쉽게 만듭니다. 또한 Proxy객체는 다른 객체와 독립적으로 존재하기 때문에 관찰 대상의 상태 변경으로 인해 다른 객체들이 영향을 받지 않습니다.
2. 런타임 오류 방지
프록시 객체는 기본적으로 엄격한 모드에서 작동하기 때문에, 코드의 안전성을 향상시켜 런타임 오류를 방지하는 효과가 있습니다.
3. 향상된 성능
프록시 객체는 상태 변경을 추적하여 전체 객체를 재생성하지 않고도 구독자의 추가 또는 제거와 같은 작업을 수행할 수 있습니다. 따라서 성능이 향상됩니다.
1. Proxy객체를 사용하면 코드가 복잡해질 수 있습니다.
Proxy객체를 사용하는 Observer Pattern은 ES6의 Proxy객체를 사용하기 때문에 이를 익히지 않은 개발자들에게는 이해하기 어려울 수 있습니다.
2. 일부 브라우저에서는 Proxy객체를 지원하지 않을 수 있습니다.
Proxy객체는 ES6에서 처음 도입되었으며, 모든 브라우저에서 지원하지는 않습니다.
마지막으로 Observer Pattern의 장점과 단점을 살펴봅시다.
1. 느슨한 결합
Subject와 Observer는 서로 독립적으로 존재하며 서로의 구현 내용을 알지 못합니다. 이로 인해 코드 간의 결합도가 낮아져 유지보수와 확장성이 좋아집니다.
2. 단일 책임 원칙
Observer 패턴을 사용하면 각 클래스가 자신의 역할과 책임을 갖게 되어 코드의 가독성과 유지보수가 향상됩니다.
3. 이벤트 핸들링
이벤트를 발생시키는 객체와 이벤트를 처리하는 객체를 분리함으로써 이벤트 핸들링 코드를 보다 쉽게 작성할 수 있습니다.
1. 메모리 누수
Observer Pattern을 사용하면 옵저버 객체가 서브젝트 객체에 등록된 채로 계속 유지됩니다. 따라서 등록을 해제하지 않는 경우 메모리 누수가 발생할 수 있습니다.
2. 동시성 이슈
멀티 스레드 환경에서 Subject객체가 동시에 여러 개의 Observer에게 통지를 할 경우 문제가 발생할 수 있습니다. 이를 해결하기 위해 동기화 처리가 필요합니다.
3. 이벤트 처리 순서
Observer Pattern을 사용하면 이벤트 핸들링 코드의 처리 순서가 보장되지 않을 수 있습니다. 이를 보완하기 위해 Observer Pattern을 확장한 다른 패턴들이 존재합니다.