옵저버 패턴(Observer Pattern)이란?
옵저버패턴(Observer Pattern)이란 객체의 상태 변화를 관찰하는 옵저버들(관찰) 목록을 객체에 등록하여 상태 변화가 있을 때마다 notify 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다.
어떤 객체의 상태가 변할 때 그와 연관된 객체 들에게 알림을 보내는 디자인 패턴 이라고 생각하면 된다.
옵저버 패턴에는 주체 객체와 상태의 변경을 알아야 하는 관찰 객체(Observer Object)가 존재하며 이들의 관계는 1:1이 될 수도 있고 1:N이 될 수도 있다.
- 주로 분산 이벤트 핸들링 시스템을 구현한다.
- 발행/ 구독 모델로 알려져 있기도 하다.
notify 함수는 관찰 대상이 발행한 메시지 이외에, 옵저버 자신이 생성한 인자값을을 전달 가능하다.
각각의 파생 옵저버는 notify 함수를 구현함으로써 이벤트가 발생했을 때 처리할 각각의 동작을 작성할 수 있다.
옵저버 패턴은 MVC(Model-View-Contrllor)패러다임과 자주 결합된다. 옵저버 패턴은 MVC에서 모델과 뷰 사이를 느슨히 연결하기 위해 사용된다.
대표적으로 모델에서 일어나는 이벤트를 통보받는 옵저버는 뷰의 내용을 바꾸는 스위치를 작동시킨다.
실생활 예시
객체의 상태 변화를 관찰하는 옵저버(관찰자)들의 목록을 객체(유튜버)에 등록하여 상태 변화가 있을 때마다 notify를 통해 객체(유튜버)가 직접 목록의 각 옵저버들에게 통지하도록 할 수 있다.
즉, 유튜버가 이벤트를 올리면 해당 유튜버 구독자들은 알림을 받을 것이다!!
옵저버 패턴 예시
크루(학생)들은 코치가 하는 일들을 모두 notify(알림) 받아야 한다. 즉, 코치가 "밥을 먹는다"면 모든 크루들은 코치가 밥을 먹었다는 것을 알아야 한다. 코치가 "농땡이를 친다"면 모든 크루들은 코치가 농땡이를 치는 것을 알아야 한다.
베디라는 객체는 코치이다. (코치 인터페이스를 구현해야 한다)
코치(코치 인터페이스)의 기능은 크루들을 등록한다, 크루들을 등록 해제한다, 크루들에게 행동을 알린다. 아주 간단하게 이 세가지의 기능을 가지고 있다.
이렇게 코치인 베디는 모든 크루들에게 정보를 알려야 한다.
크루(크루 인터페이스)의 기능은 자신의 상태를 업데이트하는 기능을 가진다. 아주 간단하게 하나의 기능을 가지고 있다고 하자.
위의 도식대로 인터페이스를 정의해보자.
이렇게 두 개의 인터페이스를 정의할 수 있겠다.
그리고 Coach를 구현하는 베디 클래스를 만들어보자.
베디(코치)는 Crew들의 리스트를 가지고 있고, 세 가지 기능을 가진다. 밥 먹기, 농땡이 치기, 귀여워 지기
그리고 인터페이스에 정의된 대로 3개의 함수를 구현한다. 주목할 것은 notifyCrew 메서드를 각 기능에서 호출한다는 것이다. 그리고 크루들에게 한 명씩 업데이트 메서드를 호출하게 한다. (이 부분이 알림을 보내는 부분이다)
티버(크루)는 베디(코치)의 알림을 받고 싶어서 구독을 하고 싶어 한다.
다른 크루들 르윈, 제이도 마찬가지다. 클래스 다이어그램으로 나타내보면 다음과 같다.
이제 메인함수를 만들어보자.
코치인 베디에게 3명의 크루들이 구독을 하였다. 그리고 upgradeCutie() 메서드를 호출한다. 그렇게 되면 구독한 3명의 크루 객체들에게 메세지가 전달된다.
베디코치가 귀여움을 강화했다
Lewin 수신 : 나 더 귀여워 졌따
Tiber 수신 : 나 더 귀여워 졌따
Jay 수신 : 나 더 귀여워 졌따
이에 Lewin은 구독을 해지한다.
baedi.unsubscribe(lewin);
baedi.eatFood();
베디코치가 밥을 먹는다
Tiber 수신 : 나 밥 먹었따
Jay 수신 : 나 밥 먹었따
결국 Lewin은 알림 대상에서 제외가 된다.
(첨언으로, 구독을 해지하는 주체가 잘못되었다고 지적할 수 있다. 지금은 아주 간단한 예시이고 추후 자바에서 구현한 클래스에서는 구독자가 해지할 수 있도록 this 를 넘겨주는 부분이 있으므로 코드를 참조하자)
이로써 객체의 상태가 변화될 때 연관된 객체 들에게 알림을 보낼 수 있게 된다. 이것이 옵저버 패턴이다.
조금 더 "옵저버" 답게 이름을 리팩토링을 하게 된다면 Crew 인터페이스는 Observer 인터페이스가 되겠고, Coach 인터페이스는 Observable 인터페이스가 되겠다.
다시 말해 Observable 은 발행자가 되고 Observer는 구독자가 된다.
Java 의 옵저버
자바에서 제공하는 옵저버가 있을까? 위의 예제는 아주 간단히 구현해본 것이므로 멀티 스레드환경에서는 취약할 것이다. 그것을 보완하고 조금 더 상세히 구현한 옵저버 관련 클래스들이 있다.
[Java Platform SE8 Observable]
Observer 인터페이스와 Observable 클래스를 제공한다. 주목할 점은 Observable 은 클래스 라는 점이다.
위의 예제처럼 구현한다면 베디 코치의 클래스는 다음과 같은 식으로 변경될 것이다.
notifyObservers라는 메서드는 Observable에 존재하는 메서드이다. 인자는 Object로 받게 되고 구독자들에게 저 Object들을 전달하게 된다. setChanged()는 내부 플래그를 true 로 만들어 알림이 동작하게 끔 한다. 그 이유는 내부코드를 보니 저 flag를 동기화 lock을 푸는 key로써 사용한 것으로 보인다. 밑에 코드를 첨부했으니 참고 바란다.
public class Observable {
private boolean changed = false;
private Vector<Observer> obs;
/** Construct an Observable with zero Observers. */
public Observable() {
obs = new Vector<>();
}
// ... 중략
실제로 Observable 의 모습인데 Observer 들의 집합을 가지고 있고 changed 라는 flag 변수를 가지고 있다.
옵저버를 추가하는 함수는 다음과 같다.
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o);
}
}
옵저버의 배열도 set과 같은 형태로 동작하게 끔 구현한 듯 하다.
notfiy하는 부분을 보면,
public void notifyObservers(Object arg) {
/*
* a temporary array buffer, used as a snapshot of the state of
* current Observers.
*/
Object[] arrLocal;
synchronized (this) {
/* We don't want the Observer doing callbacks into
* arbitrary code while holding its own Monitor.
* The code where we extract each Observable from
* the Vector and store the state of the Observer
* needs synchronization, but notifying observers
* does not (should not). The worst result of any
* potential race-condition here is that:
* 1) a newly-added Observer will miss a
* notification in progress
* 2) a recently unregistered Observer will be
* wrongly notified when it doesn't care
*/
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
다음과 같이 생겼다. flag 변수를 통해 synchronized 를 보장한다. 주석의 설명을 정리해보면,
벡터를 꺼내고 저장하는 과정은 동기화가 필요하다. 하지만 옵저버들에게 통보하는 것은 동기화가 필요하지 않다. (그래서 동기화 블럭 밖에 update 메서드를 놓은듯 하다)
최악의 잠재적인 레이스 컨디션(Race condition)은 (여기서 레이스 컨디션이란 공유자원에 대해 여러 프로세스가 선점하기 위해 경쟁하는 것을 말한다)
- 막 등록된 옵저버가 알림을 받지 못하는 것
- 막 삭제된 옵저버가 잘못 알림을 받는 것
그리고 마지막에 clearChanged()를 불러서 flag를 다시 리셋 해준다.
그리고 옵저버 인터페이스는 다음과 같고, 위에서 지적한 구독자가 구독을 취소하는 방식으로 구현이 가능해보인다.
public interface Observer {
/**
* This method is called whenever the observed object is changed. An
* application calls an <tt>Observable</tt> object's
* <code>notifyObservers</code> method to have all the object's
* observers notified of the change.
*
* @param o the observable object.
* @param arg an argument passed to the <code>notifyObservers</code>
* method.
*/
void update(Observable o, Object arg);
}
옵저버블 객체도 전달받을 수 있어 조금 더 활용성 높게 코드를 만들 수 있게 된다.
추가적으로, 이 Observable 클래스를 사용하려면 상속을 해야하는데 자바는 다중상속을 지원하지 않기 때문에 한계점이 발생할 수 있다. 그나마 다행인건 패턴을 구현하기가 어렵지 않다는 점이므로 옵저버블을 인터페이스로 만들고 직접 구현하는 것이 확장성이 있을것으로 보인다.
옵저버 패턴의 장단점
장점
개방/폐쇄 원칙
1. 실시간으로 한 객체의 변경사항을 다른 객체에 전파할 수 있다.
2. 느슨한 결합으로 시스템이 유연하고 객체간의 의존성을 제거할 수 있다.
3. 런타임에 객체 간의 관계들을 형성할 수 있습니다.
단점
1. 너무 많이 사용하게 되면, 상태 관리가 힘들 수 있다
2. 데이터 배분에 문제가 생기면 자칫 큰 문제로 이어질 수 있다.
3. 구독자들은 무작위로 알림을 받는다.
다른 패턴과의 관계
- 커맨드, 중재자, 옵서버 및 책임 연쇄 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룬다.
- 책임 연쇄 패턴은 잠재적 수신자의 동적 체인을 따라 수신자 중 하나에 의해 요청이 처리될 때까지 요청을 순차적으로 전달한다.
- 커맨드 패턴은 발신자와 수신자 간의 단방향 연결을 설립한다.
- 중재자 패턴은 발신자와 수신자 간의 직접 연결을 제거하여 그들이 중재자 객체를 통해 간접적으로 통신하도록 강제한다.
- 옵서버 패턴은 수신자들이 요청들의 수신을 동적으로 구독 및 구독 취소할 수 있도록 한다.
- 중재자와 옵서버 패턴의 차이는 종종 애매하다. 대부분의 경우 두 패턴 중 하나를 구현할 수 있으나, 때로는 두 패턴을 동시에 적용할 수 있다.
이것이 어떻게 가능한지 살펴보겠다.
옵서버 패턴에 의존하는 중재자 패턴의 인기 있는 구현이 있다. 중재자 객체는 출판사의 역할을 맡고, 컴포넌트들은 중재자의 이벤트들을 구독 및 구독 취소하는 구독자들의 역할을 맡는다. 중재자가 이러한 방식으로 구현되면 옵서버 패턴과 매우 유사하게 보일 수 있다.이제 모든 컴포넌트가 출판사가 되어 서로 간의 동적 연결을 허용하는 프로그램을 상상해 보자. 중앙화된 중재자 객체는 없고 분산된 옵서버들의 집합만 있을 것이다. - 만약 혼란스러우면 중재자 패턴을 다른 방법들로 구현할 수 있다. 예를 들어 모든 컴포넌트를 영구적으로 같은 중재자 객체에 연결하는 방법이 있다. 이 구현은 옵서버 패턴과 유사하지 않겠지만 여전히 중재자 패턴의 인스턴스일 것이다.
- 중재자의 주목적은 시스템 컴포넌트들의 집합 간의 상호 의존성을 제거하는 것이다. 그러면 이러한 컴포넌트들은 대신 단일 중재자 객체에 의존하게 된다. 옵서버 패턴의 목적은 객체들 사이에 단방향 연결을 설정하는 것으로, 여기서 일부 객체는 다른 객체의 종속자 역할을 한다.
정리
옵저버 패턴에 대해 한마디로 정리하자면
어떤 객체(발행 : 유튜버)의 상태가 변할 때 그와 연관된 객체들(옵저버:구독자)에게 알림을 보내는 디자인 패턴이다.