brunch

제13장: 옵저버 패턴

객체 간 일대다 의존성 관리

by jeromeNa

구독이라는 말은 많이 들어봤을 겁니다. 요즘은 대부분이 구독 서비스를 제공하고 있습니다. 뉴스 구독 서비스는 새로운 뉴스가 나오면 구독자들에게 자동으로 알림을 제공합니다. 구독자들은 뉴스 사이트를 계속 확인할 필요가 없이, 사이트가 구독자에게 새로운 뉴스를 알려줍니다. 예전에는 이런 서비스가 구독 서비스였지만, 지금은 한 번에 결제가 아닌, 매월 이용료를 결제하는 방식으로 인식되고 있습니다.


구독은 ‘정해진 기간 동안 책이나 신문, 잡지 따위를 구입하여 읽음’이라는 의미를 가지고 있습니다. 영어 단어인 ‘subscribe’는 ‘sub(아래에)’와 ‘scribere(쓰다)’라는 라틴어에서 유래했으며, ‘서명하다’, ‘이름을 적다’라는 의미가 있습니다.


고대 로마에서는 문서 하단에 자신의 이름을 적어 동의나 승인을 표시하던 관행에서 비롯되었고, 중세 시대에는 ‘서면으로 동의하다’ 또는 ‘지지를 서약하다’라는 의미로 확장되었습니다. 15세기 구텐베르크의 인쇄술 발명으로 인해, 정기 출판물이 등장하면서 이 단어(subscribe)의 의미가 더욱 발전했습니다. 즉, 정기적으로 발행되는 출판물을 지속적으로 받아보기 위해 신청하고 비용을 지불하는 행위를 말합니다.


옵저버(Observer)는 ‘관찰자’, ‘감시자’라는 의미를 가지고 있습니다. 옵저버는 구독과 비슷한 맥락을 가지고 있습니다. 구독은 발행을 ‘관찰’하다는 의미로 볼 수 있습니다. 발행을 ‘관찰’하고 있다가 발행이 되면 반응을 합니다.


여기 브런치스토리도 마찬가지입니다. 관심 있는 작가를 구독하면 작가가 새로운 글을 발행하면 메시지를 보내줍니다. 작가를 ‘구독’하고 있는 것이고, 이 말은 작가를 ‘관찰’하는 ‘옵저버’인 것입니다.


1970년대 스몰토크의 MVC(Model-View-Controller) 아키텍처 개발 과정에서 모델(Model)의 변경사항을 여러 뷰(View)에 전파하는 메커니즘으로 옵저버 패턴이 사용되었습니다. 1980년대 자바 이전의 객체지향 프레임워크인 시스템에서도 이벤트 처리를 위해 비슷한 패턴이 사용되었고, 1994년 GoF에 의해 공식적으로 문서화되었습니다.


옵저버 패턴은 발행하는 주체, 즉 ‘관찰’되는 주체(객체)가 있어야 하고, 그 주체의 변경 사항을 전달받는 구독자(‘관찰자’)가 있어야 합니다. 주체의 상태가 변경되면 변경되었다는 알림을 발행하고, 그 알림을 구독자가 받아서 반응합니다.


기상 모니터링 시스템을 예제로 살펴보겠습니다. 기상 스테이션에서 날씨 데이터를 수집하고, 이를 다양한 디스플레이 장치에 표시하는 시스템을 생각하시면 됩니다.


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

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

// 구체적 주체: 날씨 데이터
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 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();
}

// Getter 메서드들
public float getTemperature() {
return temperature;
}

public float getHumidity() {
return humidity;
}

public float getPressure() {
return pressure;
}
}

// 구체적 옵저버: 현재 조건 디스플레이 (구독자)
class CurrentConditionsDisplay implements Observer {
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();
}

public void display() {
System.out.println("현재 조건: 온도 " + temperature + "°C, 습도 " + humidity + "%");
}
}

// 구체적 옵저버: 기상 통계 디스플레이
class StatisticsDisplay implements Observer {
private float maxTemp = 0.0f;
private float minTemp = 200.0f;
private float tempSum = 0.0f;
private int numReadings = 0;
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();
}

public void display() {
System.out.println("기상 통계: 평균/최고/최저 온도 = " +
(tempSum / numReadings) + "/" + maxTemp + "/" + minTemp);
}
}

// 구체적 옵저버: 기상 예보 디스플레이
class ForecastDisplay implements Observer {
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();
}

public void display() {
System.out.print("기상 예보: ");
if (currentPressure > lastPressure) {
System.out.println("날씨가 좋아지고 있습니다!");
} else if (currentPressure == lastPressure) {
System.out.println("변화가 없습니다.");
} else {
System.out.println("더 시원하고 비가 올 가능성이 있습니다.");
}
}
}


위와 같이 객체들을 정의했으면, 아래 코드와 같이 사용합니다.


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(27, 65, 30.4f);

System.out.println("\n두 번째 기상 정보 갱신:");
weatherData.setMeasurements(28, 70, 29.2f);

System.out.println("\n세 번째 기상 정보 갱신:");
weatherData.setMeasurements(26, 90, 29.2f);

// 옵저버 제거 테스트
System.out.println("\n기상 예보 디스플레이 제거 후:");
weatherData.removeObserver(forecastDisplay);
weatherData.setMeasurements(24, 80, 30.6f);
}
}


코드들을 실행하면 다음과 같습니다.


첫 번째 기상 정보 갱신:
현재 조건: 온도 27.0°C, 습도 65.0%
기상 통계: 평균/최고/최저 온도 = 27.0/27.0/27.0
기상 예보: 날씨가 좋아지고 있습니다!

두 번째 기상 정보 갱신:
현재 조건: 온도 28.0°C, 습도 70.0%
기상 통계: 평균/최고/최저 온도 = 27.5/28.0/27.0
기상 예보: 더 시원하고 비가 올 가능성이 있습니다.

세 번째 기상 정보 갱신:
현재 조건: 온도 26.0°C, 습도 90.0%
기상 통계: 평균/최고/최저 온도 = 27.0/28.0/26.0
기상 예보: 변화가 없습니다.

기상 예보 디스플레이 제거 후:
현재 조건: 온도 24.0°C, 습도 80.0%
기상 통계: 평균/최고/최저 온도 = 26.25/28.0/24.0


옵저버 패턴에서 ‘구독(subscribe)’은 핵심 개념입니다. 이 패턴은 ‘발행-구독(publish-subscribe)’모델로도 불립니다. 구독은 옵저버가 주체에 자신을 등록하는 해위를 말하고, ‘구독 취소’는 옵저버가 더 이상 알림을 받지 않기 위해 등록을 해제하는 행위, ‘발행’은 주체가 상태 변화를 모든 구독자에게 알리는 행위를 말합니다. 프로그래밍에서 이러한 구독 메커니즘은 느슨한 결합(loose coupling)을 가능하게 하며, 유지 보수와 확장성에 큰 이점을 제공합니다.


‘구독 경제’라는 단어를 들어보셨을 겁니다. 제품을 일회성으로 판매하는 대신, 지속적인 서비스로 제공하며 정기적인 요금을 받는 방식을 말합니다. 소프트웨어, 콘텐츠, 실물 제품까지 구독 모델로 전환되고 있는 시점입니다.


일회성으로 구매함으로써 ‘내 거’라는 강한 소유의 결합보다는 ‘구독’으로 언제라도 해지가 가능한 느슨한 결합으로 한정된 ‘소유’보다는 다양한 자신의 취향을 살릴 수 있습니다. 시스템 또한 커다란 하나의 통합 시스템이 아닌 분할된 다양한 시스템끼리의 구독 조합으로 더 향상된 서비스(MSA - Micro Service Architecture)를 지향하고 있습니다.

keyword