프로그래밍/디자인패턴

[디자인패턴] 옵저버 패턴 (Observer Pattern) 아주 간단하게 정리해보기

Jay Tech 2019. 10. 7. 22:35

옵저버 패턴이란?

 

 

옵저버

옵저버란 스타크래프트 프로토스의 유닛으로 적들을 관찰하기 위해 탄생한 유닛이다. 테란전에서 필수 유닛이며

 

 

옵저버 패턴(observer pattern)은 객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인 패턴이다. 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다. 발행/구독 모델로 알려져 있기도 하다.

 

라고 위키에 쓰여 있다.

 

아주 간단히 얘기하자면 어떤 객체의 상태가 변할 때 그와 연관된 객체 들에게 알림을 보내는 디자인 패턴이 옵저버 패턴이라고 할 수 있다.

아주 간단한 예시를 통해 구현해 보자.

 

크루(학생)들은 코치가 하는 일들을 모두 notify(알림) 받아야 한다. 즉, 코치가 "밥을 먹는다"면 모든 크루들은 코치가 밥을 먹었다는 것을 알아야 한다. 코치가 "농땡이를 친다"면 모든 크루들은 코치가 농땡이를 치는 것을 알아야 한다.

 

베디라는 객체는 코치이다. (코치 인터페이스를 구현해야 한다)

코치(코치 인터페이스)의 기능은 크루들을 등록한다, 크루들을 등록 해제한다, 크루들에게 행동을 알린다. 아주 간단하게 이 세가지의 기능을 가지고 있다.

 

코치와 크루

이렇게 코치인 베디는 모든 크루들에게 정보를 알려야 한다.

 

크루(크루 인터페이스)의 기능은 자신의 상태를 업데이트하는 기능을 가진다. 아주 간단하게 하나의 기능을 가지고 있다고 하자.

위의 도식대로 인터페이스를 정의해보자.

 

Coach 인터페이스
Crew 인터페이스

이렇게 두 개의 인터페이스를 정의할 수 있겠다.

그리고 Coach를 구현하는 베디 클래스를 만들어보자.

 

Baedi(코치) 클래스

 

베디(코치)는 Crew들의 리스트를 가지고 있고, 세 가지 기능을 가진다. 밥 먹기, 농땡이 치기, 귀여워 지기

그리고 인터페이스에 정의된 대로 3개의 함수를 구현한다. 주목할 것은 notifyCrew 메서드를 각 기능에서 호출한다는 것이다. 그리고 크루들에게 한 명씩 업데이트 메서드를 호출하게 한다. (이 부분이 알림을 보내는 부분이다)

 

티버(크루)는 베디(코치)의 알림을 받고 싶어서 구독을 하고 싶어 한다.

 

Crew인 Tiber

다른 크루들 르윈, 제이도 마찬가지다. 클래스 다이어그램으로 나타내보면 다음과 같다.

 

다이어그램

 

이제 메인함수를 만들어보자.

 

Main 함수

 

코치인 베디에게 3명의 크루들이 구독을 하였다. 그리고 upgradeCutie() 메서드를 호출한다. 그렇게 되면 구독한 3명의 크루 객체들에게 메세지가 전달된다.

 

베디코치가 귀여움을 강화했다
Lewin 수신 : 나 더 귀여워 졌따
Tiber 수신 : 나 더 귀여워 졌따
Jay 수신 : 나 더 귀여워 졌따

 

 

이에 Lewin은 구독을 해지한다.

 

 baedi.unsubscribe(lewin);
 baedi.eatFood();

 

베디코치가 밥을 먹는다
Tiber 수신 : 나 밥 먹었따
Jay 수신 : 나 밥 먹었따

 

 

결국 Lewin은 알림 대상에서 제외가 된다.

(첨언으로, 구독을 해지하는 주체가 잘못되었다고 지적할 수 있다. 지금은 아주 간단한 예시이고 추후 자바에서 구현한 클래스에서는 구독자가 해지할 수 있도록 this 를 넘겨주는 부분이 있으므로 코드를 참조하자)

 

이로써 객체의 상태가 변화될 때 연관된 객체 들에게 알림을 보낼 수 있게 된다. 이것이 옵저버 패턴이다.

조금 더 "옵저버" 답게 이름을 리팩토링을 하게 된다면 Crew 인터페이스는 Observer 인터페이스가 되겠고, Coach 인터페이스는 Observable 인터페이스가 되겠다.

다시 말해 Observable 은 발행자가 되고 Observer는 구독자가 된다.

 

 

다이어그램

 

Java 의 옵저버

자바에서 제공하는 옵저버가 있을까? 위의 예제는 아주 간단히 구현해본 것이므로 멀티 스레드환경에서는 취약할 것이다. 그것을 보완하고 조금 더 상세히 구현한 옵저버 관련 클래스들이 있다.

https://docs.oracle.com/javase/8/docs/api/index.html?java/util/Observable.html

https://docs.oracle.com/javase/8/docs/api/index.html?java/util/Observer.html

Observer 인터페이스와 Observable 클래스를 제공한다. 주목할 점은 Observable 은 클래스 라는 점이다.

 

java.util의 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 메서드를 놓은듯 하다)

최악의 잠재적인 레이스 컨디션은 (여기서 레이스 컨디션이란 공유자원에 대해 여러 프로세스가 선점하기 위해 경쟁하는 것을 말한다)

  1. 막 등록된 옵저버가 알림을 받지 못하는 것

  2. 막 삭제된 옵저버가 잘못 알림을 받는 것

그리고 마지막에 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 클래스를 사용하려면 상속을 해야하는데 자바는 다중상속을 지원하지 않기 때문에 한계점이 발생할 수 있다. 그나마 다행인건 패턴을 구현하기가 어렵지 않다는 점이므로 옵저버블을 인터페이스로 만들고 직접 구현하는 것이 확장성이 있을것으로 보인다.