본문 바로가기

Fundamental/Design Pattern

[디자인 패턴] 2. 옵저버 패턴(Observer Pattern)

 

옵저버 패턴(Observer Pattern)
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용을 갱신해주는 패턴으로써, 일대다 의존성을 정의하는 패턴이다. 어느 한 객체가 다른 객체의 상태를 구독해야 하는 경우 효과적이다.

 

기상 스테이션 시스템 구축 프로젝트

 

Weather-O-Rama라는 업체로부터 날씨 정보를 표시하는 디스플레이 애플리케이션을 일을 수주하게 되었다.

Weather-O-Rama에서 요구하는 기능은 아래와 같다.

 

  1. 실시간으로 변화하는 날씨 정보(상태, 온도, 습도, 기압 등)를 표시하는 기능이 있어야 함
  2. 추후 이러한 디스플레이 기능을 다른 개발자들도 추가할 수 있어야 함

 

기본적으로 Weather-O-Rama는 WeatherData라는 클래스를 제공해주고 있으며, 날씨 정보가 변경되면 그 클래스의 measurementsChanged()라는 메서드가 호출된다고 한다.

 

 

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

 

 

이 상황에서 서로 다른 3개의 디스플레이 객체들의 상태를 변경해주려면 아래와 같이 구현할 수 있다.

 

public class WeatherData {
	
    // ...
    
    public void measurementsChanged() {
    	
        float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();
        
        // 서로 다른 3개의 디스플레이의 상태 업데이트
        currentConditionDisplay.update(temp, humidity, pressure);
        statisticDisplay.update(temp, humidity, pressure);
        forecastDisplay.update(temp, humidity, pressure);
        
    }
    
}

 

 

하지만 위와 같이 구현하면, WeatherData라는 객체는 자신을 구독하고 있는 디스플레이 객체들을 미리 알고 있어야 하므로, 구독하고 있는 디스플레이 객체가 바뀌거나 추가됐을 때 WeatherData 객체 코드를 계속 수정해줘야 한다.

(즉, 바뀌는 부분이 캡슐화되어 있지 않기 때문에 좋은 설계 방법은 아니다. 위 Display 객체들과 같이 추후 변경될 가능성이 있는 요소들은 구현체를 사용하는 것이 아니라 interface를 활용해서 캡슐화 하는 것이 좋다)

 

이제 WeatherData, 각 Display 객체들을 옵저버 패턴으로 만들어 보자!

 

https://refactoring.guru/design-patterns/observer

 

 

우선 Subject(구독받는 객체), Observer(구독하는 객체)를 interface로 만들어 두고...

 

public interface Subject {
    public void registerObserver(Observer o); // 구독 신청
    public void removeObserver(Observer o); // 구독 해지
    public void notifyObservers(); // 구독 중인 옵저버들에게 전달
}

public interface Observer {
    // Subject가 보내준 상태값으로 업데이트
    public void update(float temp, float humidity, float pressure);
}

public interface DisplayElement {
    public void display(); // 상태값을 화면에 표시
}

 

 

이제 Subject 인터페이스를 구현해서 WeatherData 클래스를 캡슐화한다.

 

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

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

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

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

    public void notifyObservers() {
        for(Observer o : observers) {
            o.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 class CurrentConditionDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private WeatherData weatherData;

    public CurrentConditionDisplay(WeatherData weatherData) {
        this.weatherData = weatherData;
    }

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

    public void display() {
        System.out.println("Current temperature: " + temperature + "F, humidity: " + humidity + "%");
    }
}

 

 

마지막으로 실행해보면...

 

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

        CurrentConditionDisplay currentConditionDisplay = new CurrentConditionDisplay(weatherData);
        weatherData.registerObserver(currentConditionDisplay);

        weatherData.setMeasurements(80, 70, 60);
    }
}

 

 

위와 같이 인터페이스를 사용해서 구현하면, Subject 구현체는 정확한 내용은 모르지만 Observer 구현체가 update()라는 메서드를 가지고 있고, 상태가 변경되었을 때 update() 메서드를 호출하면 된다라는 것만 알고 있으면 되기 때문에, 객체 간의 결합을 느슨하게 만들 수 있다.

 

이렇게 옵저버 패턴은 Subject와 Observer 객체가 일대다 관계로 이루어져 있고, Subject의 상태가 변경되었을 때 Observer들에게 알려주는 패턴이며, JavaScript의 이벤트스위프트의 key-value 옵저핑 프로토콜에도 쓰이는 것을 확인할 수 있다.

 

옵저버 패턴에선 상태가 변경될 때마다 옵저버들에게 알려주는 것이 아니라, 옵저버가 Subject의 값을 필요로 할 때에만 getter() 함수를 사용해서 상태값을 가져오는 풀(Pull) 방식으로 구현할 수도 있다. 상황에 따라 풀 방식이 더 효율적일 수 있으므로 참고하면 좋다.


옵저버 패턴에서 배울 수 있는 객체지향 원칙

  • 바뀌는 부분은 캡슐화한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
  • 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.

 

옵저버 패턴(Observer Pattern)
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용을 갱신해주는 패턴으로써, 일대다 의존성을 정의하는 패턴이다. 어느 한 객체가 다른 객체의 상태를 구독해야 하는 경우 효과적이다.