옵저버 패턴(Observar Pattern)

2008/11/16 23:04
크리에이티브 커먼즈 라이선스
Creative Commons License

[head first design patterns]내용을 일부 인용 했습니다.

옵저버 패턴 (Observer Pattern)

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로
일대다(1:n) 의존성을 정의 합니다. 

이 글을 읽는 사람은 '스타크래프트'를 한번이라도 해보거나 혹은 구경이라도 해본 적이 있을 것이다.

가장 편하게 설명하자면 여기에 등장하는 유닛인 '옵저버'를 패턴으로 만든다고 생각 하면 될 것이다.

Q. 그렇다면 생각해 보자. 스타크래프트에서 옵저버가 하는 일은 무엇인가 ..?
A. '클록킹 유닛을 찾는다.'

Q. ... 물론 그렇다. 하지만 클록킹 유닛을 찾는 것 외에 더 기본적으로 무엇을 하는가 ..?
A. '상대방을 관찰 한다.'

정확하다. 옵저버에 대한 사전적 뜻은 '관찰자'인 것이다. 우리는 프로그래밍을 한다.

그렇다면 프로그래밍에서 관찰한다는 것은 무엇에 해당하는가? 바로 객체들의 상태를 관찰한다는 의미이다.

이것은 우리가 흔히 사용하는 'windows'에서 쉽게 볼 수 있다.

예를들면 인터넷으로 영화를 다운받는다고 생각 하자..

탐색기를 열어 하드디스크 용량을 보고 있으면.. 얼마 단위로 용량이 착착 바뀌는 것을 알 수 있다.

이런 것을 구현할 수 있게 해주는 패턴이다.(풀링과 푸쉬방식의 차이는 있을 수 있다. 이부분은 차후 설명)

그렇다면 실제 어떤 문제에 대해 옵저버 패턴을 구현하는지 살펴보자.

먼저 옵저버 패턴은 우에 설명한 대로 1:n의 의존적 관계이다. 하나의 '주제'가 있다면 그것에 연관된 다른 객체들이 있고,

주제의 상태가 바뀌게 되면 다른 객체들은 바뀐 주제의 상태값을 받아오는 방식인 것이다.
(받아온 후 해당 객체에선 바뀐값에 대한 갱신과 기타 다른 연산이 있을 수 있다.)

이전에 작성된 스트래티지 패턴을 다시 살펴보면 우리는 느슨한 결합을 통해 얻은 점이 많이 있다.

예제로 사용할 코드는 '신문사'와 '신문구독자' 관계에서 도출된 내용을 기준으로 작성 되었다.

'신문배포사' (이하 신문사)는 신문구독자(이하 구독자)에게 2가지 정보를 바로바로 알려주게 된다.

그것은 '새로운 뉴스 건, 갱신 시간'을 알려주게 된다.

코드는 아래와 같으며 이번에도 느슨한 결함을 통해 옵저버 패턴을 구현하게 되면 다음과 같은 코드가 작성 될 것이다.
(단, 신문사에서 의 상태 변경(뉴스추가)은 setNewsUpdate()  메소드를 호출하기로 했다고 한다.)

INewsPaprCmpny.java [신문사 인터페이스] 
INewsReader.java [구독자 인터페이스] 
NewsPaprCmpny.java [신문사 구현된 클레스]
NewsReader.java [구독자 구현 클레스] 
StartApp.java [실행 프로그램] 

위 코드의 주석을 보면 대충 보기만 해도 감이 올 것이다.

먼저 우리는 interface INewsPaprCmpny (신문사)를 사용했다. 그리고 interface INewsReader (구독자)를 이용했다.

그것을 상속받은 클레스는 해당 메소드를 구현 했고,신문사의 뉴스가 추가발생하게 되면 setNewsUpdate() 메소드를 호출 한다.

메소드가 호출되면 'this.notifyReaders();' 구문이 실행되므로 구독하고 있는 모든이에게(ArrayList) 뉴스가 추가되었고,

구독자의 메소드에 뉴스 갯수와 날짜를 알려주므로 구독자는 그 정보를 갱신하게 된다. 그리고 그 결과를 display() 메소드로 출력 한다.

Q. 위와같은 코드로 작성한다면 변경이 있을때마다 모두에게 호출하게 된다.
   어떤 객체는 단1개의 값만 요구할 때가 있을 수 있다. 그런거라면 getter를 이용해 해당 객체가 알아서 가져오는건 어떨까?
A. 이것은 사용자가 임의로 경우에 맞게 사용할 수 있다. 바로 '푸시'와 '풀' 방식을 이용할 수 있기 때문이다.

간단히 설명하자면 푸시방식은 위의 방식대로 '신문사 -> 구독자에게 값을 밀어주는(푸시) 방식'이다.

주제의 값이 바뀌면 신문사는 메소드를 호출해 구독자에게 변경값을 통보해 준다.

하지만 모든 구독자가 이런 방식을 취하지 않아야 하는 경우도 있을수 있다. 예를 들어 신규뉴스건만 필요하고 갱신날짜는 필요 없을 경우도 있다.

바로 '신문사 -> 구독자에게 요청해 값을 가져가는(풀 방식) 방식'인 것이다.

이렇게 될 경우 단지 구독자 클레스는 신문사 클레스의 레퍼런스를 보유하고, 게터(getter)를 이용해 받아오는 방법을 적용하면 쉽게 될 것이다.

java에서는 친절하게도 이러한 옵저버 패턴을 손쉽게 사용할 수 있는 API를 제공한다.

바로 java.util패키지에 있는 Observer [interface]와 Observable [class]가 그것이다. 두가지를 살펴보면 ..

위에서 주목할 것은 setChanged() 메소드와 관련된 것 들이다. Obserable 소스를 살펴보면 알겠지만 내부적으로 변경여부에 대한 boolean changed 값이 존재하고, setChanged() 메소드를 통해 값을 'true'로 바꾼다. 이렇게 값이 바꾼 뒤  notifyObservers() 메소드를 진행하게 된다. 이때 if (!changed) 조건으로 조회를 하므로 반드시 setChanged()를 호출해야 하며, 호출 뒤 값은 자동으로 스위칭 된다. (스위칭을 강제로 하려면 clearChanged() 를 호출하고, changed값을 알고자 한다면 hasChanged()를 호출하면 된다. 기타 자세한 것은 javadoc 이용)

이제 java API를 이용해 코드를 작성하면 다음과 같다.

NewsPaprCmpny.java [신문사 클레스]
NewsReader.java [구독자 클레스]
NewsInfoBean.java [신문정보 Bean]
StartApp.java [실행 프로그램]

중요한것은 바로 '풀'과 '푸시'방식의 선택이다. 위에선 주석처리 되었지만 구독자 클레스의 public void update(Observable o, Object arg)를 보면 알 수 있다.

이쯤되면 이미 알고 있겠지만 푸시 방식은 신문사가 값을 구독자에게 '떠먹여 주는'방식이다. 따라서 신문사가 푸시 방식을 적용하게 되면 notifyObservers(Object arg) 메소드를 호출하게 되는데,

 구독자 클레스의 public void update(Observable o, Object arg) 에서 arg로(신문사에서 NewsInfoBean로 넘겨 줌) 넘겨주게 되고, 구독자는 그것을 Object로 받아 케스팅 후 게터(getter)로 자신의 값을 변경 했다.
 
 즉, 신문사가 'bean에 담아서 정보를 너에게 줄테니 넌 그것을 받아서 자신의 값을 변경하도록 해!' 하는 방식인 것이다.
 
 풀 방식을 적용하게 되면 인자 없이 notifyObservers()를 호출하게 되는데 당연히 구독자의 void update(Observable o, Object arg) 중 arg는 null넘어가게 되고 o 객체에는 '신문사'객체가 넘어가게 된다.
 
 신문사 객체는 자신의 값을 가져올 수 있는 게터(getter)를 보유하고 있으니 구독자는 그것을 받아와 사용할 것이다.
 
 즉, 구독자가 '신문사가 자신의 객체를 넘겨주고, 그것에 게터(getter)가 있으니 그걸 받아서 내 값을 변경하면 되겠다!'는 방식인 것이다.

여기서 java.util.Observable의 단점은 다음과 같다.

1. Observable는 class이므로 내가 사용하는 class가 이미 상속받은 class가 있다면 사용할 수 없다.
(이것은 자바에서 말하는 class상속은 단 1개만 된다는 것으로 인해 발생 한다.
 또 Observable 인터페이스가 제공되지 않는다.)

2. Observable의 핵심 메소드는 외부에서 호출될 수 없다.
setChanged() 메소드가 protected로 선언되어 있으므로 상속관계 등의 관계가 아니라면 아예 접근할 수 없다.


위의 단점을 극복하기 위해선 애초에 상위 클레스에 Observable를 상속받아 작성한 후 그것을 다시 상속하는 방법이 있을 수 있다. 아니면 아예 처음에 만들었던 방식으로 직접 구현하는 방식 등의 여러 해결책이 있을 수 있다.

디자인 원칙

애플리케이션에서 바뀌는 부분을 찾아내서 바뀌지 않는 부분으로부터 분리 시킨다.
(이 패턴에서는 주제와 옵저버 상태만 바뀌게 된다. 주제의 내용(코드)를 바꾸지 않고도 청취객체를 바꿀 수 있다.)
특정 구현이 아닌, 인터페이스에 맞춰서 프로그래밍 한다.
(주제와 청취객체들은 모두 interface를 이용하므로 느슨한 결합이 가능하다.)
상속보다는 구성을 활용한다.
(주제는 옵저버를 관리한다. 상속관계가 아닌 구성으로 이루어 졌으므로 유용하다.)

핵심정리

1. 옵저버 패턴에서는 객체들 사이에 1:n관계가 형성된다.
2. 주제 또는 Observable객체는 동일한 인터페이스를 사용해 청취객체에게 연락한다.
3. Observable에서는 청취객체들이 Obserable인터페이스를 구현하다는것을 제외하면 청취객체의 내용을 전혀 모른다.
    (느슨한결합)
4. 옵저버 패턴엔 푸시방식과 풀방식이 있다.
5. java.util.Obserable에서는 청취객체들에게 연락하는 순서에 의존해선 안된다.
저작자 표시 비영리 변경 금지

Pupustory Dev/Design Patterns

  1. Blog Icon
    초보자

    좋은 글 감사합니다. 풀 방식을 찾고 있었는데 소스까지 보여주는 곳이 없어서 한참을 찾았네요.