1. 옵저버 패턴 정의
💡 옵저버 패턴 (observer pattern)
한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.
옵저버 패턴을 구현하는 방법에는 여러가지가 있지만 대부분 주제 (subject) 인터페이스와 옵저버 (observer) 인터페이스가 있는 클래스 디자인을 바탕으로 한다.
일대다(1:n) 관계는 주제와 옵저버에 의해 정의된다.
옵저버는 주제에 의존하며, 주제의 상태가 바뀌면 의존하는 옵저버에게 연락이 간다.
① subject : 상태를 저장하고 있는 주제 인터페이스를 구현한 하나의 주제객체
② observer : 주제객체에 의존하고 있는 옵저버 인터페이스를 구현한 여러개의 옵저버객체
데이터의 변경이 발생했을 때, 상대 클래스나 객체에 의존하지 않으면서 데이터 변경을 통보할 때 유용하다.
2. 옵저버 패턴 특징
행위 (Behavioral) 패턴
※ 데이터 전달 방식
Push 방식 : 주제객체에서 옵저버로 데이터를 보내는 방식
Pull 방식 : 옵저버에서 주제객체의 데이터를 가져가는 방식
옵저버 패턴은 주제와 옵저버가 느슨하게 결합되어 있는 객체 디자인을 제공한다.
■ 느슨한 결합 (Loose Coupling)
📌 디자인 원칙
느슨한 결합 (Loose Coupling)
서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.
주제는 옵저버가 특정 인터페이스 (옵저버 인터페이스) 를 구현한다는 것 외에는 모른다.
주제는 옵저버 인터페이스를 구현하는 객체 목록에만 의존하기 때문에 옵저버는 언제든지 추가, 변경, 삭제할 수 있다.
새 옵저버를 추가하려 할 때 주제를 변경할 필요가 없다.
주제나 옵저버를 변경해도 서로에게 영향을 주지 않는다. 그래서 주제와 옵저버는 서로 독립적으로 재사용 할 수 있다.
느슨하게 결합하는 디자인을 사용하면 변경사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.
3. 기본 예제
1. 주제 인터페이스
public interface Subject {
void registerObserver(Observer observer); // 옵저버 등록
void removeObserver(Observer observer); // 옵저버 삭제
void notifyObserver(); // 상태변경이 있을때 알림
}
2. 옵저버 인터페이스
public interface Observer {
void update(int value);
}
3. 주제 인터페이스 구현
public class ConcreteSubject implements Subject {
private List<Observer> observerList; // 옵저버들
private int value = 0;
public ConcreteSubject() {
this.observerList = new ArrayList<>();
}
@Override
public void registerObserver(Observer observer) {
observerList.add(observer); // 등록
}
@Override
public void removeObserver(Observer observer) {
observerList.remove(observer); // 삭제
}
@Override
public void notifyObserver() {
for (Observer o : observerList) {
o.update(value); // 등록되어있는 모든 옵저버들에게 값 전달
}
}
public void setValue(int value) { // 값이 변경이 있을때
this.value = value;
notifyObserver(); // 값 전달하는 메소드 호출
}
}
4. 옵저버 인터페이스 구현
public class ConcreteObserver implements Observer {
private int value;
private Subject subject;
public ConcreteObserver(Subject subject) {
this.subject = subject;
subject.registerObserver(this); // 옵저버 생성할때 주제에 등록
}
@Override
public void update(int value) { // 값 갱신되고
this.value = value;
display(); // 갱신 표시
}
public void display() {
System.out.println("Value = " + value);
}
// 테스트용 getter
public int getValue() {
return value;
}
}
5. 확인
@Test
void basic() {
ConcreteSubject concreteSubject = new ConcreteSubject(); // 주제생성
ConcreteObserver concreteObserver = new ConcreteObserver(concreteSubject); // 옵저버 생성 및 주제등록
concreteSubject.setValue(50);
concreteObserver.display();
Assertions.assertThat(concreteObserver.getValue()).isEqualTo(50); // OK
concreteSubject.setValue(100);
concreteObserver.display();
Assertions.assertThat(concreteObserver.getValue()).isEqualTo(100); // OK
// 제거하면 값이 변경되지 않는다.
concreteSubject.removeObserver(concreteObserver);
concreteSubject.setValue(33333);
concreteObserver.display();
Assertions.assertThat(concreteObserver.getValue() != 33333).isTrue(); // OK
Assertions.assertThat(concreteObserver.getValue()).isEqualTo(100); // OK
}
6. display() 출력확인
Value = 50
Value = 100
Value = 100 // 제거하면 값이 변경되지 않는다.
4. 예제 : 날씨데이터 (Push 방식)
날씨 데이터를 가지고 있는 한 회사와 데이터를 연동하여 여러 종류의 디스플레이에 각각의 정보를 출력해줘야하는 업무가 생겼다. 온도, 습도, 기압 정보를 출력한다.
간단히 구현해보자.
public class WeatherData{
// 인스턴스 변수들
// measurementsChanged() : 기상 관측값이 갱신될 때마다 알려주기위한 메소드
public void measurementsChanged(){
float temp = getTemperature(); // 온도
float humidity = getHumidity(); // 습도
float pressure = getPressure(); // 기압
// 디스플레이 갱신
currentCondirionsDisplay.update(temp, humidity, pressure);
statisticsDisplay.update(temp, humidity, pressure);
forecastDisplay.update(temp, humidity, pressure);
}
// 기타 메소드들
}
이렇게 구현하면 문제가 있다.
update() 메소드를 보면 구체적인 구현에 맞춰서 코딩이 되어 있다. 이러한 경우 프로그램을 고치지 않고서는 다른 디스플레이 항목을 추가 및 제거 할 수 없다. 향후에 바뀔 수 있는 부분은 캡슐화해서 분리해야 하므로, 효과적으로 모든 디스플레이들에게 Weather 의 상태정보를 알려줄 수 있는 방법이 필요하다.
해결해보자
1. 기상 스테이션 - 인터페이스
// 1. 주제 인터페이스
public interface Subject {
void registerobserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
// 2. 옵저버 인터페이스
public interface Observer {
void update(float temp, float humidity, float pressure);
}
// 3. 디스플레이 인터페이스
public interface DisplayElement {
void display();
}
2. subject 인터페이스를 구현
public class WeatherData implements Subject {
private List<Observer> observers; // 옵저버들
private float temperature;
private float humidity;
private float pressure;
public WeatherData() {
observers = new ArrayList<>();
}
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);
}
}
// 기상 관측값이 갱신될 때마다 알려주기위한 메소드
public void measurementsChanged() {
notifyObservers();
}
// 새로운 기상 관측값 갱신 메소드
public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
// getter
public float getTemperature() { return temperature; }
public float getHumidity() { return humidity; }
public float getPressure() { return pressure; }
}
3. Observer, DisplayElement 인터페이스 구현
// 현재 날씨
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); // 옵저버 등록
}
@Override
public void update(float temp, float humidity, float pressure) {
this.temperature = temp;
this.humidity = humidity;
display();
}
@Override
public void display() {
System.out.println("현재 날씨 = 기온 : " + temperature + "도, 습도 : " + humidity + "%");
}
}
// 예보
public class ForecastDisplay implements Observer, DisplayElement {
private float currentTemperature = 26.56f;
private float lastTemperature;
private WeatherData weatherData;
public ForecastDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this); // 옵저버 등록
}
public void update(float temp, float humidity, float pressure) {
lastTemperature = currentTemperature;
currentTemperature = temp;
display();
}
public void display() {
System.out.print("기상예보 = ");
if (currentTemperature > lastTemperature) {
System.out.println("어제보다 기온이 높아 따뜻합니다.");
} else if (currentTemperature == lastTemperature) {
System.out.println("어제와 비슷한 기온의 날씨입니다.");
} else if (currentTemperature < lastTemperature) {
System.out.println("기온이 내려가 어제보다 쌀쌀합니다.");
}
}
}
// 측정
public class StatisticsDisplay implements Observer, DisplayElement {
private float maxTemp = 0.0f;
private float minTemp = 10;
private float tempSum= 0.0f;
private int numReadings;
private WeatherData weatherData;
public StatisticsDisplay(WeatherData weatherData) {
this.weatherData = weatherData;
weatherData.registerObserver(this); // 옵저버 등록
}
public void update(float temp, float humidity, float pressure) {
tempSum += temp;
numReadings++;
if (temp > maxTemp) { maxTemp = temp; }
if (temp < minTemp) { minTemp = temp; }
display();
}
public void display() {
System.out.println("평균/최고/최저 온도 = " + (tempSum / numReadings)
+ "/" + maxTemp + "/" + minTemp);
}
}
4. 실행
@Test
void execute() {
WeatherData weatherData = new WeatherData();
// 옵저버 등록
CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData);
StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
weatherData.setMeasurements(20, 65, 30.4f);
weatherData.setMeasurements(15, 70, 29.2f);
weatherData.setMeasurements(32, 80, 27.2f);
weatherData.setMeasurements(32, 90, 29.2f);
// 예보 디스플레이만 삭제
weatherData.removeObserver(forecastDisplay);
weatherData.setMeasurements(27, 78, 31.1f);
}
5. 출력확인
현재 날씨 = 기온 : 20.0도, 습도 : 65.0%
평균/최고/최저 온도 = 20.0/20.0/10.0
기상예보 = 기온이 내려가 어제보다 쌀쌀합니다.
현재 날씨 = 기온 : 15.0도, 습도 : 70.0%
평균/최고/최저 온도 = 17.5/20.0/10.0
기상예보 = 기온이 내려가 어제보다 쌀쌀합니다.
현재 날씨 = 기온 : 32.0도, 습도 : 80.0%
평균/최고/최저 온도 = 22.333334/32.0/10.0
기상예보 = 어제보다 기온이 높아 따뜻합니다.
현재 날씨 = 기온 : 32.0도, 습도 : 90.0%
평균/최고/최저 온도 = 24.75/32.0/10.0
기상예보 = 어제와 비슷한 기온의 날씨입니다.
현재 날씨 = 기온 : 27.0도, 습도 : 78.0%
평균/최고/최저 온도 = 25.2/32.0/10.0
마지막 실행 결과에 예보 옵저버를 삭제시켰기 때문에 기상예보가 표시되지 않는 것을 확인할 수 있다.
■ UML Class Diagram