들어가며

이 글에 나오는 내용은 헤드 퍼스트 디자인 패턴에서 나오는 내용을 정리한 것이다.

어플리케이션의 요구사항

  • 실제 기상 정보를 수집하는 장비가 존재함
  • WeatherData 객체(기상 스테이션으로부터 오는 데이터를 추적하는 객체)
  • 현재 기상 조건을 보여주는 디스플레이

이하 세 가지 요소로 이루어져 있다. 습도센서, 온도센서, 압력센서를 통해 기상 스테이션으로 정보가 축적된다. 정보가 축적된 기상 스테이션으로부터 WeatherData 가 데이터를 가져오고, 그 데이터를 디스플레이 장비가 하면에 표시한다.

WeatherData 객체를 사용하여 현재 조건, 기상 통계, 기상 예측 이렇게 세 항목을 디스플레이 장비에서 데이터를 갱신하면서 보여주어야 한다.

WeatherData Spec

class WeatherData {
	getTemperature() // 측정된 온도
	getHumidity()  // 측정된 습도
	getPressure() // 측정된 기압
	public void measurementsChanged() {
		// 우리가 구현해야 할 부분. 기상 관측값이 갱신될 때마다 화면에게 데이터를 전달해주어야 한다.
	}
}

확실하게 제공된 스펙은 다음과 같다.

  • WeatherData 클래스에는 온도, 습도, 기압을 받아올 수 있는 메서드가 존재한다.
  • 새로운 기상 관측 데이터가 전달될 때마다 measurementsChanged() 메소드가 호출된다.
  • 기상 데이터를 사용하는 세 디스플레이의 항목을 구현해야 한다. 하나는 현재 조건을 표시하는 것이고, 다른 하나는 기상 통계를 표시하는 것이고, 나머지 하나는 기상 예보를 표시하는 것이다. 새로운 측정값이 들어올때마다 새로운 디스플레이를 갱신해야 한다.
  • 시스템이 확장 가능해야 한다. 별도의 디스플레이 화면을 구성할 수 있어야 하고, 사용자들이 애플리케이션에 마음대로 디스플레이 항목을 추가/제거할 수 있어야 한다.

이렇게 한번 해볼까?

public class WeatherData {
	public void measurementsChanged() {
			float temp = getTemperature();
			float humidity = getHumidity();
			float pressure = getPressure();
			
			currentConditionsDisplay.update(temp, humidity, pressure);
			statisticsDisplay.update(temp, humidity, pressure);
			forecastDisplay.update(temp, humidity, pressure);
	}
}

이 코드는 구체적인 디스플레이 대상을 지정하여 업데이트를 하고 있기 때문에, 위에서 말한 네 번째 스펙인 시스템이 확장 가능해야 한다라는 조건을 충족시키지 못하고 있다. 디스플레이의 구현체에 업데이트를 시행하고 있기 때문이다.

그렇다면 옵저버 패턴이란?

  • subject 객체가 데이터를 관리한다.
  • subject 객체에서 옵저버 객체에게 전달할 데이터가 생기면, 옵저버 객체에게 그 데이터를 전달한다.
  • 옵저버 객체는 subject 객체를 구독하고 있으며, subject 의 데이터가 바뀌게 되면 갱신 내용을 전달받게 된다.

즉 옵저버 패턴이란 다음과 같다.

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

이는 다음과 같은 장점이 생긴다.

  1. subject 가 옵저버에 대해 아는 것은, 옵저버가 특정 인터페이스를 구현한다는 것 뿐이다. 따라서 두 객체 사이의 Loose coupling 이 생긴다.
  2. 옵저버는 얼마든지 새로 추가할 수 있다. 옵저버 인터페이스를 구현한 모든 객체는 옵저버로 추가할 수 있다.
  3. 새로운 형식에 옵저버를 추가하려면, 새로운 클래스에서 옵저버 인터페이스를 구현한 후에 subject 객체에 추가하면 된다.
  4. subject 와 옵저버는 서로 독립적으로 재사용할 수 있고, 내부적으로 변경이 발생한다고 해도 서로 영향을 미치지 않는다.

위의 장점을 취할 수 있는 이유는, 두 객체가 Loose coupling 로 이어져있기 때문이다. 이 지점에서 다음과 같은 디자인 원칙을 추출할 수 있다.

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

구체적인 코드로는?

먼저 인터페이스를 정의한다. Subject 인터페이스는 옵저버들을 관리하고 데이터에 변경이 생겼을 때 각각 옵저버에게 notify 를 하는 객체가 구현한다.

public interface Subject {
	void registerObserver(Observer o);
	void removeObserver(Observer o);
	void notifyObservers(); // Subject 의 상태가 변경된 후, 모든 옵저버들에게 알리기 위해 호출하는 메서드
}
 
public interface Observer {
    void update(float temp, float humidity, float pressure); // 이 파라미터들은 기상 정보가 변경되었을 때 옵저버들에게 전달되는 상태값
}
 
public interface DisplayElement {
    void display(); // 디스플레이 항목을 화면에 표시해야 하는 경우에 이 메서드를 호출한다.
}

WeatherData 에서 Subject interface 구현하기

이제 WeatherData 클래스에서 subject 인터페이스를 구현한다.

/**
* 이 클래스는 이 포스팅의 맨 처음에서 설명한 WeatherData 를 구현하였다.
* WeatherData 는 Subject 를 구현하였고, 내부에 Observer 목록을 가지고 있다가, 데이터의 변동이 생기면 해당 observer 들에게 알린다.
*/
public class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;
    
    public WeatherData() {
        observers = new ArrayList<>();
    }
    
    public void registerObserver(Observer o) {
        observers.add(o);
    }
    
    public void removeObserver(Observer o) {
        observers.remove(o);
    }
    
    public void notifyObservers() {
        for(Observer each : observers) {
            each.update(temperature, humidity, pressure);
        }
    }
    
    public void measurementsChanged() {
        notifyObservers();
    }
    
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
}

디스플레이 항목 구현

이제 데이터가 변경되면, 변경된 데이터를 받는 디스플레이 클래스를 구현해보자. 요구사항은 다음 세 가지 디스플레이를 구현하는 것이다.

  • 현재 기상 조건을 표시하는 디스플레이
  • 기상 통계를 표시하는 디스플레이
  • 기상 예보를 표시하는 디스플레이
public class CurrentConditionsDisplay implements Observer, DisplayElemnt {
    private float temperature;
    private float humidity;
    private Subject weatherData; // 이 객체는 당장 사용할 일이 없지만 저장해 둔다. 만약에 observer 에서 탈퇴할 일이 생긴다면 유용하게 활용할 수 있다.
    
    // 생성자에 subject 객체를 전달하고, 그 객체를 사용하여 디스플레이를 옵저버로 등록한다.
    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;
        dispaly();
    }
    
    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(); // observer 생성
        
        CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
        
        // 새로운 기상 측정값이 들어온것처럼 만든다.
        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

자바 자체적으로 지원하는 객체

지금까지는 우리가 직접 만든 코드로 옵저버 패턴을 구현하였다. 그러나, 자바에서 몇 가지 API 를 통하여 자체적으로 옵저버 패턴을 지원하기도 한다.

가장 일반적으로 java.util 패키지에 들어있는 Observer 인터페이스와 Observable 클래스를 들 수 있다. 또한 이걸 사용하면, 푸시 방식으로도 갱신할 수 있고 풀 방식으로도 갱신이 가능하다. 즉, Observable 클래스는 위의 Subject 와 같은 역할을 하고, Observer 인터페이스는 위의 옵저버 인터페이스와 같은 역할을 한다.

만약에 자바 자체적으로 지원하는 객체를 사용하고 싶다면, 다음과 같이 코드를 변경해준다.

객체가 옵저버가 되는 방법

  1. Observer 인터페이스를 구현하고, Observable 객체의 addObserver() 메소드를 호출하여 subject 클래스에서 옵저버를 등록한다.
  2. 옵저버 목록에서 탈퇴하고 싶다면, deleteObserver() 를 호출한다.

Observable 에서 연락을 돌리는 방법

  1. Observable 수퍼클래스를 확장하여, Observable 클래스를 만든다.
  2. setChanged() 를 호출하여, 객체의 상태가 변경되었음을 알린다.
  3. 그 다음으로는 다음 중 하나의 메소드를 호출해야 한다.
    • notifyObservers()
    • notifyObservers(Object arg) // 연락할 때 각 옵저버 객체에게 전달할 임의의 데이터 객체를 파라미터로 받는다.

옵저버가 연락을 받는 방법

update() 메서드를 구현한다. 다시 말하면, update(Observable o, Object arg) 형태의 메서드를 호출한다.

구현한 객체와의 차이점

setChanged() 메소드를 먼저 호출한다는 점이 다르다. setChanged() 가 호출되지 않은 상태에는 notify 를 해도 옵저버에게 아무 연락도 가지 않는다. 코드로 확인하면 다음과 같을 것이다.

setChanged() {
    changed = true;
}
 
notifyObservers(Object arg) {
    if(changed) {
        for(Observer each : observers) {
            each.update(this, arg);
        }
    }
    
    changed = false;
}
 
notifyObservers() {
    notifyObservers(null);
}

이와 같은 flag 를 두면 어떤 이득이 있을까? 만약에 기상 값이 굉장히 예민해서, 0.1 도 단위로 쉴세없이 변경된다고 가정해보자. 이와 같은 상황에서 0.5 도씩 변경될때마다 옵저버들에게 알리고 싶다면, setChanged 를 조건에 따라 호출함으로서 원하는 바를 달성할 수 있다.

자바 내장 기능을 활용한 기상 스테이션

Observable

/**
* 이 클래스는 자바 내장 객체를 사용하여 WeatherData 를 구현한다. 
*/
public class WeatherData extends Observable {
    private float temperature;
    private float humidity;
    private float pressure;
    
    public WeatherData() { }
    
    public void measurementsChanged() {
        setChanged();
        notifyObservers();
    }
    
    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementsChanged();
    }
    
    public float getHumidity() {
        return humidity;
    }
    
    public float getTemperature() {
        return temperature;
    }
    
    public float getPressure() {
        return pressure;
    }
}

Observer

public class CurrentConditionsDisplay implements Observer, DisplayElemnt { // java.util 패키지에 있는 Observer 를 구현한다.
    Observable observable;
    private float temperature;
    private float humidity;
    
    public CurrentConditionsDisplay(Observable observable) { 
        this.observable = observable;
        observable.addObserver(this);
    }
    
    public void update(Observable obs, Object arg) { // Observable 과 추가 파라미터를 둘 다 받는다. 
        if(obs instanceof WeatherData) {
            WeatherData weatherData = (WeatherData)obs;
            this.temperature = weatherData.getTemperature();
            this.humidity = weatherData.getHumidity();
            display();
        }        
    }
    
    public void display() {
        // 최근에 얻은 기온과 습도를 출력한다.
        System.out.println("Current conditions : " + temperature + "F degrees and " + humidity + "% humidity");
    }
}