'디자인 패턴'에 해당되는 글 1건

  1. 2008.05.03 디자인패턴 - 옵저버 패턴 (1)

JDK 에서 가장 많이 쓰이는 패턴 중 하나인 옵저버(Observer) 패턴에 대해 설명한다.

헤드퍼스트 디자인 패턴 책 참고^^

날씨를 디스플레이해주는 기상 모니터링 애플리케이션 개발을 예로 들었다.

이 애플리케이션은 크게 기상 스테이션, Weather Data 객체, 디스플레이 객체

세 요소로 이루어진다.

WeatherData 클래스이다.

WeatherData :

getTemperature()

getHumidity()

getPressure()

measurementsChanged()

위의 세가지 게터메소드는 각각 최근에 측정된 온도, 습도, 기압 값을 리턴하는 메소드이다.

measurementsChanged 메소드는 기상 관측값이 갱신될 때마다 알려주기 위한 메소드이다.

WeatherData 객체를 개발한 사람들이 우리가 추가해야 할 부분에 대한 힌트를 남겨준 것이다.

우리는 이 메소드를 현재 조건, 기상통계, 기상예측 세가지 디스플레이를 갱신할 수 있도록

구현해야 한다.

또한 시스템이 확장 가능해야한다. 다른 개발자들이 별도의 디스플레이 항목을 만들 수 있도록 해야 하고

사용자들이 애플리케이션에 마음대로 디스플레이 항목을 추가/제거할 수 있도록 해야한다.

다음은 대강 구현해 본 measurementsChanged 메소드이다.

public class WeatherData {

       // 인스턴스 변수 선언

       public void meaasurementsChanged() {

                  float temp = getTemperature();
                  float humidity = getHumidity();
                  float pressure = getPressure();

                  // 디스플레이 갱신
                  curentConditionsDisplay.update(temp, humidity, pressure);
                  statisticsDisplay.update(temp, humidity, pressure);
                  forecastDisplay.update(temp, humidity, pressure);
         }
  
         // 기타 메소드

}

이 코드에는 문제가 있다.

구체적인 구현에 맞춰서 코딩했기 때문에 프로그램을 고치지 않고는 다른 디스플레이 항목을

추가/제거할 수 없는 것이다.

출판사 + 구독자 = 옵저버 패턴

이러한 신문 구독 메커니즘을 제대로 이해한다면 옵저버 패턴을 쉽게 이해할 수 있다.

출판사를 주제(subject), 구독자를 옵저버(observer) 라고 부른다는 것을 외워두자.

주제 객체에서는 일부 데이터를 관리한다.

옵저버 객체들은 주제 객체를 구독하고 있으며 ( 주제 객체에 등록되어 있으며 ) 주제의

데이터가 바뀌면 갱신 내용을 전달 받는다.

Duck 이라는 객체는 옵저버가 되기를 원한다. 주제객체에개 자기도 옵저버가 되고싶다고 이야기하고,

공식적인 옵저버가 된다. 이제 주제 객체의 값이 바뀌면 Duck 을 비롯한 모든 옵저버들이 주제 객체의 값이

바뀌었다는 연락을 받게 된다.

Mouse 라는 객체는 옵저버 목록에서 탈퇴하고 싶다는 요청을한다. 그러면 주제 객체에서 Mouse 의 요청을

받아들여 옵저버 집합에서 제거시킨다. 주제 객체게 또 새로운 값이 들어오면 이제 Mouse 한테는

연락이 되지 않는다.

그렇다면 옵저버 패턴의 정확한 정의를 알아보자

옵저버 패턴(Observer Pattern) 에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한

테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to_many) 의존성을 정의한다.


옵저버 패턴을 구현하는 방법에는 여러 가지가 있지만, 대부분 주제(Subject) 인터페이스와 옵저버(Observer)

인터페이스가 들어있는 클래스 디자인을 바탕으로 한다.

다음은 옵저버 패턴 다이어그램이다.


사용자 삽입 이미지

이 다이어그램과 인대다 관계는 무슨 관계가 있는가?

- 옵저버 패턴에서 상태를 저장하고 지배하는 것은 주제 객체이다. 따라서 상태가 들어있는 객체는 하나만

있을 수 있다. 하지만 옵저버는 사용하긴 하지만 반드시 상태를 가지고 있어야 하는 것은 아니다. 따라서

옵저버는 여러 개가 있을 수 있으며, 주제 객체에서 상태가 바뀌었다는 것을 알려주기를 기다리는, 주제에

의존적인 성질을 가지게 되는 것이다. 그러므로 하나의 주제와 여러 개의 옵저버가 연관된, 일대다 관계가

성립되는 것이다.

의존성과는 무슨 상관이 있나?

- 데이터의 주인은 주제(Subject) 이다. 옵저버는 데이터가 변경되었을 때 주제에서 갱신해 주기를 기다리는

입장이기 때문에 의존성을 가진다고 할 수 있다. 이런 방법을 사용하면 여러 객체에서 동일단 데이터를 제어하

도록 하는 것에 비해 더 깔끔한 객체지향 디자인을 만들 수 있게 된다.

디자인의 원칙 중에는 이러한 것이 있다.

"서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 ( Loose coupling ) 디자인을

사용해야 한다."


옵저버 패턴에서는 주제와 옵저버가 느슨하게 결합되어 있는 객체 디자인을 제공한다.

주제와 옵저버는 서로 독립적으로 재사용할 수 있다. 주제나 옵저버를 다른 용도로 활용할 일이 있다고

해도 손쉽게 재사용할 수 있다. 그 둘이 서로 단단하게 결합되어 있지 않기 때문이다.

주제나 옵저버가 바뀌더라도 서로한테 영향을 미치지는 않는다. 둘이 서로 느슨하게 결합되어 있기

때문에 주제 혹은 옵저버 인터페이스를 구현한다는 조건만 만족된다면 어떻게 바꿔도 문제가 생기는

일은 없는 것이다.

느슨하게 결합하는 디자인을 사용하면 객체 사이의 상호 의존성을 최소화할 수 있기 때문에 변경사항

이 겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있게 된다!.

다음은 기상 스테이션의 다이어그램 설계이다.

사용자 삽입 이미지

이 다이어그램을 바탕으로 코드를 만들어 보자.

public interface Subject {

         public void registerObserver(Observer o);     // 옵저버를 등록
         public void removeObserver(Observer o);     // 옵저버를 제거
         public void notifyObservers();  // 주제 객체의 상태가 변경되었을 때 모든 옵저버들에게 알리는 메소드
}

public interface Observer {
          // Observer 인터페이스는 모든 옵저버 클래스에서 구현해야 한다.
          // 따라서 모든 옵저버는 update() 메소드를 구현해야 한다.
          public void update(float temp, float humidity, float pressure);

}

public interface DisplayElement {
          // display() 메소드 하나만 갖는다. 디스플레이 항목을 화면에 표시해야 하는 경우 호출한다.
          public void display();

}

WeatherData 에서 Subject 인터페이스 구현하기

public class WeatherData implements Subject {
 
 private ArrayList observers;
 private float temperatures;
 private float humidity;
 private float pressure;
 
 public WeatherData() {
  observers = new ArrayList();
 }
 
 public void registerObserver(Observer o) {
  observers.add(o);
 }
 
 public void removeObserver(Observer o) {
  int i = observers.indexOf(o);
  if(i >= 0) {
   observers.remove(i);
  }
 }
 
 /*
  * 상태에 대해 모든 옵저버들한테 알려주는 부분
  * 모두 Observer 인터페이스를 구현하는, 즉 update() 메소드가 있는
  * 객체들이므로 손쉽게 알려줄 수 있습니다.
  */
 public void notifyObservers() {
  for(int i = 0 ; i < observers.size() ; i++) {
   Observer observer = (Observer)observers.get(i);
   observer.update(temperatures, humidity, pressure);
  }
 }
 
 /*
  * 기상 스테이션으로부터 갱신된 측정치를 받으면 옵저버들한테 알립니다.
  */
 public void measurementsChanged() {
  notifyObservers();
 }
 
 public void setMeasurements(float temperature, float humidity, float pressure) {
  this.temperatures = temperature;
  this.humidity = humidity;
  this.pressure = pressure;
  measurementsChanged();
 }
}

디스플에이 항목 만들기


public class CurrentConditionsDisplay implements Observer, DisplayElement {
 
 private float temperature;
 private float humidity;
 private Subject weatherData;
 
 
 /**
  * 생성자에 weatherData 라는 주제 객체가 전달되며,
  * 그 객체를 써서 디스플레이를
  * 옵저버로 등록한다.
  */
 public CurrentConditionsDisplay(Subject weatherData) {
  this.weatherData = weatherData;
  weatherData.registerObserver(this);
 }
 
 public void update(float temperature, float humidity, float pressure) {
  this.temperature = temperature;
  this.humidity = humidity;
  display();
 }
 
 public void display() {
  System.out.println("Current conditions: " + temperature +
    "F degrees and " + humidity + " % humidity");
 }
}

테스트용 기상 스테이션 클래스


public class WeatherStation {

 public static void main(String [] args) {
  WeatherData weatherData = new WeatherData();
 
  CurrentConditionsDisplay currentDisplay =
   new CurrentConditionsDisplay(weatherData);
 
 // 코드 생성해야 함
 // StatisticsDisplay statisticsDisplay =
 //  new StatisticsDisplay(weatherData);
 // ForecastDisplay forecastDisplay =
 //    new ForecastDisplay(weatherData);
 
  // 새로운 기상 측정 값이 들어왔다고 가정
  weatherData.setMeasurements(80, 65, 30.4f);
  weatherData.setMeasurements(82, 60, 26.2f);
  weatherData.setMeasurements(78, 90, 29.3f);
 
 }
}

이제 주제 객체와 옵저버가 직접 대화를 하고 있게 되었다.

옵저버들이 Observer 인터페이스를 구현하고 있기 때문인 것이다.

이러한 옵저버 패턴은 사실 자바에 내장되어있다.

java.util 패키지에 들어있는 Observer 인터페이스와 Observable 클래스가 그것이다.

다음은 자바 내장 기능을 활용한 기상 스테이션 구현 코드이다.


import java.util.*;

public class WeatherDataExt extends Observable { // Observable 의 서브클래스
 
 private float temperature;
 private float humidity;
 private float pressure;
 
 public WeatherDataExt() {
  // 이제 생성자에서 옵저버들을 저장하기 위한 자료구조를 만들 필요가 없다.
 }
 
 public void measurementsChanged() {
  setChanged(); // 상태가 바뀌었다는 것을 알린다.
  notifyObservers();
 }
 
 public void setMeasurements(float temperature, float humidity, float pressure) {
  this.temperature = temperature;
  this.humidity = humidity;
  this.pressure = pressure;
  measurementsChanged();
 }

 
 //
 // 옵저버가 WeatherData 객체의 상태를 알아낼 때는 이 메소드를 사용한다.
 //
 public float getTemperature() {
  return temperature;
 }

 public float getHumidity() {
  return humidity;
 }

 public float getPressure() {
  return pressure;
 }
}

import java.util.*;

public class CurrentConditionsDisplayExt implements Observer, DisplayElement {

 Observable observable;
 private float temperature;
 private float humidity;
 
 public CurrentConditionsDisplayExt(Observable observable) {
  this.observable = observable;
  observable.addObserver(this);
 }
 
 public void update(Observable obs, Object arg) {
  if(obs instanceof WeatherDataExt) {
   WeatherDataExt weatherData = (WeatherDataExt)obs;
   this.temperature = weatherData.getTemperature();
   this.humidity = weatherData.getHumidity();
   display();
  }
 }
 
 public void display() {
  System.out.println("Current conditions : " +
    temperature + "F degrees and " + humidity + "% humidity");
 }
}

이번에는 java.util.Observable 의 단점에 대해 알아본다.

Observable 은 인터페이스가 아닌 클래스인 데다가, 어떤 인터페이스를 구현하는 것도 아니다.

안타깝게도 활용도와 재사용성에 있어서 제약조건으로 작용하는 몇 가지 문제점이 있다.

Observable 은 클래스이다.

Observable 이 클래스이기 때문에 서브클래스를 만들어야 한다는 점이 문제가 된다.

이미 다른 수퍼클래스를 확장하고 있는 클래스에 Observable 의 기능을 추가할 수 없기 때문이다.

그래서 재사용성에 제약이 생기게 되는 것이다.

또한 Observable 인터페이스라는 것이 없기 때문에 자바에 내장된 Observer API 하고 잘 맞는 클래스를

직접 구현하는 것이 불가능하다. java.util 구현을 다른 구현으로 바꾸는 것도 불가능하다.

그렇다면 어떻게 해야 할까?

java.util.Observable 을 확장한 클래스를 쓸 수 있는 상황이라면 Observable API 를 쓰는 것도 괜찮을

것이다. 하지만 앞에서 했던 것처럼 직접 구현할 수도 있다. 둘 중 어떤 방법을 쓰든 옵저버 패턴만 제대로

알고있다면 그 패턴을 활용하는 API 는 어떤 것이든 잘 활용할 수 있을 것이다.

Posted by 행복한 프로그래머 궁금쟁이박

댓글을 달아 주세요

  1. BlogIcon replica watches 2013.01.22 16:56  댓글주소  수정/삭제  댓글쓰기

    난 여기 이렇게 다른 사람들이 읽을 수에 내 사이트로 연결되는 링크를 넣어. 내 독자에 대한 같은 행복합니다.