Observer Pattern 이란 기본적으로 변화에 대한 Notification 을 받고 싶을 때 사용할 수 있는 Pattern 으로, publish & subscribe (구독) 패턴이라고도 한다. 다르긴 하지만 Mediator Pattern 과 더불어 특정 trigger 에 대한 callback 을 진행해주는 패턴으로 볼 수 있다.
위 상황처럼 Humidity / Temperature / Pressure 에 대해 변화를 감지하고 이를 데이터화하여 UI 에 보여주는 모델이 있다고 해보자. Naive 하게 짠다면 Weather Data 를 보관하는 곳에서 바뀐 값들을 받아와 각 UI 에 setting 해주는 방식으로 짜여질 것이다.
// 어떤 방식으로든 변화를 감지해서 해당 method 가 발동된다
update(){
float temp = getCurTemperature();
float humidity = getCurHumidity();
float pressure = getCurPressure();
curDisplay(temp, humidity, pressure);
// ... 변화를 일일이 반영한다
}
이와 같은 방식은 변화해야하는 값이 추가되거나, 변화를 반영해야하는 Object 들이 추가될 경우 해당 method 를 매번 수정해줘야 하는 방식으로, OCP 가 전혀 지켜지지 않는다. (가령, 미세먼지 농도가 추가되어야 한다면, 보수되어야 하는 부분이 많아진다)
옵저버 패턴은 위와 같이 변화 관찰의 대상이 되는 Publisher 에 대하여 Object 들이 Observer Interface 를 통해 subscribe / unsubscribe 할 수 있다. 즉, 변화가 발생할 때마다 Observer 들에게 notify 를 통한 데이터가 전송된다. Subject 는 Observer Interface 에 대해서만 알고 있고(변화 감지시 notify 를 해줘야 하기 때문), Concrete Observer Class 들에 대해선 아무 의존성이 없기 때문에, Observer 들의 추가 / 변경이 훨씬 용이하기 된다. (위 그림에서 Subject 는 관찰 대상, Observer 는 구독을 하여 변화에 대한 알림을 받는 대상이다)
위 그림에서 Subject 는 Observer 에 대해 아는게 없고 (Observer 객체들이 있다는 것만 알고 있다), Observer 역시 데이터만 전달받도록 설계된다면 Subject 에 대해 아는게 없다. 따라서, 이 패턴은 Loosely Coupled 된 패턴인데, 두 객체가 서로에 대해 조금만 알고 있는 상황이기 때문이다. (실제 객체 데이터 레벨에서 참여 수준을 제어하므로, Object Scope 의 디자인 패턴이다)
옵저버 패턴은 Subject 에 의존하지 않기 때문에 code - reuse 측면에서 매우 우수하다. 위 모델만 봐서는 파악하기 어려우니, WeathterData 의 예시로 계속 살펴보자.
/*
관찰의 대상이되는 Publisher 이다
Publisher 는 자신한테 등록된 Observer 들을 직접 제어할 수 있다
*/
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyAllObservers();
}
/*
Observer 는 action 에 대한 update 를 진행해줘야 한다
> 현재 예시를 기준으로 생각한다
*/
public interface Observer {
public void update(float temp, float humidity, float pressure);
}
역할체들은 준비되었으니, 이제 구현체들을 준비한다. 관찰의 대상이 되는 측은 WeatherData 이며, Observer 로 등록되는 측은 CurrentConditionDisplay UI 장치이다.
public class WeatherDataV1 implements Subject{
private ArrayList<Observer> observers; // 자료구조의 형태로 observer 들을 보관한다
/*
현재 실제 Weather Data 들이 반영된다
*/
private float temp;
private float humidity;
private float pressure;
public WeatherDataV1(float temp, float humidity, float pressure) {
this.observers = new ArrayList<>();
this.temp = temp;
this.humidity = humidity;
this.pressure = pressure;
}
@Override
public void registerObserver(Observer observer) {
this.observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
this.observers.remove(observer);
}
// 바뀐 날씨 Data 들을 받아서 update 를 시작한다
public void setChangedMeasurements(float temp, float humidity, float pressure) {
this.temp = temp;
this.humidity = humidity;
this.pressure = pressure;
notifyAllObservers(); // 변화를 자신한테 관심있는 Observer 들에게 notify 날린다
}
@Override
public void notifyAllObservers() {
for (Observer observer : this.observers) {
observer.update(temp, humidity, pressure);
}
}
}
public class CurrentConditionDisplayV1 implements Observer {
private float temp;
private float humidity;
private float pressure;
/*
바뀌는 과정은 subject 에서 진행하고,
전달받은 데이터를 Observer 에서는 사용만하면 된다
*/
@Override
public void update(float temp, float humidity, float pressure) {
this.temp = temp;
this.humidity = humidity;
this.pressure = pressure;
showDisplay(); // 바뀌면 Display 를 바꾼다
}
private void showDisplay() {
System.out.println("Current Condition (T,H,P): (" + temp + ", " + humidity + ", " + pressure + ")");
}
}
이와 같이 Subject 에 대해 notify 를 받고 싶은 UI 들은 쉽게 Subject.addObserver() 형태로 등록만 하면 된다. 위와 같은 방식으로 다음과 같이 Test 를 수행해볼 수 있다.
public static void main(String[] args) {
// 해당 객체들은 자연스럽게 추가가 될텐데, Subject 와 Observer 의 역할체로써 선언되지는 않을 것 같다
// 자신의 원래 역할대로 선언되는게 더 자연스러울 것 같은 느낌
WeatherDataV1 weatherDataV1 = new WeatherDataV1(10, 10, 10);
CurrentConditionDisplayV1 displayV1 = new CurrentConditionDisplayV1();
weatherDataV1.registerObserver(displayV1);
weatherDataV1.setChangedMeasurements(20, 20, 20);
// 다른 Observer 도 추가하고 싶으면 간단하게 추가하면 된다
}
-- 실행 결과
Current Condition (T,H,P): (20.0, 20.0, 20.0)
Observer Pattern 은 각 Subject 와 Observer 의 Concrete 구현체들에 대한 의존도가 매우 낮기 때문에 code-reuse 가 우수하다. 이에 따라 Java 는 Observer Pattern 을 Observable, Observer Interface 로 제공한다 (Deprecated 되긴 했지만). Java Interface 를 사용하여 V2 를 만들어보자. (위에서 Subject 였던 객체는 Observable 객체로 상속된다)
/*
V2 : Java 에서 제공하는 Subject (Observable) 을 사용한다
> Observable abstract class 에 이미 Observer 보관소가 구현되어 있기 때문에,
> add, remove, notify 등을 사용하기만 하면 된다
> 다만, Observable 에게 자명하게 update 가 필요한 상황임을 전달하기 위해, Observable 에서 사용하는 boolean changed 값을 제어해줘야 한다
*/
public class WeatherDataV2 extends Observable {
// Observer 보관소는 Observable 에 구현되어 있다
private float temp;
private float humidity;
private float pressure;
// 바뀐 날씨 Data 들을 받아서 update 를 시작한다
public void setChangedMeasurements(float temp, float humidity, float pressure) {
this.temp = temp;
this.humidity = humidity;
this.pressure = pressure;
measurementChangeCallback(); // 변화를 notify 날린다
}
private void measurementChangeCallback() {
super.setChanged(); // Observable 의 boolean attribute 를 변경하여 notify 상황임을 체크해둔다 (안해주면 notfiy 안함)
super.notifyObservers();
}
public float getTemp() {
return temp;
}
public float getHumidity() {
return humidity;
}
public float getPressure() {
return pressure;
}
}
public class CurrentConditionDisplayV2 implements Observer {
// 등록은 외부에서 해줘도 되고, 내가 원하는대로 해주면 된다
private Observable observable;
// display 수치들
private float temp;
private float humidity;
private float pressure;
public CurrentConditionDisplayV2(Observable observable) {
this.observable = observable;
this.observable.addObserver(this); // 등록은 원하는 방식대로 해줘도 된다
}
// Java 에서는 뭘 전달해야할지 모르기 때문에 알아서 하라고 객체 그 자체를 전달한다
@Override
public void update(Observable o, Object arg) { // update notify 가 발생하면 호출된다
if (o instanceof WeatherDataV2) { // 내가 원하는 로직인지 체크
WeatherDataV2 wd = (WeatherDataV2) o; // 바뀐 데이터를 가져올 수 있다
this.temp = wd.getTemp();
this.humidity = wd.getHumidity();
this.pressure = wd.getPressure();
showDisplay();
} else if (o instanceof OTHER_OBSERVER){
// ...
}
// ...
// 이와 같은 방식을 통해 1 개 이상의 Observable (Subject) 를 등록할 수 있다
// 등록 방식만 잘 설계하면 된다
}
private void showDisplay() {
System.out.println("Current Condition (T,H,P): (" + temp + ", " + humidity + ", " + pressure + ")");
}
}
Subject Class 에서는 훨씬 간단하게 구현할 수 있고 Observer 측에 대한 그 어떤 의존성도 존재하지 않는다. Observer 들이 보관되어야 하는 자료구조도 관리해주기 위해 Observable 은 interface 가 아닌 abstract class 로 정의되었다. 그저 나는 Subject 객체이니 혹시나 나에게 등록된 애 있으면 알려라 정도로 구현된 수준으로, 별도 Observer 가 추가 / 삭제 / 변경되는 것도 모르게 할 수 있다.
Observer 측 또한 여러 개의 Subject 를 구독할 수 있는 구조로 설계가 된 점을 알 수 있으며, 원하는 Observable 에게 잘 등록만 되도록 설계되면 된다 (위에서는 하나이므로 attribute 로 두어서 생성자에서 직접 등록하게 하였다). 또한 Subject 객체 그 자체를 전달받으므로 notify 받는 Observer 마다 Subject 객체를 각자 활용하여 다르게 사용할 수도 있다.
정리하며
Observer Pattern 은 특정 trigger 에 의해 다른 Object 들이 변경을 감지할 때 도입해볼만한 Pattern 이다. Subject 는 Observer 에 대해서 아는 것이 없어서 유지 / 보수에 매우 유리하며, Java API 까지 사용하면 Observer 등록과 notify 로 수신한 data handling 이 매우 용이하기 때문이다.
Mediator 와 의존성 측면에서 많이 다르지만, callback 느낌의 pattern 이라는 점에서 유사한 것 같다. Java API 의 구조를 아니 훨씬 이해가 잘 된 것 같고, 앞으로 잘 사용해보면 좋을 것 같다.
출처
1) 에릭 프리먼, 『Head First Design Patterns』, O'Reilly Media (2004)
2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중
'SW 설계 > Design Pattern' 카테고리의 다른 글
[Design Pattern] Creational - 빌더 패턴 (Builder Pattern) (0) | 2023.10.21 |
---|---|
[Design Pattern] Creational - 팩토리 메소드 패턴 (Factory Method Pattern) (0) | 2023.10.20 |
[Design Pattern] Behavioral - 중재자 패턴 (Mediator Pattern) (1) | 2023.10.20 |
[Design Pattern] Behavior - 템플릿 메소드 패턴 (Template Method Pattern) (0) | 2023.10.19 |
[Design Pattern] Behavioral - 전략 패턴 (Strategy Pattern) (0) | 2023.10.19 |