관리 메뉴

SW

[개발서적] 헤드퍼스트 디자인 패턴 Ch2. 옵저버(Observer) 패턴 본문

개발서적/헤드퍼스트 디자인패턴

[개발서적] 헤드퍼스트 디자인 패턴 Ch2. 옵저버(Observer) 패턴

SWKo 2024. 7. 14. 17:51

1. 기상 모니터링 애플리케이션 알아보기

기상 스테이션 : 실제 기상 정보를 수집하는 물리 장비

WeatherData 객체 : 기상 스테이션으로부터 오는 정보를 추적하는 객체

디스플레이 장비 : 사용자에게 현재 기상 조건을 보여주는 장비

 

WeatherData 객체로 현재 조건, 기상 통계, 기상 예보, 이렇게 3가지 항목을 디스플레이 장비에서 갱신해 가면서 보여 주는 애플리케이션을 만들어야 합니다.

 

2. WeatherData 클래스 살펴보기

기상 스테이션에서 갱신된 데이터를 가져오는 일은 WeatherData 객체가 알아서 해줍니다.

WeatherData에서 갱신된 값을 가져올 때마다 measurementChanged() 메소드가 호출됩니다.

 

/*
 * 기상 관측값이
 * 갱신될 때마다
 * 이 메소드가 호출됩니다.
 *
 */
public void measurementsChanged() {
	// 코드가 들어갈 자리
}

 

현재 조건, 기상 통계, 기상 예보를 보여주는 3가지 디스플레이가 업데이트되도록 measurementChanged() 메소드를 구현해봅니다.

 

3. 구현 목표

  • WeatherData 클래스에는 3가지 측정값(온도, 습도, 기압)의 getter 메소드가 있습니다.
  • 새로운 측정값이 들어올 때마다 measurementsChanged() 메소드가 호출됩니다. (어떤 식으로 호출되는지 알 필요는 없습니다.)
  • 디스플레이 요소 3가지(현재 조건, 기상 통계, 기상 예보)를 구현해야 합니다.
  • 디스플레이를 업데이트 하도록 measurementsChanged() 메소드에 코드를 추가합니다.

 

4. 기상 스테이션 코드 추가하기

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

 

위 코드의 문제점

  • 디스플레이를 갱신하는 부분에서 인터페이스가 아닌 구체적인 구현을 바탕으로 코딩을 하였습니다.
  • 새로운 디스플레이 항목이 추가/제거 될 때마다 코드를 수정해야 합니다.
  • 실행 중에 동적으로 디스플레이 항목을 추가하거나 제거할 수 없습니다.

 

5. 옵저버 패턴의 이해

신문사(주제: Subject) + 구독자(옵저버: Observer) = 옵저버 패턴

 

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

 

 

옵저버는 주제(Subject)에 딸려 있으며 주제의 상태가 바뀌면 옵저버(Observer)에게 정보가 전달됩니다.

 

 

6. 옵저버 패턴의 구조

주제와 옵저버를 인터페이스로 구현해 구상클래스로 구현하면 됩니다.

옵저버가 될 가능성이 있는 객체는 반드시 Observer 인터페이스를 구현해야 합니다.

 

 

 

Observer 인터페이스에는 주제의 상태가 바뀌었을 때 호출되는 update() 메소드밖에 없습니다.

Observer 인터페이스만 구현한다면 무엇이든 Observer 클래스가 될 수 있습니다.

 

 

7. 느슨한 결합의 위력

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

느슨한 결합을 활용하면 유연성이 좋아집니다. 옵저버 패턴은 느슨한 결합의 훌륭한 예시입니다.

 

  • Subject는 Observer가 특정 인터페이스(Observer 인터페이스)를 구현한다는 사실만 압니다.
  • Observer는 언제든지 새로 추가할 수 있습니다.
  • 새로운 형식의 Observer를 추가할 때도 Subject를 변경할 필요가 전혀 없습니다.
  • Subject와 Observer는 서로 독립적으로 재사용할 수 있습니다.
  • Subject나 Observer가 달라져도 서로에게 영향을 미치지는 않습니다.
디자인 원칙
상호작용하는 객체 사이에는 가능하면 느슨한 결합을 사용해야 한다.

 

 

8. 기상 스테이션 설계하기

Observer 인터페이스 : Subject에서 Observer에게 갱신된 정보를 전달하는 방법을 제공합니다.

Display들은 WeatherData 객체로부터 얻은 측정값들을 보여줍니다.

 

 

Observer 인터페이스를 구현하기만 하면 어떤 클래스든 Observer가 될 수 있습니다.

다른 Display를 추가해도 기존 코드를 수정하지 않아도 됩니다. (확장성이 좋습니다.)

 

 

9. 기상 스테이션 구현하기

Subject, Observer, Display 인터페이스

// Subject 인터페이스
interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

// Observer 인터페이스
interface Observer {
    void update(float temp, float humidity, float pressure);
}

// DisplayElement 인터페이스
interface DisplayElement {
    void display();
}
  • Observer 인터페이스는 모든 Observer 클래스에서 구현해야 합니다.( = update() 메소드를 구현해야 합니다.)

 

WeatherData 클래스

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

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

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

    @Override
    public void removeObserver(Observer o) {
        int i = observers.indexOf(o);
        if (i >= 0) {
            observers.remove(i);
        }
    }

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

 

  • registerObserver, removeObserver에서는 Observer 객체를 이자로 받아서 추가/제거 합니다.
  • weatherData가 setMeasurements 메소드를 통해 최신으로 반영되면 measurementsChanged 메소드에서 Observer들에게 알립니다.

디스플레이 요소 구현하기

class CurrentConditionsDisplay implements Observer, DisplayElement {
    private float temperature;
    private float humidity;
    private Subject weatherData;

    public CurrentConditionsDisplay(Subject 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 display() {
        System.out.println("Current conditions: " + temperature + "F degrees and " + humidity + "% humidity");
    }
}

 

class StatisticsDisplay implements Observer, DisplayElement {
    private float maxTemp = 0.0f;
    private float minTemp = 200;
    private float tempSum = 0.0f;
    private int numReadings;
    private Subject weatherData;

    public StatisticsDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        tempSum += temperature;
        numReadings++;

        if (temperature > maxTemp) {
            maxTemp = temperature;
        }

        if (temperature < minTemp) {
            minTemp = temperature;
        }

        display();
    }

    @Override
    public void display() {
        System.out.println("Avg/Max/Min temperature = " + (tempSum / numReadings) + "/" + maxTemp + "/" + minTemp);
    }
}

 

class ForecastDisplay implements Observer, DisplayElement {
    private float currentPressure = 29.92f;
    private float lastPressure;
    private Subject weatherData;

    public ForecastDisplay(Subject weatherData) {
        this.weatherData = weatherData;
        weatherData.registerObserver(this);
    }

    @Override
    public void update(float temperature, float humidity, float pressure) {
        lastPressure = currentPressure;
        currentPressure = pressure;

        display();
    }

    @Override
    public void display() {
        System.out.print("Forecast: ");
        if (currentPressure > lastPressure) {
            System.out.println("Improving weather on the way!");
        } else if (currentPressure == lastPressure) {
            System.out.println("More of the same");
        } else if (currentPressure < lastPressure) {
            System.out.println("Watch out for cooler, rainy weather");
        }
    }
}

 

  • 각 Display 클래스에서 update 메서드를 구현해줍니다.
  • update()가 호출되면 일련의 처리 후에 display()를 수행합니다.

기상 스테이션 테스트

public class WeatherStation {
    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.setMeasurements(80, 65, 30.4f);
        weatherData.setMeasurements(82, 70, 29.2f);
        weatherData.setMeasurements(78, 90, 29.2f);
    }
}
  • 현재 조건, 기상 통계, 기상 예보가 3번씩 print 됩니다.
  • 기상 스테이션에서 디스플레이 요소를 알고 있으니, 메서드 하나만 호출해도 측정값을 알려줄 수 있습니다.

 

10. 옵저버 데이터의 Push, Pull 방식

현재 만들어 놓은 WeatherData 디자인은 하나의 데이터만 갱신해도 되는 상황에서도 update() 메소드에 모든 데이터를 보내고 있습니다.

만약 이후에 "풍속"과 같은 데이터 값을 추가한다면 어떨까요? 모든 디스플레이의 update() 메소드를 바꿔야 합니다.

 

Subject가 Observer로 데이터를 보내는 Push 방식을 사용하거나 Observer가 Subject로부터 데이터를 당겨오는 Pull 방식 중 선택하는 것은 구현 방법의 문제입니다. 

하지만, 대체로 Pull 방식이 더 좋습니다.

Subject가 자신의 데이터에 관한 Getter 메소드를 가지게 만들고 필요한 데이터를 당겨올 때 해당 메소드를 호출할 수 있도록 옵저버를 고쳐 주기만 하면 됩니다.

 

Subject에서 update 하기

public void notifyObservers() {
  for(Observer observer: observers) {
    observer.update();
  }
}
  • Observer의 update 메소드를 인자 없이 호출하도록 수정합니다.

Observer에서 알림 받기

public interface Observer {
  void update();
}
  • Observer 인터페이스에서 update() 메소드에 매개변수가 없도록 수정합니다.
@Override
public float getTemperature() {
    return temperature;
}

@Override
public float getHumidity() {
    return humidity;
}

@Override
public float getPressure() {
    return pressure;
}
  • weatherData에 Getter 메소드를 추가해줍니다.
public void update() {
  this.temperature = weatherData.getTemperature();
  this.humidity = weatherData.getHumidity();
  display();
}
  • Subject의 날씨 데이터를 가져오도록 각 Observer 구상클래스(update)를 수정해줍니다.
  • CurrentConditionsPlay 클래스는 위와 같이 수정됩니다.

 

FE 옵저버 패턴 : https://patterns-dev-kr.github.io/design-patterns/observer-pattern/

 

Observer 패턴

Observable을 활용해 Subscriber에게 이벤트 발생을 알린다 - Observer 패턴에서 특정 객체를 구독할 수 있는데. 구독하는 주체를 Observer라 하고. 구독 가능한 객체를 Observable…

patterns-dev-kr.github.io

 

Comments