옵저버 패턴(Observer Pattern)(1/3) - java.util.Observable

2019. 7. 30. 01:00 JAVA/Design Patterns

옵저버 패턴 (Observer Pattern)


옵저버 패턴(Observer Pattern)에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의합니다.


옵저버 패턴을 설명하기 위해 아래의 구조를 상황으로 옵저버 패턴을 구현해 보겠습니다.


그림을 설명하면, 옵저버 패턴으로 구성된 시스템은 여러개의 디스플레이 장비를 옵저버로 등록돼 있습니다. 일대다로 의존관계에 있는 이 시스템에 새로운 값이 들어오면, 옵저버로 등록되어 있는 디스플레이 장비에 갱신된 데이터를 전달합니다.


상황

기상스테이션에서 옵저버 패턴 시스템에 주기적으로 온도, 습도, 압력 데이터를 전송한다.

옵저버 패턴 시스템에서는 전달받은 데이터를 등록된 디스플레이 장비에 전송하여 데이터를 갱신한다.

디스플레이 장비는 해당 데이터를 사용하여 각 목적에 맞도록 데이터를 디스플레이 한다.


동작 시나리오


① WeatherStation에서는 온도, 습도, 기압 정보를 수집한다.

② WeatherStation에서 주기적으로 (30분에 한번식) 현재 데이터(온도,습도,기압)을 WeatherData에 던져 준다.

③ WeatherData는 새로운 데이터를 받으면, Observable에 Observer로 등록되어 있는, 현재상태 출력장비(CurrentConditionDisplay)와 기압변동 출력장비(ForecastDisplay)에 새로운 데이터를 전달한다.

④ 현재상태 출력장비와 기압변동 출력장비는 새로운 데이터(온도, 습도, 기압)을 각자의 활용 방법에 따라 화면에 출력한다.

⑤ WeatehrStation에서 다시 새로운 데이터를 던져준다. 

⑥ ①~④ 번이 반복되며, 주기적으로 현재상태 출력장비와 기압변동 출력장비는 새로운 데이터를 출력한다.


옵저버 패턴에 대해 이해가 되시나요?

쉽게 말하면 내가 데이터를 받을때마다 나의 정보를 구독하길 원하는 대상(옵저버)들에게 데이터를 던져주는 것입니다.


JAVA에서 옵저버 패턴을 구현하는 방법은 두가지가 있습니다.

첫번째는, JDK에서 지원하는 Observable과 Observer API를 활용하는 것입니다.

두번째는, API를 활용하지 않고 직접 구현하는 방법입니다.

물론 두가지 모두 장점과 단점이 있습니다. 두가지 경우를 설명하면서 장점과 단점에 대해 확인해봅시다.





1. 자바 내장 옵저버 패턴(JDK API)을 사용하여 옵저버 패턴구현


(1) 옵저버 패턴 UML 구조


옵저버 패턴은 직접 구현할 수 있지만, 자바에서 API로 제공합니다. (java.util.Observable , java.util.Observer)

위의 기상정보 옵저버 패턴 시스템을 UML로 구현했습니다.




Observable 클래스와 Observer 인터페이스는 java.util 패키지에 들어 있습니다. 자바 내장 API 입니다.

Observable 클래스는 등록된 옵저버들을 관리하며, 새로운 데이터가 들어오면 등록된 옵저버에게 데이터를 전달합니다.

Observer 인터페이스를 implements 하여 등록된 옵저버들은 Observable로 부터 데이터를 받을 수 있습니다.

이 자바 내장 옵저버 패턴을 사용하면 옵저버 패턴을  직접 구현할 필요 없이, 간단히 해당 클래스를 상속하면 됩니다.


그러면 Observer Pattern을 구현한 소스 코드를 살펴보겠습니다.



(2) WeatherData.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.util.Observable;

public class WeatherData extends Observable{        // java.util.Observable 클래스 상속

    private float temperature;                           // 온도
    private float humidity;                               // 습도
    private float pressure;                             // 기압
    
    public WeatherData(){

    }
    
  // 새로운 데이터를 전달 받아 갱신하고 새로운 데이터가 들어왔음을 알린다.
  // 기상스테이션(WeatherStation)에서는 주기적으로 이 함수를 사용해 최신 데이터를 전달한다.
    public void setMeasurements(float temperature, float humidity, float pressure){
        this.temperature = temperature;   
        this.humidity = humidity; 
        this.pressure = pressure;
        measurementsChanged();
    }

   // 갱신할 새로운 데이터 여부의 플래그 값을 변경하고(setChanged())
   // 옵저버들에게 새로운 데이터를 전달한다. (notifyObservers())
   public void measurementsChanged(){
        setChanged();
        notifyObservers();
    }
    
   // 온도값 반환
    public float getTemperature(){
        return temperature;
    }

  // 습도값 반환
    public float getHumidity(){
        return humidity;
    }
    
  // 기압값 반환
    public float getPressure(){
        return pressure;
    }

}


- setMeasurements(float temperature, float humidity, float pressure) : 기상스테이션에서 온도, 습도, 기압 측정값을 전달할때 호출한다. 파라미터로 받은 데이터를 갱신하고 measuermentChanged()를 호출한다.

- measurementsChanged() : 옵저버에 전달할 새로운 데이터가 있다고 알리고(setChanged()), 등록된 옵저버에 데이터를 전달한다.(notifyObservers())  (setChanged()와 notifyObservers()는 Observable에서 상속받은 함수이다.)


보는것과 같이 WeatherData.java는 Observable 을 상속하였다. 따라서 Observable의 함수를 사용하여, Observer들에게 데이터를 전달할 수 있다.


15열의 setMeasurements() 함수와 24열의 measurementsChanged() 함수를 보자

기상스테이션(WeatherStation.java)는 주기적으로 데이터(온도,습도,기압)값을 WeatherData의 setMeasurements() 함수를 사용하여 전달한다.

setMeasurements()는 전달받은 데이터를 각 변수에 갱신한 뒤, measurementsChanged() 함수를 호출한다.


measuremensChanged() 함수는 Observer에게 데이터를 전달한다.

먼저 setChanaged() 함수를 호출하여, 전달할 새로운 데이터가 있음을 설정한다. 그리고 notifyObservers() 함수를 호출하여, 갱신한 새로운 데이터를 각 등록된 옵저버에게 전달한다.


정말 간단하다. Observable 을 상속한뒤, 옵저버들에게 데이터를 전달할 때, setChanged()와 notifyObservers()를 순차적으로 호출하면 된다. 그러면 데이터가 등록된 옵저버들에게 전달되는 것이다.


정말 쉽지 않나요?



(3) DisplayElement.java (인터페이스)


그러면 데이터를 전달받을 옵저버를 구성해 보자.

데이터를 받아서 표시할 옵저버는 디스플레이 장비들이다. 디스플레이 장비는 여러대가 될 수 있고, 또 확장될 수 있다.

앞에 스트래티지 패턴에서 확인했듯이, 상속보다는 인터페이스를 사용하여 느슨하게 결합해야 한다.


디스플레이 장비들을 구현할 인터페이스를 만들어보자.


1
2
3
4
public interface DisplayElement {
    public void display();
}


- display() : 화면에 출력한다.


현재는 화면을 출력해주는 display() 함수만 만들었다.


디자인 원칙을 기억하자.

서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.


이어서 Observer와 DisplayElement를 implements하여 구현하는 옵저버인 디스플레이 장비를 살펴보자.


(4) CurrentConditionDisplay.java


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.Observable;
import java.util.Observer;

public class CurrentConditionDisplay implements Observer, DisplayElement{    // Observer, DisplayElement implements
    

    Observable observable;                                // 등록될 Observable

    private float temperature;                        // 온도
    private float humidity;                            // 습도
    
    public CurrentConditionDisplay(Observable observable){         // 생성자 
        this.observable = observable;                            // 등록될 Observable을 import
        observable.addObserver(this);                            // this(CurrentConditionPlay) 옵저버로 등록
    }
    
    public void update(Observable obs, Object arg){                // update 로 새로운 데이터 갱신
        if(obs instanceof WeatherData){                            // Observable이 WeatherData인지 확인
            WeatherData weatherData = (WeatherData)obs;            // WeatherData로  변환
            this.temperature = weatherData.getTemperature();    // 온도 값 갱ㅇ신
            this.humidity = weatherData.getHumidity();            // 습도값 갱신
            display();                                            // 최신 값 출력
        }
    }

    @Override
    public void display() {                                        // 출력
        System.out.println("현재 온도 : " + temperature + "도,  현재 습도 : " + humidity + "%");
        
    }

}


- CurrenttConditionDisplay(Observable observable) : 객체가 생성될때 가져온 Observable에 옵저버로 등록한다.

- update(Observable obs, Object arg) : Observable에서 새로운 데이터를 전달할때 update를 호출한다. 전달받은 데이터를 갱신하고 출력한다.  (update()는 Observer 인터페이스의 함수를 구성한 것이다.)


옵저버 중에 하나인 CurrentConditionDisplay는 기온, 습도를 출력하는 장비이다.(DisplayElement Implements)

그리고 Observable을 상속한 WeatherData에 옵저버가 되어 최신값을 주기적으로 전달 받기 위해 Observer를 Implements 했다.Observable을 파라미터로 CurrentConditionDisplay가 생성되면, observable.addObserver(this)로 이 객체를 옵저버로 등록한다. 이제 WeatherData로 부터 데이터를 전달받을 수 있게 된다.


WeatherData에서 옵저버에 데이터를 전달하면 Observable에서 해당 update를 호출하여 데이터를 전달한다.

CurrentConditionDisplay는 update 함수가 호출되면서 넘어온 데이터를 갱신하고, display 함수를 호출하여 장비에 출력한다.


ForecastDisplay.java 도 마찬가지다. 이후에 추가되는 장비도 addObserver로 옵저버로 등록하고 update 함수를 구현하여, Observable로 부터 데이터를 전달받을 수 있다.


장비의 추가 그리고 Observable의 확장 등은 뒤에서 설명하겠다.


ForecastDisplay.java 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.Observable;
import java.util.Observer;

public class ForecastDisplay implements Observer, DisplayElement{    // Observer, DisplayElement implements

    Observable observable;                                 // 등록될 Observable

    private float currentPressure  = 29.92f;            // 현재 기압 (Default : 29.92f)
    private float lastPressure;                            // 마지막 기압

    public ForecastDisplay(Observable observable) {        // 생성자 
        this.observable = observable;                    // 등록될 Observable을 import
        observable.addObserver(this);                    // this(ForecastDisplay) 옵저버로 등록
    }

    public void update(Observable obs, Object arg){                // update 로 새로운 데이터 갱신
        if(obs instanceof WeatherData){                            // Observable이 WeatherData인지 확인
            WeatherData weatherData = (WeatherData)obs;            // WeatherData로  변환
            this.lastPressure = currentPressure;                // 온도 값 갱ㅇ신
            this.currentPressure = weatherData.getPressure();    // 습도값 갱신
            display();                                            // 최신 값 출력
        }
    }
    
    public void display() {                                        // 출력
        System.out.print("Forecast: ");
        if (currentPressure > lastPressure) {
            System.out.println("기압 증가");
        } else if (currentPressure == lastPressure) {
            System.out.println("기압 변동 없음");
        } else if (currentPressure < lastPressure) {
            System.out.println("기압 하강");
        }
    }

}

JAVA API Observable, Observer를 사용하여, Observer 패턴을 구현해 보았다.

길게 설명했지만 간단하게 말하면,


① Observable 클래스를 상속한 클래스를 만들고, 새로운 데이터가 들어오면 setChanged(), notifyObservers()를 호출하도록 구현한다.


② Observer 를 implements한 클래스를 만들고 Observable에 addObserver(this)로 자신을 Observer로 등록한다. update() 함수를 구현하여, 전달받은 데이터를 처리해준다.




옵저버패턴 실행 확인하기


WeatherStation에서 WeatherData에 데이터를 던져주면 CurrentConditionDisplay와 ForecastDisplay에 새로운 데이터로 display()가 실행된다. 간단한 실행코드로 확인해보자


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

public class WeatherStation {

    static WeatherData weatherData;                    // weatherData Import
    
    
    static CurrentConditionDisplay currentDisplay;    //currentConditionDisplay
    static ForecastDisplay forecastDisplay;            // forecastDisplay
    
    public static void weatherStation(){            // weatherStation 초기화
        weatherData = new WeatherData();            // WeatherData 객체 생성
        
        currentDisplay = new CurrentConditionDisplay(weatherData);        // CurrentConditionDisplay 생성 (WeatherData에 옵저버 등록)
        forecastDisplay = new ForecastDisplay(weatherData);                // ForecastDisplay 생성 (WeatherData에 옵저버 등록)
    }
    
    // WeatherData의 setMeasurements 함수 실행
    public static void changeWeather(float temp, float humity, float pressure) {  
        
        weatherData.setMeasurements(temp, humity, pressure);

    }
    
    
    public static void main(String[] args){
        
        weatherStation();                    // WeatherStation 생성
        
        // WeatherStation에서 날씨의 변화를 입력한다.
        System.out.println("-----날씨가 변한다.----");
        changeWeather(40, 50, 10);                    // WeatherData에 새로운 데이터 전송
                
        System.out.println("");
        
        System.out.println("-----날씨가 변한다.----");
        changeWeather(50, 60, 20);                    // WeatherData에 새로운 데이터 전송
        
        System.out.println("");
                
    }

}


WeatherData와 Display 객체를 생성한 후, WeatherData에 새로운 데이터를 전송하면, 옵저버인 각 Display 장비에게 전달되어 display를 출력한다.




이게 전부다. 쉽지 않나요?

물론, 좀 더 다이나믹하게 활용할 수 있다. 그런 의미에서 java.util.Observable과 java.util.Observer 를 살펴보자.




(5) java.util.Observable.class  확인


java.util.Observable.class


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

package java.util;


public class Observable {
    private boolean changed = false;              // 신규 데이터 여부 체크
    private Vector obs;


    public Observable() {                              // Observable 생성자
        obs = new Vector();
    }

    public synchronized void addObserver(Observer o) {      // Observer 등록
        if (o == null)
            throw new NullPointerException();
        if (!obs.contains(o)) {
            obs.addElement(o);
        }
    }

    public synchronized void deleteObserver(Observer o) {   // Observer 삭제
        obs.removeElement(o);
    }

    public void notifyObservers() {                               // Observer에 데이터 전달
        notifyObservers(null);
    }

    public void notifyObservers(Object arg) {              // Observer에 데이터 전달
    
        Object[] arrLocal;

        synchronized (this) {
        
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }

        for (int i = arrLocal.length-1; i>=0; i--)
            ((Observer)arrLocal[i]).update(this, arg);
    }

    public synchronized void deleteObservers() {            //등록된 Observer 전체 제거
        obs.removeAllElements();
    }

    protected synchronized void setChanged() {             // 신규 데이터 수신; 플래그값 변경
        changed = true;
    }

    protected synchronized void clearChanged() {         // 신규 데이터 없음; 플래그값 변경
        changed = false;
    }
    public synchronized boolean hasChanged() {         // 신규 데이터 여부 확인
        return changed;
    }

    public synchronized int countObservers() {            // 현재 등록된 옵저버 수 조회
        return obs.size();
    }
}



- addObserver(Observer o) : 옵저버를 등록한다. 이후에 들어오는 데이터는 등록된 옵저버에 전달된다.

- deleteObserver(Observer o) : 옵저버를 제거한다. 이후에 들어오는 데이터는 해당 옵저버에 전달되지 않는다.

- notifyObservers(), notifyObservers(Object arg) : 새로운 데이터가 들어오면 등록된 옵저버에 새로운 데이터와, 파라미터를 전달한다.

- deleteObservers() : 등록된 모든 옵저버를 제거한다. 이후에 들어오는 데이터는 전달되지 않는다.

- setChanged() : 신규 데이터가 들어오면, changed 값을 true로 변경, changed 변수가 true 일때만 데이터가 옵저버에 전달된다.

- clearChanged() : changed 값을 false 로 변경한다. 신규 데이터를 옵저버에 전달이 완료되면 clearChanged()를 호출한다.

- hasChanged() : 현재 changed 의 값을 반환한다.

- countObservers() : 현재 등록되어있는 옵저버의 수를 반환한다.



(6) Observer.class (java.util) 확인


Observer.class (java.util)  ; 인터페이스


1
2
3
4
5
6
package java.util;

public interface Observer {
    
    void update(Observable o, Object arg);
}



- update(Observable o , Object arg) : Observable에게 전달받은 새로운 데이터를 갱신한다.


Observable 클래스에서 notifyObservers() 함수를 통해 등록된 Observer에 데이터를 전달할 때, Observer 의 update(Observable o, Object arg) 함수를 호출한다. 그러므로 Observer를 implements한 옵저버는 update 함수를 구현하여, 전달받은 데이터를 갱신하고 활용해야 한다.



2. JDK 내장 옵저버 패턴의 단점과 한계 (java.util.Observable)


옵저버 패턴은 JAVA에서 많이 사용하고 유용한 패턴이다. 그래서 JAVA에 내장 API로 나왔을 것이다. 위에서 본 바와 같이 JAVA 내장 API를 사용하면 옵저버 패턴 구현이 정말 간단하다. 하지만 이 내장 API에 단점이 있고 한계가 있다.


java.util.Observable은 인터페이스가 아닌 클래스인 데다가, 어떤 인터페이스를 구현하는 것도아니다. 따라서 java.util.Observable 구현에는 활용도와 재사용성에 있어서 제약조건으로 작용하는 몇 가지 문제점이 있다.


(1) Observable은 클래스다.


첫 번째로, Observable이 클래스기 때문에 서브클래스를 만들어야 한다는 점이 문제다. 이미 다른 수퍼클래스를 확장하고 있는 클래스에 Observable의 기능을 추가할 수 없기 때문이다. 그래서 재사용성에 제약이 생긴다.

두 번째로, Observable 인터페이스라는 것이 없기 때문에 자바에 내장된 Observer API하고 잘 맞는 클래스를 직접 구현하는 것이 불가능하다. java.util 구현을 다른 구현으로 바꾸는 것도 불가능하죠. (ex: 멀티스레드 구현)


(2) Observable 클래스의 핵심 메소드를 외부에서 호출할 수 없다.


Observable API를 살펴보면, setChanged() 메소드가 protected로 선언되어 있다. Observable의 서브클래스에서만 setChanged()를 호출할 수 있다. 결국 직접 어떤 클래스를 만들고, Observable의 서브클래스를 인스턴스 변수로 사용하는 방법도 쓸 수 없다. 이런 디자인은 상속보다는 구성을 사용한다는 디자인 원칙에도 위배된다.


(3) 해결책?


java.util.Observable을 확장한 클래스를 쓸 수 있는 상황이라면 Observable API를 사용하는 것도 괜찮지만, 여의치 않다면 직접 구현하는 방법도 있다. 확장과 재사용성을 고려하여 인터페이스로 구현하는 것도 어렵지 않다.


다음 포스팅에서 이어서 구현하도록 하겠다.



출처: https://hyeonstorage.tistory.com/165?category=549763 [개발이 하고 싶어요]