본문 바로가기
👩🏻‍💻Technical things/Design pattern

[Design pattern] Observer Pattern

by Bㅐ추 2020. 6. 22.
728x90
반응형

본 게시글은 Head First Design Pattern을 읽고 정리한 내용 입니다.

해당 소스코드는, https://github.com/so1gging/Design-Pattern를 참고하세요:)


0. 정의

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다. 즉, 어떤 객체에 이벤트가 발생했을 때, 이 객체와 관련된 객체들 (옵저버들)에게 통지하는 패턴.

객체의 상태가 바뀌면 특정 객체에 의존하지 않으면서 상태의 변경을 관련된 객체들에게 통지하는 것이 가능해진다.

이 패턴은 Pub/Sub(발행/구독) 모델으로 불리기도 한다.

 

신문 구독방법을 생각해보자.

 

독자가 특정 신문사에 구독 시청을 하면 매번 새로운 신문지가 나올 때 마다 배달을 받을 수 있다. 계속 구독자로 남아있는 이상 계속해서 신문을 받을 수 있다. 만약 신문을 더 이상 보고 싶지 않으면 구독 해지 신청을 할 수 있다. 여기서의 신문사를 Subject(주제), 구독자를 Obsever(옵저버)라고 부른다.

 

1. 요구사항

 

다음과 같은 기상 모니터링 어플리케이션을 만든다고 가정해보자.

  1. Weather Class
    • 세 가지 측정 값(온도, 습도, 기압)을 알아내기 위한 getter method
    • 새로운 기상 측정 데이터가 나올 때 마다 measurementsChanged()가 호출됨. (이 method가 어떤 원리로 호출되는지 보단 이 method가 어떤 식으로든 호출된다는 것 자체가 중요하다.)
  2.  기상 데이터를 사용하는 세 개의 디스플레이 항목을 구현
  3. 시스템이 확장 가능해야 함.

 

결과

public class WeatherData {
	public void measurementsChange() {
    	float temp = getTemperature();
        float humidity = getHumidity();
        float pressure = getPressure();
        
        display1.update(temp, humidity, pressure);
        display2.update(temp, humidity, pressure);
        display3.update(temp, humidity, pressure);
    }
	//....
}

 위 처럼 만들 수는 있지만..

 

위의 코드는 문제점이 존재한다. 무엇인가 유연하지 않으며, 전에 배운 디자인 패턴의 원칙과는 동떨어져보인다.

우선 구체적인 구현에 맞춰 코딩했기 때문에 프로그램을 고치지 않고서는 다른 디스플레이 항목을 추가/제거할 수 없다. 그리고 디스플레이 항목과 데이터를 주고받는 데 있어서 적어도 공통된 인터페이스를 사용하고 있는 것 같긴 하다. 모두 update()메소드를 가지고 있으니까. 바뀔 수 있는 부분은 캡슐화해야 한다.

 

 

2. Observer pattern에 대한 개요

위 기상 모니터링 애플리케이션 코드를 리팩토링하기 전에 Observer 패턴에 대해 알아보자.

위 그림에서 Subject 객체와 Observer 객체는 Loose Coupling(느슨한 결합) 하다고 볼 수 있다.

=> 두 객체가 느슨하게 결합되어 있다는 것은 , 그 둘이 상호작용을 하긴 하지만 서로에 대해 잘 모른다는 것을 의미한다.


 1) Subject 객체가 옵저버에 대해 아는 것은 옵저버가 특정 인터페이스만 구현한 것만 알 수 있다. 옵저버의 구상 클래스가 무엇인지, 옵저버가 무엇을 하는지 등에 대해서 알 필요가 없다.
 2) 옵저버는 언제든지 추가, 삭제 될수있다. Subject는 Obsrver 인터페이스를 구현하는 객체의 목록에만 의존하기 때문에 언제든지 새로운 옵저버를 추가할 수 있다.
 3) 새로운 형식의 옵저버를 추가하려고 할때, Subject를 전혀 변경할 필요가 없다. 옵저버가 되어야 하는 새로운 구상 클래스가 생겼다고 가정해보자. 이 때도 새로운 클래스 형식을 받아들일 수 있도록 주제를 바꿔야할 필요는 없다. 새로운 클래스에 Observer 인터페이스를 구현하고 옵저버로 등록하기만 하면 된다. (그리고, 위에서도 말했듯이 Subject는 옵저버가 특정 인터페이스 즉 Observer를 구현한 것만 알 기 때문에 어떤 클래스인지 알 필요 없다.)
 4) Subject와 Observer는 독립적으로 재사용 될 수있다.
 5) 주제와 옵저버의 코드가 변경되더라도 Code에 대한 Side effect가 없다.

 

디자인원칙
서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용한다.

 

3. Observer pattern을 기상 모니터링 어플리케이션에 적용

 

 

interface Subect.java

public interface Subject {
	public void registerObserver(Observer o);
	public void removeObserver(Observer o);
	public void notifyObservers();
}

 

 

WeatherData.java

setMeashrements(..)메서드를 이용하여 변수들을 설정해주면,

measurementsChanged()메서드가 실행되고,

measurementsChanged()메서드 내에는 notifyObservers()메서드가 실행되어

WeatherData내에 저장된 Observer(List observers) 들에게 update()메서드로 알려준다.

public class WeatherData implements Subject {
	private List<Observer> observers; // Observer 객체들을 저장하기 위한 List
	private float temperature;
	private float humidity;
	private float pressure;
	
	public WeatherData() {
		observers = new ArrayList<Observer>(); // 생성자에서 추가한다.
	}
	
	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 (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();
	}
    
    //...생략
}

 

interface Observer.java

Observer 인터페이스는 모든 옵저버 클래스에서 구현해야 한다. 따라서, 모든 옵저버는 update()메소드를 구현해야 한다.

public interface Observer {
	public void update(float temp,float humidity,float pressure);
}

 

DisplayElement.java생략

 

CurrentConditionsDisplay.java

public class CurrentConditionsDisplay implements Observer, DisplayElement {
	private float temperature;
	private float humidity;
	private WeatherData weatherData;
	
	// 생성자에 weatherDate라는 주제 객체가 전달되며, 그 객체를 써서 디스플레이를 옵저버로 등록한다.
	public CurrentConditionsDisplay(WeatherData weatherData) {
		this.weatherData = weatherData;
		weatherData.registerObserver(this);
	}
	
	public void update(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		display();
	}
	
	public void display() {
		System.out.println("Current conditions: " + temperature 
			+ "F degrees and " + humidity + "% humidity");
	}
}

 

main class

	public static void main(String[] args) {
		WeatherData weatherData = new WeatherData(); // [1] 우선 WeatherData객체를 생성한다.
	
		// [2] 세 개의 디스플레이를 생성하면서 WeatherData객체를 인자로 전달.
		CurrentConditionsDisplay currentDisplay = new CurrentConditionsDisplay(weatherData); 
		StatisticsDisplay statisticsDisplay = new StatisticsDisplay(weatherData);
		ForecastDisplay forecastDisplay = new ForecastDisplay(weatherData);
		/*		내부에서는..
		 * 		this.weatherData = weatherData;
		 *		weatherData.registerObserver(this);
		 * 		weatherDate클래스 내부의 List에 해당 Observer(여기서는 Display)가 추가된다.
		 * */

		// [3] 새로운 기상 측정값이 들어온다
		weatherData.setMeasurements(80, 65, 30.4f);
		weatherData.setMeasurements(82, 70, 29.2f);
		weatherData.setMeasurements(78, 90, 29.2f);
	
	}

[3] 번에서의 

weatherData.setMeasurements(80, 65, 30.4f);

해당 코드가 실행될 때의 순서를 살펴보면.

	public void setMeasurements(float temperature, float humidity, float pressure) {
		this.temperature = temperature;
		this.humidity = humidity;
		this.pressure = pressure;
		measurementsChanged();
	}

변수들이 설정되고, meaurementsChanged()가 실행된다.

 

	public void measurementsChanged() {
		notifyObservers();
	}

그리고, notifyObservers()를 호출한다.

 

	public void notifyObservers() {
		for (Observer observer : observers) {
			observer.update(temperature, humidity, pressure);
		}
	}

 notifyObservers()는 WeatherData(Subject)가 Observer들에게 통지하는 메서드이다.

List에 저장된 Observer들에게 update한다.

 

	public void update(float temp, float humidity, float pressure) {
        lastPressure = currentPressure;
		currentPressure = pressure;

		display();
	}

Observer에는 다음과 같이 변수를 셋팅하고 display()한다.

 

그런데 말이죠..

지금까지는 Subject가 Observer에게 통지를 해야 만 (정보를 보내야 만) Observer들이 받을 수 (가져갈 수) 있었다. Observer입장에서는 통지가 올 때만 정보를 가져올 수 있고 , 가져오고 싶지 않아도 통지가 오면 억지로 정보를 가져와야 만 한다. Observer들이 필요할 때만 정보를 가져갈 수 있게 할 수 없을까?

 

자바에서는 이런 데이터를 보내는 방식(Push)데이터를 가져가는 방식(Pull)을 마음대로 쓸 수 있게 할 수 있도록 내장된 옵저버 패턴이 존재한다.

 

4. 자바 내장 Observer pattern 사용하기

지금까지 직접 만든 코드로 옵저버 패턴을 구현했다. 하지만 자바에서 몇 가지 API를 통해 자체적으로 옵저버 패턴을 지원하기도 한다.

코드는 생략하겠다.

 

5. 다시 정의 확인

한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다. 즉, 어떤 객체에 이벤트가 발생했을 때, 이 객체와 관련된 객체들 (옵저버들)에게 통지하는 패턴.

객체의 상태가 바뀌면 특정 객체에 의존하지 않으면서 상태의 변경을 관련된 객체들에게 통지하는 것이 가능해진다.

이 패턴은 Pub/Sub(발행/구독) 모델으로 불리기도 한다.

 

728x90
반응형