본문 바로가기

ComputerScience/DesignPattern

[DesignPattern] Observer Pattern

Publishers + Subscribers = Observer Pattern

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

 

 

이미지 출처: https://en.wikipedia.org/wiki/Observer_pattern

1. Class Diagram (클래스 다이어그램)

  • Subject 클래스는 의존 객체(dependent objects)의 상태를 직접적으로 업데이트하지 않습니다.
  • 대신, Subject는 Observer 인터페이스의 메서드 update()를 참조하여 상태를 업데이트합니다. 이렇게 함으로써 Subject는 의존 객체의 상태가 어떻게 업데이트되는지에 독립적이게 됩니다. Observer1과 Observer2 클래스는 Observer 인터페이스를 구현하고, 자신의 상태를 Subject와 동기화하기 위해 update() 메서드를 사용합니다.

2. Sequence Diagram (순서 다이어그램)

  • Observer1과 Observer2 객체는 Subject1에서 attach(this)를 호출하여 자신을 등록합니다. 이로써 Subject1은 이 두 객체를 상태 변경의 관찰 대상으로 등록합니다.
  • 만약 Subject1의 상태가 변경된다면, Subject1은 notify()를 호출합니다. notify()는 등록된 Observer1과 Observer2 객체에게 update() 메서드를 호출합니다.
  • Observer1과 Observer2는 이후 getState()를 호출하여 Subject1의 변경된 데이터를 요청하고, 그 데이터를 사용하여 자신의 상태를 업데이트 (동기화)합니다. 

이러한 설계는 Observer 패턴을 따라 객체 간의 느슨한 결합을 제공하며, Subject가 여러 Observer 객체에게 동시에 상태 변경을 통지할 수 있도록 합니다. 이로써 객체 간의 유연성과 재사용성이 향상됩니다.

Subject

객체들은 이 인터페이스를 사용하여 자신을 옵서버로 등록하거나 옵서버에서 제거하는 데 이용합니다
이렇게 함으로써 Subject 인터페이스는 객체 간의 관계를 설정하고 관리하는데 사용됩니다.

Concrete Subject (subject의 구현체)

Concrete Subject는 항상 Subject 인터페이스를 구현합니다. register 및 remove 메서드 외에도, 구체적인 Subject는 state가 변경될 때 모든 현재 옵서버들을 업데이트하기 위해 사용되는 notifyObservers() 메서드를 구현합니다.
이러한 디자인은 Observer 패턴에서 Concrete Subject가 Observer에게 상태 변화를 알리고, Observer들이 이에 대응하는 역할을 수행할 수 있도록 하는데 중점을 둡니다.

Observer

모든 옵서버는 Observer 인터페이스를 구현해야 합니다. 이 인터페이스는 단 하나의 메서드인 update()를 가지고 있으며, 이 메서드는 Subject의 상태가 변경될 때 호출됩니다.
이러한 설계는 Subject와 Observer 간의 느슨한 결합을 유지하면서도 Subject의 상태 변화에 대한 효과적인 응답을 제공합니다.

Concrete Observer (Observer의 구현체)

Concrete Observer들은 Observer 인터페이스를 구현한 어떤 클래스든 될 수 있습니다. 각각의 Observer는 Concrete Subject에 등록하여 업데이트를 받을 수 있습니다.
이러한 구조는 Observer 패턴에서 Subject와 Observer 간의 유연성을 제공하며, 여러 클래스들이 서로간의 독립성을 유지하면서도 상호 작용할 수 있도록 합니다.

디자인 원칙

상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 합니다.
느슨하게 결합하는 디자인을 사용하면 변경사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있습니다.
옵저버 패턴은 느슨한 결합을 보여주는 훌륭한 예입니다.

느슨한 결합(Loose Coupling)

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

옵저버 패턴이 느슨한 결합을 만드는 방식

  • Subject는 Observer가 특정 인터페이스를 구현한다는 사실만 알고 있다.
  • Observer는 언제든지 새로 추가할 수 있다.
  • 새로운 형식의 Observer를 추가해도 Subject 코드를 변경할 필요가 없다.
  • Subject와 Observer는 서로 독립적으로 재사용할 수 있다.
  • Subject와 Observer는 달라져도 서로에게 영향을 끼치지 않는다.

옵저버 패턴 예시

1. data push


기상 관측 결과가 update 되는 WeatherData 객체가 있고, 이 update 된 결과를 다양한 Display 객체들이 받아서 원하는 형식으로 display 해야한다고 해봅시다.
Display들이 Observer, WeatherData가 Subject가 됩니다.

 

대략적인 호출 순서

  1. Observer들은 생성자로 받아온 weathreData에 자기 자신을 register 한다.
  2. 기상에 변화가 생기면 WeatherData의 상태가 변경되는 코드와 함께 measurementChanged()가 호출된다.
  3. measurementChanged()에서 notifyObservers()를 호출
  4. notifyObservers()에서 List에 등록된 Observer에 notify(update 메서드)를 보낸다.
  5. Observer는 update 메서드를 통해 data를 받아 Display 객체들에서는 해당 data를 Display 특성에 맞게 display()한다.

Subject

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

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

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

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

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyObservers() {
        for (Observer observer: observers) {
            observer.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();
    }
}

 

Observer 

public interface Observer {
    void update(float temp, float humidity, float pressure);
}

public class ConditionDisplay implements Observer, DisplayElement{
    private float humidity;
    private float temperature;

    private WeatherData weatherData;

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

    @Override
    public void display() {
        System.out.println("now temp: " + temperature + ", 습도: " + humidity);
    }

    @Override
    public void update(float temp, float humidity, float pressure) {
        this.temperature = temp;
        this.humidity = humidity;
        display();
    }
}

 

main code

public class Application {
    public static void main(String[] args) {
        WeatherData weatherData = new WeatherData();
        ConditionDisplay conditionDisplay = new ConditionDisplay(weatherData);
        StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);

        weatherData.setMeasurements(12.0f,55f,20f);
        weatherData.setMeasurements(-1.0f,55f,17f);
    }
}

 

Output

now temp: 12.0, 습도: 55.0
Avg/Max/Min temperature = 12.0/12.0/12.0
now temp: -1.0, 습도: 55.0
Avg/Max/Min temperature = 5.5/12.0/-1.0

Process finished with exit code 0

 

2. data pull

 

위에서는 subject가 data를 push하고, observer가 data를 받아서 가공했습니다.

이렇게 되면, 필요한 data만 가지고 있는 것이 아니라 필요 없는 data도 일단은 받고 사용해야 하는 경우가 생깁니다.

확장성의 측면으로 볼때, observer에서 notify 함수가 불렸을 때 subject객체로부터 data를 가져오는 방식(pull)이 더 나은 선택일 수 있습니다.

observer는 subject에 등록과 삭제하기 위해 subject 객체를 가지므로 subject.getData()를 통해 데이터를 pull 해올 수 있습니다.

 

수정된 Observer code

public interface Observer {
    void update();
}

public class ConditionDisplay implements Observer, DisplayElement{
    private float humidity;
    private float temperature;

    private WeatherData;

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

    @Override
    public void display() {
        System.out.println("now temp: " + temperature + ", 습도: " + humidity);
    }

    @Override
    public void update() {
    /*  이전 코드
     	this.temperature = temp;
        this.humidity = humidity;
   	*/
        this.temperature = weatherData.getTemperature(); // 인터페이스에서 getter를 추가 구현해야함
        this.humidity = weatherData.getHumidity();
        display();
    }
}

참고 자료 

https://github.com/IT-Book-Organization/HeadFirst-DesignPattern/tree/main/Chapter_02

https://en.wikipedia.org/wiki/Observer_pattern