[디자인 패턴] 2. the Observer Pattern

StandingAsh·2024년 10월 15일
3

참고: Head First Design Patterns

가정


날씨를 모니터링하는 어플리케이션이 있다. 어플리케이션은 아래 3가지 부분으로 구분할 수 있다.

  • 날씨 정보를 측정하는 Weather Station
  • 측정한 정보를 추적하고 업데이트 하는 WeatherData 객체
  • 업데이트된 정보를 보여주는 Display

이 중, WeatherData 객체를 이용하여 다음 3가지 화면을 Display에 업데이트하는 기능을 구현해야 한다.

  • 현재 날씨 상황(Current Conditions)
  • 기온, 습도, 대기압의 측정치(Statistics)
  • 일기 예보(Forecast)

WeatherData 객체

class WeatherData {
	getTemperature()
    getHumidity()
    getPressure()
    measurementsChanged()
}

WeatherData는 3가지 정보에 대한 getter 메소드와 measurementsChanged() 메소드를 가진다. 그 중 measurementsChanged() 메소드는 측정치에 업데이트가 생길 때 마다 호출할 메소드이다.

  • 이제 이 메소드가 Display의 3가지 화면을 업데이트 하도록 구현해보자.
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);
}

위와 같이 getter로 데이터를 받아온 후, Display 객체의 upate() 메소드에게 전달하도록 구현할 수 있겠다.

문제점

위의 코드는 과연 최선일까? 앞서 Strategy Pattern을 공부하면서 배웠던 디자인 원칙들을 되새기면서 문제점들을 분석해보자.

※ [디자인 패턴] 1. the Strategy Pattern 보러 가기

1. 인터페이스 대신 구현체에 대해 프로그래밍 하고 있다

앞에서 동적으로 구현체가 결정되도록 프로그래밍 하는 법을 배운 것이 무색하게 Display의 구현체들을 각각 선언하여 직접 접근하고있다.

2. 업데이트 로직이 중복된다

똑같은 Display 인터페이스의 구현체의, 똑같은 update() 메소드에게, 똑같은 매개변수를 전달하는 코드가 3줄이나 된다. 구현할 화면이 많아진다면 중복되는 코드도 더 많아질 것이다.

정리하자면 캡슐화의 부재라고 할 수 있다.

  • Observer Pattern을 적용하여 프로그램을 개선해보자.

옵저버 패턴이란?


유튜버와 구독자 관계를 생각해보자. 유튜버가 영상을 올리면 구독자에게 알림이 보내진다. 구독자는 알림을 받고, 구독중인 유튜버의 새로운 영상이 올라왔다는 것을 알 수 있다.

이제, 유튜버를 서브젝트(Subject), 구독자를 옵저버(Observer)에 대입해보자. 옵저버는 서브젝트를 '구독'중이다. 마치 유튜버의 구독자처럼 서브젝트에 변화가 생길 때 마다 옵저버는 그 내용을 전달받는다. 당연히 구독자는 여려명일 수 있다.

구독자는 원한다면 언제든지 구독 해지를 할 수 있으며, 새로운 유튜버를 구독할 수도 있다. 마찬가지로 옵저버도 유연하게 서브젝트를 구독 - 해지할 수 있다. 이런 옵저버와 서브젝트의 관계가 옵저버 패턴의 핵심이다.

따라서, 옵저버 패턴은 아래과 같이 정의한다.

어떤 객체의 상태가 바뀔 때, 그 객체에 의존하는 모든 객체들이 자동으로 업데이트 되도록 하는 객체들 간의 1:N 의존성

구현 방법은?

여러 가지 구현 방법이 있지만, 대부분 서브젝트와 옵저버 인터페이스를 이용해 구현한다.

interface Subject {
	registerObserver()
    removeObserver()
    notifyObservers()
}
interface Observer {
	update()
}

위 코드에서 알 수 있드시 옵저버의 등록, 제거, 상태 변화 알려주기 모두 서브젝트 인터페이스를 통해서 진행한다. 옵저버 인터페이스는 단지 상태 변화 정보를 받아 업데이트하는 update() 메소드 단 하나만을 가진다.

class ConcreteSubject implements Subject {
	register ... 
    remove ... 
    notify ... 
    
    getState()
    setState()
}
class ConcreteObserver implements Observer {
	update()
}

서브젝트 인터페이스의 구현체는 인터페이스에 선언된 메소드 외에도 '상태'에 대한 gettersetter를 가질 수 있다. 옵저버의 구현체는 update() 외에는 정해진 형식은 크게 없다. 왜냐하면, 옵저버 인터페이스를 구현한 그 어떤 객체도 옵저버가 될 수 있기 때문이다.

  • 옵저버 패턴은 서로 다른 여러 객체가 동일한 데이터직접 접근하도록 하지 않고, 서브젝트만이 데이터를 관리하도록 한다는 점에서 큰 객체지향적 의의를 가진다.

느슨한 결합(Loose Coupling)

두 객체가 '느슨하게 결합되어있다'라는 말은 상호작용이 가능하나, 서로의 정보에 대해선 잘 모른다는 의미이다. 옵저버 패턴은 서브젝트와 옵저버가 느슨하게 결합되어있는 관계라고 볼 수 있다.

서브젝트가 옵저버에 대해 아는 것은 '오브젝트 인터페이스를 구현하였구나'가 전부이다. 그 외의 정보는 필요하지 않다. 또한, 새로운 옵저버를 언제든 추가할 수 있으며 기존 옵저버들과 다른 '타입'의 옵저버 객체를 서브젝트의 수정 없이 등록할 수 있다.
이 느슨한 결합 덕분에 서브젝트와 옵저버는 독립적으로 재사용이 가능하고, 어느 한쪽의 수정은 다른 쪽에 전혀 영향을 주지 않는다.

  • 즉, 유연한 객체지향으로 객체들간의 상호 의존성최소화 할 수 있다.

여기서 오늘의 디자인 원칙을 배울 수 있다.

상호작용하는 두 객체들 사이에서는 느슨한 결합을 지향하라.

옵저버 패턴의 적용


자, 다시 날씨 어플리케이션으로 돌아가보자. 옵저버 패턴은 데이터를 관리할 '1'과 데이터 변경을 업데이트 받을 'N'의 결합이라고 하지 않았던가? 그렇다면, 우리의 WeatherData 객체가 1, Display가 N이라고 볼 수 있겠구나!

class WeatherData implements Subject {
	registerObserver()
    removeObserver()
    notifyObservers()
    
    getTemperature()
    getHumidity()
    getPressure()
    measurementsChanged()
}
class CurrentConditionsDisplay implements Observer, Display {
	update()
    display()
}

class StatisticsDisplay implements Observer, Display {
	update()
    display()
}

class ForecastDisplay implements Observer, Display {
	update()
    display()
}

위와 같은 구조로 설계할 수 있겠다. Display 인터페이스는 display()메소드만을 갖는다.

WeatherData 구현

이제 WeatherData 클래스를 구체적으로 어떻게 구현할 수 있을지 살펴보자. 서브젝트이기 때문에 옵저버를 관리할 자료구조를 가져야 할 것이다.
Java가 제공하는 ArrayList를 이용해 선언해보자.

private List<Observer> observers;

public WeatherData() { // 생성자
	observers = new ArrayList<>();
}

다음으로 register, remove, notify 메소드를 구현해보자.

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

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

public void notifyObservers() {
    for (int i = 0; i < observers.size(); i++) {
    	Observer observer = observers.get(i);
        observer.update(temperature, humidity, pressure);
    }
}

registerremove는 ArrayList가 제공하는 메소드로 간단하게 구현하였다. notify 역시 크게 어렵지 않은데, 서브젝트에 등록된 모든 옵저버들에게 새로운 데이터를 update해주면 그만이다.

자, 이제 드디어 measurementsChanged() 메소드를 구현할 차례이다. 과연 옵저버 패턴을 적용한 이 메소드 어떤 모습일까?

public void measurementsChanged() {
	notifyObservers();
}

구현할 내용이 없다!

measurementsChanged() 메소드의 요구사항은 데이터가 업데이트 될 때, Display에게 값을 넘겨주는 것이지 않았나? 그런데, WeatherData는 서브젝트 객체이기에 이미 notify 해주는 메소드를 가진다. 따라서, 그저 notifyObservers()를 호출해주면 그만이다.

Display 구현

이렇게 우리의 서브젝트 객체 WeatherData의 구현이 완료되었다. 우리의 옵저버 Display는 어떻게 구현할까?

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

Display의 구현체는 서브젝트, 즉 WeatherData를 인스턴스로 가지며 생성과 동시에 register한다.
옵저버 인터페이스의 유일한 메소드 update()를 구현해보자.

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

서브젝트가 넘겨준 정보를 받아 저장하고 display() 메소드를 호출하여 화면에 이쁘게 보여주면 Display의 구현도 끝이다.

정리


다시 강조하지만, 서브젝트 객체는 notify할 대상누구인지, 무얼 하는지 알지 못 할 뿐더러 알 필요도 없다.

이 점을 명심하고 옵저버 패턴을 적용하기 전의 measurementsChanged() 메소드를 다시 보면 느껴지는 점이 많을 것이다. 가령, 더 많은 종류의 Display를 만들어야 한다고 생각해보자.

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);
    
    // Display 추가
    anotherDisplay.update(temp, humidity, pressure);
    andAnotherDisplay.update(temp, humidity, pressure);
    moreDisplay.update(temp, humidity, pressure);
    andMoreDisplay.update(temp, humidity, pressure);
    andMoreAndMoreDisplay.update(temp, humidity, pressure);
    ...
}

Display가 하나 추가 될 때 마다 코드를 수정해야 한다. 심지어, 모두 똑같은 로직의 코드다.

반면, 똑같이 더 많은 Display를 추가해야 하는 상황에서 우리의 '서브젝트' WeatherData의 코드는 어떻게 바뀔까?

public void measurementsChanged() {
	notifyObservers();
}

당연하게도 코드를 수정할 필요가 없다. 새로 만들 Display가 무엇이던, 무엇을 하던 옵저버이기만 하면 객체 생성과 동시에 WeatherData라는 서브젝트에 register되고, 그 순간부터 모든 업데이트는 서브젝트가 알아서 해주기 때문이다.

이러한 객체지향 설계는 다른 디자인 패턴들과 함께 더 자세히 다루겠지만, 아주 중요한 객체지향 설계 원칙을 관통한다.

The 'Open - Closed' Principle (OCP) :
코드의 수정에 있어서는 닫혀있되, 프로그램의 확장에 있어서는 열려있도록 프로그래밍하라.

우리는 옵저버 패턴을 이용해 코드를 매번 수정하지 않아도 되도록 구현하여 Display 종류의 확장에 있어서 유연한 날씨 어플리케이션을 만들었다.

더 나아가...


Java는 java.util.Observer, java.util.Observable을 통해 옵저버 패턴을 built-in으로 제공한다.
직접 구현한 옵저버 패턴과의 차이점이라면, 서브젝트 객체는 Subject 인터페이스 대신 Observable 클래스를 상속하는 방식으로 사용하며, 더 다양한 기능을 제공한다.

  • 잠깐, 상속이라고? 우리는 구현체보단 인터페이스에 대해 프로그래밍하기로 배우지 않았나?

정확하다. 이것이 Java가 제공하는 Observable의 가장 큰 단점이다. 인터페이스가 아니기 때문에 이미 다른 수퍼클래스를 상속중인 객체는 Observable의 서브클래스가 되지 못한다.

또한, 인터페이스가 아닌 클래스이기 때문에 제공하는 메소드의 커스텀이 자유롭지 못하다. 추가로, Observer이 들어있는 API는 'Protected'로 선언되어 있다. 즉, 다른 패키지의 클래스들은 접근할 수 없다.

  • 이러한 단점들이 큰 영향을 주지 않는 프로그램이라면 내장된 옵저버 패턴을 사용하는 것도 방법이 될 수 있다. 그렇지 못한 경우, 서브젝트와 옵저버를 직접 구현하는 것이 유리 할 것이다.
profile
우당탕탕 백엔드 생존기

0개의 댓글