[디자인 패턴] 2. 옵저버 패턴

2022. 8. 23. 19:10CS/디자인 패턴

옵저버 패턴이란?

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의한다.

  • 주제(Subject Object)에서 중요한 데이터를 관리한다.
  • 주제 데이터가 바뀌면 옵저버에게 그 소식이 전해진다.
  • 옵저버 객체들은 주제를 구독하고 있으며(주제 객체에 등록되어 있으며) 주제 데이터가 바뀌면 갱신 내용을 전달받는다.
  • 옵저버가 아닌 객체는 주제 데이터가 바뀌어도 아무 연락을 받을 수 없다.

 

구현

  • 구현은 보통 주제 인터페이스와 옵저버 인터페이스가 들어있는 클래스 디자인으로 구현한다.

 

구조

  • Subject
    • 주제를 나타내는 인터페이스
    • 주제가 상태를 저장하고 제어한다. 데이터의 주인은 주제이다.
    • 객체에서 옵저버로 등록하거나 옵저버 목록에서 탈퇴하고 싶을 때는 이 인터페이스에 있는 메소드를 사용
    • 각 주제마다 여러 개의 옵저버가 있을 수 있다.
  • Observer
    • 옵저버는 상태를 사용한다.
    • 옵저버가 될 가능성이 있는 객체는 반드시 옵저버 인터페이스를 구현해야 한다. 이 인터페이스에는 주제의 상태가 바뀌었을 때 호출되는 update() 메서드만 있다.
  • Concrete Subject
    • 주제 역할을 하는 구상 클래스에는 항상 Subject 인터페이스를 구현해야 한다.
    • 주제 클래스에는 등록 및 해지용 메서드와 상태가 바뀔 때마다 모든 옵저버에게 연락하는 notifyObservers() 메소드도 구현해야 한다.
  • Concrete Observer
    • Observer 인터페이스만 구현한다면 무엇이든 옵저버 클래스가 될 수 있다. 각 옵저버는 특정 주제에 등록해서 연락받을 수 있다.

 

옵저버 패턴 예시

예시 코드는 ‘날씨 데이터(온도, 습도, 기압)를 받아 현재 조건, 기상 통계, 기상 예보 디스플레이’ 를 구성하는 것으로 이루어져 있다.

목표는 디스플레이를 구현하고 새로운 값이 들어올 때마다 디스플레이를 자동으로 업데이트 해야 한다.

 

WeatherData 클래스
--------------------
getTemperature()
getHumidity()
getPressure()

// WeatherData에서 온도, 습도, 기압 값을 새로 받을 때마다 호출되는 메소드
measurementsChanged() {
	// 디스플레이를 업데이트하는 코드
}

 

기존 코드(옵저버 패턴 적용 전)

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

	// 기타 메서드
}
  • 현황
    • 구체적인 구현에 맞추어 코딩
    • 공통 인터페이스를 사용하여 온도, 습도, 기압을 받아들이는 update() 메소드를 가지고 있다.
  • 문제점
    • 구체적인 구현에 맞추어 코딩했으므로 프로그램을 고치지 않고는 다른 디스플레이 항목을 추가하거나 제거할 수 없다.
  • 해결
    • 옵저버 패턴

 

기상 스테이션 옵저버 패턴 적용하여 설계

  • WeatherData는 Subject 인터페이스를 구현
  • 디스플레이 구현체들은 Observer, DisplayElement를 구현한다.
  • DisplayElement
    • 모든 디스플레이 요소에 구현 인터페이스를 하나 더 만듦. 디스플레이 항목에는 display() 메소드만 구현하면 된다.
  • ThirdPartyDisplay
    • 다른 개발자들도 Observer와 DisplayElement를 구현하여 새로운 디스플레이 요소를 만들 수 있다.

 

구현

- 인터페이스

public interface Subject {
    public void registerObserver(Observer o); // 옵저버 등록
    public void removeObserver(Observer o); // 옵저버 제거
    public void notifyObservers(); // 상태가 변경되었을 때 변경 내용을 알리려고 호출
}

public interface Observer {
    public void update(int value); // 기상정보가 변경되었을 때 옵저버에게 전달되는 상태값
}

public interface DisplayElement {
    public void display();
}

 

- Subject 구현체 (WeatherData)

public class WeatherData implements Subject {
    private List<Observer> observers; // 옵저버 객체들을 저장
    private float temperature;
    private float humidity;
    private 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 observer : observers) {
            observer.update(temperature, humidity, pressure);
    	}
    }

    // 옵저버에게 상태 변화를 알려주느 부분. 옵저버 인터페이스를 구현하는 옵저버 구현체들은
    // 모두 update()메서드가 있으므로 손쉽게 상태 변화를 알려줄 수 있다.
    public void measurementsChanged() {
    	notifyObservers();
    } // 갱신된 데이터를 옵저버에게 알림

    public void setMeasurements(float temperature, float humidity, float pressure) {
    	this.temperature = temperature;
    	this.humidity = humidity;
    	this.pressure = pressure;
    	measurementsChanged(); // 데이터가 갱신될 때 measurementsChanged 를 호출해 옵저버들에게 알림
    }

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

}

 

- 디스플레이 요소 구현

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

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

    public void display() {
        System.out.println("Current conditions: " + temperature 
            + "F degrees and " + humidity + "% humidity");
    }
}
  • 생성자에 weatherData 주제가 전달되며, 그 객체를 써서 디스플레이를 옵저버로 등록한다.
  • 옵저버가 호출되면 데이터를 저장하고 display()를 호출한다.

 

- 테스트

public static void main(String[] args) {
    WeatherData weatherData = new WeatherData(); // 객체 생성

    CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
    StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
    ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
    // 디스플레이 생성하고 weatherData 객체를 인자로 전달

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

    weatherData.removeObserver(forecastDisplay);
    weatherData.setMeasurements(62, 90, 28.1f);
}

 

- 푸시를 풀로 바꾸기

기존에는 하나의 데이터만 갱신해도 되는 상황에서도 update() 메소드에 모든 데이터를 보내도록 되어있다.

여기서 풍속이라는 데이터가 추가되면 어떨까? 대부분의 디스플레이에서 풍속데이터를 쓰지 않더라도, 모든 디스플레이의 update() 메소드를 바꿔줘야 한다.

옵저버로 데이터를 보내는 푸쉬(push)를 사용하거나 옵저버가 주제로부터 데이터를 당겨오는 풀(pull)을 사용하는 방법 중 어느 하나를 선택하는 일은 구현 방법의 문제이다. 하지만 대체로 데이터를 골라서 가져가는 풀(pull) 이 낫다.

pull 방식으로 바꾸는건 쉽다. 아래와 같이 바꾸면 된다.

public void notifyObservers() {
    for (Observer observer : observers) {
    	observer.update(temperature, humidity, pressure);
    }
}

=====> 매개변수 없애준다.

public void notifyObservers() {
    for (Observer observer : observers) {
    	observer.update();
    }
}
===> observer의 update에서 get메서드로로 데이터를 pull 한다.

public void update() {
    this.temperature = weatherData.getTemperature();
    this.humidity = weatherData.getHumidity();
    this.pressure = weatherData.getPressure();
    display();
}
  • 주제가 자신의 데이터에 관한 게터 메서드를 가지게 한다.
  • 필요한 데이터를 당겨올 때 해당 메서드를 호출할 수 있도록 옵저버를 수정한다.

 

디자인 원칙

  • 상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.
    • 느슨한 결합이란 객체들이 상호작용할 수 있지만 서로를 잘 모르는 관계를 의미
    • 느슨하게 결합하는 디자인을 사용하면 변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축 가능. 객체 사이의 상호의존성을 최소화하기 때문
    • 옵저버 패턴은 느슨한 결합을 보여주는 예이다.
      • 주제는 옵저버가 특정 인터페이스를 구현한다는 사실만 안다.
        • 옵저버의 구상 클래스를 알 필요없고 옵저버가 무엇을 하는지 알 필요 없다.
      • 옵저버는 언제든지 추가 가능
        • 주제는 옵저버 목록객체만 의존하므로 언제든지 추가 가능
      • 새로운 옵저버 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 없다.
        • 새로운 옵저버가 생겨도 등록만 하면 끝. 주제에 신경 안 써도 됨
      • 주제와 옵저버는 독립적으로 재사용할 수 있다.
        • 옵저버를 다른 용도로 활용할 일이 있더라도 재사용 가능. 둘이 단단히 결합되어 있지 않기 때문
      • 주제나 옵저버가 달라져도 서로에게 영향을 미치지 않는다.
        • 주제나 옵저버 인터페이스를 구현한다는 조건만 만족하면 어떻게 고쳐도 무관

 

기타

  • 스윙, RxJava, 자바빈, RMI, 코코아나 스위프트, 자바스크립트 같은 다른 언어의 프레임워크에서도 옵저버 패턴을 많이 사용한다.
  • 옵저버 패턴은 여러 개의 주제와 메세지 유형이 있는 복잡한 상황에서 사용하는 출판-구독 패턴과 친척이다.

'CS > 디자인 패턴' 카테고리의 다른 글

[디자인 패턴] 1. 전략 패턴  (0) 2022.08.22