1. Observer Pattern

2021. 9. 6. 17:30Computer Sciences/Design Patterns

문제

인터넷 기반 기상정보스테이션을 구축해달라는 요청이 들어왔다. 현재의 기상 조건(기온, 습도, 기압)을 화면에 표시하는 기능의 프로그램이다. 그래서 우리 팀은 다음과 같이 데이터를 관리하는 클래스를 작성했다.

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);
    }

    // other methods...
}

measurementsChanged()가 호출되면 변경된 기온, 습도, 기압을 가져와서 현재상태 디스플레이, 통계 디스플레이, 기상 디스플레이의 update()를 모두 호출하는 방식이다. 딱봐도 유지보수가 헬이 될 것 같다. 이렇게 작성하면 다음과 같은 문제점이 있다.

  • 기상 조건이 추가되면 해당 조건 변수를 만들고 Getter를 만들어줘야 한다.
  • 기상 조건이 추가됨에 따라 해당 조건이 필요한 Displayupdate()도 전부 수정해야 한다.
  • Display가 추가되면 measurementsChanged()에 추가해야 한다.
  • 사용되는 필드들은 캡슐화되지 않았다.
  • 인터페이스가 아닌 구현체에 의존한다.

이는 모두 객체지향 설계 원칙 중 하나인 OCP에 위배된다. 또한 WeatherData라는 클래스가 매우 많은 책임을 지고 있으므로 SRP도 위배된다.

이렇게 WeatherData라는 중심 객체에 여러 Display가 연관되어 있는 패턴은 옵저버 패턴으로 유연하게 풀어낼 수 있다.

옵저버 패턴

옵저버 패턴은 상태를 저장하고 있는 객체를 해당 정보를 필요로 하는 객체들이 사용하는 패턴이다. 출판사(Subject)구독자(Observer)의 관계와 비슷한데, 출판사에서 새로운 간행물을 출판하면 이를 구독하고 있는 구독자들에게 모두 알리고 배송하주는 구조이다. 즉, 출판사에서 고객들에게 제공하는 정보 중 일부를 추가/변경/삭제 등과 같은 행위가 발생하면 해당 출판사를 구독한 고객들에게 모두 알리는 것이다.

  • Subject: 옵저버를 등록/제거하고 변경사항에 대해 옵저버들에게 알리는 행위를 담당한다.
    • 위에서는WeatherDataSubject의 구현체이며setMeasurements로 상태를 변경하면measurementsChanged()가 수행되고 옵저버들에게 알린다.
  • Observer:Observer를 구현한 옵저버들은 Subject의 변경 사항에 대한 알림을 받으면 각각의 옵저버들은 그에 맞춰서 상태를 변경한다.
    • 위에서는 CurrentConditionsDisplay, StatisticsDisplay, ForecastDisplayObserver의 구현체들이며 WeatherData의 필드값을 사용한다. 도중에 변경이 발생하면 알림을 받게 되고 그에 따라 각각의 옵저버들의 상태도 변경된다.
  • DisplayElement: 데이터를 표시하는 객체들이 이 인터페이스를 구현하게 하여 더 유연하게 만들었다. 이렇게 만들면 데이터를 표시하지 않는 객체들은 이 인터페이스를 구현할 필요없이 옵저버 등록만 하면 된다. 데이터 표시를 해야하는 새로운 객체는 이 인터페이스를 구현하면 된다.

이를 바탕으로 실제 코드로 구현해보자.

구현 코드

// Subject -> Observable
public interface Observable {

    public void registerObserver(Observer o);

    public void removeObserver(Observer o);

    public void notifyObservers();

}
import java.util.ArrayList;

public class WeatherData implements Observable {

    private ArrayList<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        observers = new ArrayList<>();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        int idx = observers.indexOf(o);
        if (idx >= 0) observers.remove(o);
    }

    @Override
    public void notifyObservers() {
    // JAVA 8의 스트림 사용
        observers.stream().forEach(o -> o.update(temperature, humidity, pressure));
    }

    public void measurementChanged() {
        notifyObservers();
    }

    public void setMeasurements(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        this.pressure = pressure;
        measurementChanged();
    }

    public float getTemperature() {
        return temperature;
    }

    public float getHumidity() {
        return humidity;
    }

    public float getPressure() {
        return pressure;
    }
}
public interface Observer {
    public void update(float temp, float humidity, float pressure);
}
public interface DisplayElement {
    public void display();
}
public class CurrentConditionsDisplay implements Observer, DisplayElement {

    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }
}
public class StatisticsDisplay implements Observer, DisplayElement {

    private float avg;
    private float max;
    private float min = Float.MIN_VALUE;
    private float hap;
    private int updateCount;
    private WeatherData weatherData;

    public StatisticsDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = " + avg + "/" + max + "/" + min);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        max = Math.max(max, temperature);
        min = min == Float.MIN_VALUE ? min = temperature : Math.min(min, temperature);
        hap = hap + temperature;
        avg = hap / ++updateCount;
        display();
    }
}
public class WeatherStation {

    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();

        CurrentConditionsDisplay currentConditionsDisplay = new CurrentConditionsDisplay(weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);

        weatherData.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}

실행 결과

Current conditions: 80.0F degrees and 65.0% humidity
Avg/Max/Min temperature = 80.0/80.0/80.0
Current conditions: 82.0F degrees and 70.0% humidity
Avg/Max/Min temperature = 81.0/82.0/82.0
Current conditions: 78.0F degrees and 90.0% humidity
Avg/Max/Min temperature = 80.0/82.0/78.0

💡 우리가 작성한 코드는 간단한 경우에는 동작하겠지만 멀티 쓰레드와 같이 복잡한 환경에서는 동기화 작업을 해주어야 한다.

자바에서 제공하는 Observable, Observer

자바에서는 옵저버 패턴을java.util.Observable,java.util.Observer로 지원했었다. 그러나 여러 문제점이 있어서 현재는 Deprecated되었다. 그래도 옵저버 패턴 구조는 같으니 간단히 훑어보자.

// java.util.Observable
// 눈여겨 볼 점은 클래스라는 것이다.
@Deprecated(since="9")
public class Observable extends Object {

    void addObserver(Observer o) {
        // 옵저버 추가 메서드
    }

    void notifyObservers() {
        // 옵저버들에게 알리는 메서드
    }

    protected void setChanged() {
        // 변경 사항을 적용하는 메서드
    }

    // ohters...
}
// java.util.Observer
@Deprecated(since="9")
public interface Observer {
    void update(Observable o, Object arg);
}

왜 Deprecated됐을까?

이런 클래스들을 제공해놓고 왜 Deprecated했을까? 그 이유는 Java API 문서에 나와있다.

This class and the Observer interface have been deprecated. The event model supported by Observer and Observable is quite limited, the order of notifications delivered by Observable is unspecified, and state changes are not in one-for-one correspondence with notifications. For a richer event model, consider using the java.beans package. For reliable and ordered messaging among threads, consider using one of the concurrent data structures in the java.util.concurrent package. For reactive streams style programming, see the Flow API.

요약하면 다음과 같다.

  • Observer, Observable에 의해 지원되는 이벤트 모델은 제한적이고, Observable의 알림 순서는 확실하지 않다.
  • 상태 변화는 알림과 일대일로 대응하지 않는다.
  • 9 이후의 이벤트 모델에서는java.beans패키지 사용을 고려해라.
  • 신뢰할 수 있고 쓰레드간 순서가 보장된 메시지를 위해서는java.util.concurrent패키지에서 제공하는 동시성 자료구조를 사용하는 것을 고려해라.
  • reactive stream 스타일의 프로그래밍에서는 Flow API를 찾아보아라.

정리

객체지향 프로그래밍의 디자인 패턴 중 옵저버 패턴에 대해서 알아봤다. 옵저버 패턴은 Observable한 객체를 필요로 하는 많은 Observer 객체들에게 Observable 객체가 데이터 추가/수정/삭제와 같은 변경사항에 대해 알림을 해주고 그에 따라 Observer들의 상태와 행위가 변경되는 패턴이다. 이제 디자인 패턴에 발을 디딘 코린이에게 옵저버 패턴 뿐만 아니라 앞으로 배울 디자인 패턴은 프로그래밍을 하는데 있어서 더 객체지향스럽고 유연한 사고방식을 얻을 것 같다. 앞으로 배울 것들도 기대가 된다.

Uploaded by Notion2Tistory v1.1.0

'Computer Sciences > Design Patterns' 카테고리의 다른 글

6. Command Pattern  (0) 2021.10.14
5. Singleton Pattern  (0) 2021.09.15
4. Abstract Factory Method  (0) 2021.09.15
3. Factory & Factory Method Pattern  (0) 2021.09.15
2. Decorator Pattern  (0) 2021.09.06