디자인 패턴/Design Pattern

[디자인 패턴] 관찰자 패턴(Observer 패턴)

JunsuKim 2022. 10. 11.
728x90

관찰자 패턴(Observer 패턴)이란?

객체 사이에 관계가 이루어져 있을 때, 한 객체가 바뀌는 것을 계속 확인하고 있는 것이 아니라 상태가 변화됐을 때 그에 의존하는 객체에게 알려주는 패턴이다.

 

즉, 관찰하는 객체가 능동적으로 관찰하는 것이 아닌 관찰 대상으로부터 어떤 사건이 발생하였을 때 수동적으로 통보해주길 기다리는 것이다.

사용하는 이유

예를 들어보자.

기상청이 있을 때 시민들이 기상청에서 새로운 측정이 있는지를 계속 관찰하고 있는 것은 비효율적이다.

기상청에서 새로운 측정을 했을 때, "시민들에게 이러한 측정이 있었다!"라고 알려주는 것이 효율적이다.

위와 같이 WeatherData 클래스가 있을 때 온도, 습도, 기압 메서드는 계속해서 새로운 정보를 준다고 하자.

 이제 measurementsChanged 메서드에 각각의 출력을 지원하며, 새로운 형태의 메서드가 와도 출력 지원이 되도록 확장 가능하게 구현할 것이다. 

 

우선 다음과 같은 경우를 보자.

public void measurementChanged() {
   float temperature = getTemperature();
   float humidity = getHumidity();
   float pressure = getPressure();
   
   currentConditionsDisplay.update(temperature, humidity, pressure);
   statisticDisplay.update(temperature, humidity, pressure);
   forecastDisplay.update(temperature, humidity, pressure);
}

현재의 온도, 습도, 기압 정보를 받아 각 데이터에게 보내준다.

이렇게 구현할 경우 다음과 같은 문제점이 생길 수 있다.

  1. 추상된 데이터 타입을 가지고 프로그래밍하는 것이 아닌 구체적인 데이터 타입으로 프로그래밍하게 되어 의존 관계가 강하다.
  2. 만약 온도, 습도, 기압에 새로운 기능(ex. 풍속)이 추가된다면, 코드를 수정해야 한다.
  3. 실행시간에 등록되어있는 기능들을 추가 / 삭제할 수 없다. 이를 위해 Has-a로 목록을 유지하고 처리해줘야 한다.
  4. 변할 수 있는 부분을 추상화해야 하는데, 이를 안 하고 있다.(코드 아래 세줄) 

즉, 좋은 코드가 아니다.

이러한 코드를 해결하기 위해 사용하는 것이 바로 관찰자(Observer) 패턴이다. 

위의 예시에서는 기상청이 관찰 대상(Subject) 객체가 되고, 시민들이 관찰자(Observer) 객체가 된다.

구현

Subject 객체는 기본적으로 registerObserver(observer), removeObserver(observer), notifyObserver()를 갖는다.

관찰자를 등록할 수 있어야하고, 제거하는 방법도 있어야 하며, 자신의 상태가 변했을 때 관찰자들에게 통보할 메서드가 있어야 한다.

위와 같이 구현한다면 실직적으로 한 Subject 뿐만 아니라 다양한 Subject들이 공통적으로 가져야하는 기능들을 가지고 있다. 즉, 여러 책임을 가지고 있으므로 SRP를 위배한다.

 

이를 해결하기 위해 구성을 다음과 같이 바꾼다.

관찰자가 가져야하는 기본적인 기능들을 Subject에 구현한 후 상속을 이용하는 것이다.

Subject를 추상 클래스로 정의함으로써 코드 중복을 없앨 수 있다.

자바에서는 다중 상속이 안되므로 has-a를 이용하면 된다.

전체적인 구성을 보면 위와 같다. SRP 측면에선 바람직하진 않다.

Subject

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

Observer

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

WeatherData

import java.util.ArrayList;
import java.util.List;

public class WeatherData implements Subject {
   private List<Observer> observers = new ArrayList<>();
   private float temperature;
   private float humidity;
   private float pressure;

   @Override
   public void registerObserver(Observer o) {
      if(o!=null) observers.add(o);
   }

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

   @Override
   public void notifyObservers() {
      observers.forEach(o->o.update(temperature,humidity,pressure));
   }
   
   public void measurementChanged() {
      notifyObservers();
   }
   
   public void setMeasurement(float temperature, float humidity, float pressure) {
      this.temperature = temperature;
      this.humidity = humidity;
      this.pressure = pressure;
   }
}

push vs pull

push는 Subject가 Observer에게 데이터를 주는 것이다.

pull은 Observer가 Subject에게서 데이터를 빼오는 것이다.

위에서 봤던 예시는 데이터를 관찰 대상이 관찰자에게 보내주므로 push 방식이다.

 

각각의 예시를 보면 다음과 같다.

// push
@Override public void update(float temperature, float ...) {
    this.temperature = temperature;
    ....
}
// pull
@Override
public void update(WeatherData w) {
    temperature = w.getTemperature();
    ....
}

or

@Override
public void update() {
    temperature = WeatherData.getTemperature();
    ....
}

의존 관계 측면에서는 push 방식이 Subject에 의존하지 않아 느슨한 연결이 되는 반면, pull 방식은 Subject에 대한 더 많은 정보를 알아야 하며, 호출해야 하는 메서드도 많을 수 있기에 더 단단한 관계가 된다.

하지만 push 방식은 받아야 되는 데이터가 추가되면 코드가 수정되야 하고,  pull 방식은 WeatherData 클래스에서 가져오면 되므로 코드 수정이 필요하지 않다.

각각의 장단점이 있으므로, 관찰자 패턴을 구현할 때 push를 쓸지 pull을 쓸지는 상황에 맞게 사용하면 된다.

728x90

댓글