본문 바로가기
CS 지식 공부방/디자인패턴

[헤드퍼스트 디자인패턴 2장] 옵저버 패턴

by Loper Lee 2022. 6. 15.

2장 - 옵저버 패턴

옵저버 패턴이란?

옵저버패턴 클래스 다이어그램

신문사 + 구독자 = 옵저버 패턴

  • 신문사는 주제(subject), 구독자를 옵저버(observer)라고 부른다.
  • 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에 연락이 가고 자동으로 내용이 갱신되는 방식으로 다대다 의존성을 정의합니다.
  • 옵저버 패턴은 보통 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인으로 구현합니다.
  • 옵저버로 등록하거나 탈퇴하고 싶은 경우 Subject인터페이스에 있는 메소드를 사용합니다.
  • 옵저버가 될 가능성이 있는 객체는 Observer인터페이스를 구현해야합니다.
    • 주제의 상태가 바뀌었을 때 호출되는 update() 메소드만 구현
  • 옵저버에는 데이터를 보내주는 푸시(push) 방식과 옵저버가 데이터를 가져오는 풀(pull) 방식으로 구분할 수 있습니다.
    • Push 방식의 경우 매게변수가 변경될 가능성이 있다.
    • Pull 방식의 경우 옵저버가 필요한 데이터를 가져올 수있기 때문에 더 느슨하게 결합될 수 있다.
    • 대체로 옵저버가 필요한 데이터를 가져오도록 하는게 더 좋은 방법입니다.

느슨한 결합

  • 느슨한 결합은 객체들이 상호작용 할 수는 있지만, 서로를 잘 모르는 관계를 의미합니다.

    → 느슨할 결합을 이용하면 유연성이 아주 좋아짐

  • 주제는 옵저버가 특정 인터페이스를 구현한다는 사실만 알고있습니다.

  • 새로운 옵저버가 추가될때도 주제는 신경쓸 필요없이 Observer인터페이스만 구현되어 있으면 됩니다.

  • 주제나 옵저버를 다은 용도로 활용할 일이 있다고 해도 손쉽게 재활용이 가능합니다.

  • 서로 느슨하게 결합되어 있으므로 주제나 옵저버 인터페이스를 구현한다는 조건만 만족한다면 어떻게 고쳐도 문제가 생기지 않습니다.

요구사항

Weather-O-Rama의 차세대 인터넷 기반 기상 스테이션 구축 시스템

기상정보 객체 다이어그램

호출되는 객체의 메소드

  • Weather-O-Rama와 계약을 체결하면 WeatherData 객체를 통해 현재 조건, 기상통계, 기상 예보 이 3가지 디스플레이에 갱신하며 보여주어야 함
  • 기상 관측이 변경된다면 measurementsChanged() 가 호출된다.
  • 다른 개발자가 새로운 디스플레이를 추가할 수 있고, 이후 화면에 몇가지의 항목들이 더 추가 될 수 있음

해결 방법 1 : 메소드 직접 호출

첫번째 해결 방법은 아주 단순하게 measurementsChanged() 메소드에 디스플레이 객체의 메소드를 직접호출하는 방법이다.

public class WeatherData {
    public void  measurementsChanged() {
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();

        currentConditionsDisplay.update(temp, humidity, pressure);
        statisticDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);
    }
}

단점

  • 디스플레이 객체가 변경될 수도 있다.
    • 바뀔 수 있는 부분은 캡슐화 해야한다.
  • 구체적인 구현에 맞춰 코딩했으므로 프로그램을 고치지 않고는 다른 디스플레이 항목을 추가할 수 없다.
  • 실행 중 디스플레이 객체를 더하거나 뺄수없다.

해결 방법 2 : 옵저버 패턴 적용

이제 옵저버 패턴을 적용해 느슨한 결합을 만족시키고, 확장성을 지니도록 변경해보겠습니다.

public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

주제가 되는 인터페이스

public interface Observer {
        // push 방식
    void update(float temperature, float humidity, float pressure);
        // pull 방식
    void update();
}

옵저버 인터페이스

public interface DisplayElement {
        void display();
}

디스플레이 인터페이스

public class WeatherData implements Subject {
    private List<Observer> observers;
    private float temperature;
    private float humidity;
    private float pressure;

    public WeatherData() {
        this.observers = new ArrayList<Observer>();
    }
    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }
    @Override
    public void removeObserver(Observer o) {
        observers.remove(observers.indexOf(o));
    }
    @Override
    public void notifyObservers() {
                // 실질적인 옵저버의 호출을 담당
        for (Observer observer : observers) {
            observer.update(temperature, humidity, pressure);
        }
    }

    float getTemperature() {
        return temperature;
    }
    float getHumidity() {
        return humidity;
    }
    float getPressure() {
        return pressure;
    }

    void measurementsChanged(){
        notifyObservers();
    }

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

주제를 구현한 WeatherData 클래스

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 update(float temperature, float humidity, float pressure) {
        this.temperature = temperature;
        this.humidity = humidity;
        display();
    }

    // 풀 방식을 경우
    @Override
    public void update() {
        this.temperature = weatherData.getTemperature();
        this.humidity = WeatherData.getHumidity();
        display();
    }

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

옵저버 디스플레이

장점

  • 객체들간 느슨한 결합을 이뤄내었기 때문에 각자의 책임에만 집중하면 된다.
  • 런타임 도중에도 옵저버 객체를 추가하거나, 삭제할 수 있다.
  • 옵저버 객체도 주제 인터페이스를 구현한다면 주제이자 옵저버가 될 수 있다.

정리

  • 옵저버 패턴은 객체들 사이에 일대다 관계를 정의합니다.
  • 옵저버 패턴을 사용하면 주제가 데이터를 보내거나 옵저버가 데이터를 가져올 수 있지만 일반적으로 데이터를 가져오는게 더 옳은 방식입니다.
  • 옵저버 패턴은 자주 사용되는 패턴으로 MVC를 배울때도 다시 거론됩니다.