옵서버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
Observer(관찰자)
- 상태 변화를 감지하는 대상이다.
- 옵저버에는 함수나 객체 모두 등록이 가능하다.
Obervable(객체)
- 상태가 변경되는 대상이다.
- subscribe, unsubscribe, notify 등 행동을 처리하는 메서드를 보유하고 있어야 한다.
옵저버 패턴의 핵심은 의존성을 낮추는 것(결합도를 낮추는 것)이다.
옵저버 패턴에서는 이벤트 중심으로 호출관계 흐름이 한 방향으로 진행된다.
예상할 수 있듯이, Observable에서 Observer로의 방향이다.
옵저버는 마치 구독 시스템의 push 알람처럼, 구독 대상의 변화에 의한 push 방식으로 데이터를 얻을 수 있다.
옵저버는 그저 구독 리스트에 등록만 되어 있다면, 옵저버블로부터 정보를 받아볼 수 있는 것이다.
옵저버 패턴에서는, 옵저버들이 구독하려는 대상(객체)를 알고 있으며, 객체들도 옵저버들의 목록을 알고 있다.
또한 객체 간 강한 결합(A의 상태 변화시 직접 B의 상태를 변경시키는 구체적인 코드)
보다 객체 간의 관계를 느슨한 결합
으로 만들 수 있으며 객체의 상태 변화를 옵저버에서 자동으로 알 수 있고 의존 관계가 1:N(객체 : 옵저버들) 이기 때문에 옵저버를 여러 개 만들 수도 있다.
옵저버 패턴은 대부분 동기방식으로 동작하도록 설계한다.
만약 Youtube라는 객체와 Tom, Jane, James라는 옵저버 객체가 있다면
class Youtube{
...
notifySubscriber(data){
tom.update(data);
jane.update(data);
james.update(data);
}
}
다음과 같은 구조를 지닐 수 있겠다.
이 때 예상 가능한 문제점으로는,
- Youtube 객체와 각각의 옵저버 객체들은 강한 결합을 가진다.
- Observer.update() 형태로 동일한 코드가 반복된다.
- 새로운 옵저버 객체를 등록하기 위해서는 클래스를 직접 수정해야 한다.
이는 class 객체 내부에서 직접적으로 외부 객체를 사용하면서 발생하는 문제이기 때문에
이 관계를 느슨한 결합으로 만들어 줄 필요가 있다.
이 때 옵저버 패턴을 이용할 수 있다는 것이다.
class Youtube{
constructor(){
this.observer=null;
}
register(observer){
this.observer=observer;
}
notifySubscriber(data){
this.observer.update(data);
}
}
class Observer{
update(data){
...
}
}
class Jane extends Observer{
update(data){
... // 각 각의 옵저버가 해야할 일들
}
}
느슨한 결합은 객체 내부에서 객체를 직접적으로 사용하는 의존성(=강한 결합)을 줄이려는 시도이다.
강하게 결합되어 있다는 의미는 A의 변화에 B도 맞춰서 변해야 한다는 의미이다.
강한 결합은 객체의 유연성과 코드 재사용성을 떨어뜨린다.
위에서 다룬 Youtube 객체와 Observer 객체들은 현재 강한 결합을 가지고 있다.
Youtube 객체가 Observer의 메서드를 실행하기 위해서는 각 Observer들의 구현사항을 알고 있어야 한다.
그러므로 Observer들의 구현사항에 변화가 생긴다면 Youtube 객체가 그에 맞춰 바뀌어야 한다는 것이다.
객체가 느슨한 결합으로 상호작용한다는 것은, 서로에 대해 잘 모른다는 것을 의미한다. 물론, 잘 모른다는 것이지 아예 모른다는 것은 아니다.
기본적으로 두 객체가 소통하기 위한 공통 규약등을 정해야 하며, 인터페이스가 있는 언어에서는 인터페이스에 이를 정의하는 편이다.
느슨한 결합은 시스템의 유지보수를 더욱 용이하게 하고, 전체 프레임워크를 안정적으로 만들며 시스템 유연성을 증가시키곤 한다.
class Observer () {
update() {
}
}
class Cat extends Observer () {
update() {
console.log('meow~')
}
}
class Dog extends Observer () {
update() {
console.log('bark!')
}
}
class Owner () {
constructor() {
this.animals = []
}
register (animal) {
this.animals.push(animal)
}
notify () {
this.animals.forEach(animal => animal.update())
}
}
const owner = new Owner()
const cat = new Cat()
const dog = new Dog()
owner.register(cat)
owner.register(dog)
owner.notify()
// meow~
// bark!
class Subject {
constructor() {
this.observers = [];
}
getObserversList() {
return this.observers;
}
subscribe(observer) {
this.observers.push(observer)
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notifyAll() {
this.observers.forEach(subscriber=>{
try {
subscriber.update(this.constructor.name)
} catch (err) {
console.log(err)
}
})
}
}
class Observer {
constructor(name) {
this.name = name
}
update(subj) {
console.log(`${this.name}:notified from ${subj} class!`)
}
}
const subj = new Subject();
const a = new Observer('A');
const b = new Observer('B');
const c = new Observer('C');
console.log(subj.getObserversList())
// [Observer { name : 'A'}, Observer { name : 'B'},Observer { name : 'C'}]
subj.notifyAll()
//A:notified from Subject class!
//B:notified from Subject class!
//C:notified from Subject class!
subj.unsubscribe(c)
subj.notifyAll()
//A:notified from Subject class!
//B:notified from Subject class!