디자인 패턴 - Observer Pattern

목표

  • 옵저버 패턴 이해 및 구현

1. 옵저버 패턴

정의

  • 옵저버 패턴은 한 객체의 상태가 변경되면 그 객체에 의존하는 다른 객체들에게 연락이 가고 자동으로 내용이 변경되는 일대다 의존성을 정의합니다

사용 시점

  • 객체 간의 일대다 의존성을 정의하고 싶을 때
  • 한 객체의 상태가 변경될 때 다른 객체들이 자동으로 알림을 받고 싶을 때
  • 느슨한 결합을 유지하면서 객체 간 통신이 필요할 때

클래스 다이어그램

classDiagram
    class Subject {
        <<interface>>
        +registerObserver(observer: Observer)
        +removeObserver(observer: Observer)
        +notifyObservers()
    }
    
    class Observer {
        <<interface>>
        +update(temperature: float, humidity: float, pressure: float)
    }
    
    class DisplayElement {
        <<interface>>
        +display()
    }
    
    class WeatherData {
        -observers: List~Observer~
        -temperature: float
        -humidity: float
        -pressure: float
        +registerObserver(observer: Observer)
        +removeObserver(observer: Observer)
        +notifyObservers()
        +measurementsChanged()
        +setMeasurements(temperature: float, humidity: float, pressure: float)
    }
    
    class CurrentConditionsDisplay {
        -weatherData: Subject
        -temperature: float
        -humidity: float
        +CurrentConditionsDisplay(weatherData: Subject)
        +update(temperature: float, humidity: float, pressure: float)
        +display()
    }
    
    class StatisticsDisplay {
        -weatherData: Subject
        -maxTemp: float
        -minTemp: float
        -tempSum: float
        -numReadings: int
        +StatisticsDisplay(weatherData: Subject)
        +update(temperature: float, humidity: float, pressure: float)
        +display()
    }
    
    class ForecastDisplay {
        -weatherData: Subject
        -currentPressure: float
        -lastPressure: float
        +ForecastDisplay(weatherData: Subject)
        +update(temperature: float, humidity: float, pressure: float)
        +display()
    }
    
    %% 인터페이스 구현 관계
    Subject <|.. WeatherData : implements
    Observer <|.. CurrentConditionsDisplay : implements
    Observer <|.. StatisticsDisplay : implements
    Observer <|.. ForecastDisplay : implements
    DisplayElement <|.. CurrentConditionsDisplay : implements
    DisplayElement <|.. StatisticsDisplay : implements
    DisplayElement <|.. ForecastDisplay : implements
    
    %% 구성 관계 (Composition)
    WeatherData *-- Observer : has
    
    %% 의존 관계 (Dependency)
    CurrentConditionsDisplay --> Subject : observes
    StatisticsDisplay --> Subject : observes
    ForecastDisplay --> Subject : observes

예시 코드

기상 스테이션과 다양한 디스플레이를 구현한 예시를 통해 옵저버 패턴을 살펴보겠습니다.

1. 주제(Subject) 인터페이스

1
2
3
4
5
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}

2. 옵저버(Observer) 인터페이스

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

3. 디스플레이 인터페이스

1
2
3
public interface DisplayElement {
void display();
}

4. 기상 데이터 클래스 (주제)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.ArrayList;
import java.util.List;

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

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

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

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

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

5. 현재 조건 디스플레이 (옵저버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CurrentConditionsDisplay implements Observer, DisplayElement {
private Subject weatherData;
private float temperature;
private float humidity;

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("현재 조건: " + temperature + "F 도, " + humidity + "% 습도");
}
}

6. 통계 디스플레이 (옵저버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class StatisticsDisplay implements Observer, DisplayElement {
private Subject weatherData;
private float maxTemp = 0.0f;
private float minTemp = 200.0f;
private float tempSum = 0.0f;
private int numReadings = 0;

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("평균/최고/최저 온도 = " + (tempSum / numReadings) + "/" + maxTemp + "/" + minTemp);
}
}

7. 예보 디스플레이 (옵저버)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ForecastDisplay implements Observer, DisplayElement {
private Subject weatherData;
private float currentPressure = 29.92f;
private float lastPressure;

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("예보: ");
if (currentPressure > lastPressure) {
System.out.println("날씨가 좋아지고 있습니다!");
} else if (currentPressure == lastPressure) {
System.out.println("날씨가 그대로입니다.");
} else if (currentPressure < lastPressure) {
System.out.println("비가 올 것 같습니다.");
}
}
}

8. 테스트 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);

System.out.println("=== 첫 번째 측정 ===");
weatherData.setMeasurements(80, 65, 30.4f);

System.out.println("\n=== 두 번째 측정 ===");
weatherData.setMeasurements(82, 70, 29.2f);

System.out.println("\n=== 세 번째 측정 ===");
weatherData.setMeasurements(78, 90, 29.2f);
}
}

실행 결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== 첫 번째 측정 ===
현재 조건: 80.0F 도, 65.0% 습도
평균/최고/최저 온도 = 80.0/80.0/80.0
예보: 날씨가 좋아지고 있습니다!

=== 두 번째 측정 ===
현재 조건: 82.0F 도, 70.0% 습도
평균/최고/최저 온도 = 81.0/82.0/80.0
예보: 비가 올 것 같습니다.

=== 세 번째 측정 ===
현재 조건: 78.0F 도, 90.0% 습도
평균/최고/최저 온도 = 80.0/82.0/78.0
예보: 날씨가 그대로입니다.

옵저버 패턴 장점

  1. 느슨한 결합: 주제와 옵저버가 느슨하게 결합되어 있습니다.
  2. 확장성: 새로운 옵저버를 쉽게 추가할 수 있습니다.
  3. 일대다 관계: 하나의 주제가 여러 옵저버에게 알림을 보낼 수 있습니다.
  4. 자동 알림: 주제의 상태가 변경되면 모든 옵저버가 자동으로 알림을 받습니다.

정리

  • 옵저버 패턴은 객체 간의 일대다 의존성을 정의할 때 사용합니다.
  • 한 객체의 상태가 변경될 때 다른 객체들이 자동으로 알림을 받고 싶을 때 사용합니다.
  • 느슨한 결합을 유지하면서 객체 간 통신이 필요할 때 사용합니다.

ref

  • 헤드퍼스트 디자인 패턴