본문 바로가기

IT/헤드퍼스트 디자인패턴

[디자인패턴] 옵저버패턴

안녕하세요 남갯입니다.


오늘도 어김없이 헤드퍼스트 디자인패턴의 도움을 받았습니다.


오늘은 디자인패턴 중 옵져버 패턴에 대해 포스팅해보려고 합니다.


옵져버패턴이란?

옵저버 패턴은 한객체의 상태가 바뀌면 그 객체에 의존하는 다른객체들에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대 다의 의존성을 정의합니다. subject 혹은 Observable을 통해 구독을 하고있던 옵저버에게 연락하여 갱신을 시킵니다. 옵저버를 구독하고있는 의존된 객체들은 언제든지 Subject or Observable에게 구독을 요청할 수 있어야하고 원하는 시점이 구독을 취소 할 수 있어야합니다.



사건의 시작!

기상정보스테이션 구축프로젝트의 업체로 선정되어서 WeatherData 객체를 통해 기온 습도 기압을 추적합니다. 이 데이터를 통해 세가지의 디스플레이를 만들어야하며 데이터가 변할때마다 모든 디스플레이가 갱신되어야 합니다. 또한 디스플레이가 확장이 가능하도록 만들어야합니다.


기상프로그램을 만들기위해 해야할일

1. 기상 측정데이터가 갱신되면 measurementsChanged() 메소드가 불린다.

2. 디스플레이는 세개가 구현되어야하고 확장이 가능해야한다.



그렇게 받은 정보를 받고 다음날 받은 코드는 이렇습니다.



fun measurementsChanged(){
// 기상값이 바뀌었을때 불러짐
}

위에서 정의된 uml에서 이 메소드를 통해 부르면 되도록 정의되어 있습니다.


위에서 값이 불리기 때문에 


class WeatherData {
fun measurementsChanged() {
val temp: Float = getTemperature()
val humidity: Float = getHumidity()
val pressure: Float = getPressure()
//a.displayUpdate(temp,humidity,pressure)
//b.displayUpdate(temp,humidity,pressure)
//c.displayUpdate(temp,humidity,pressure)
}
}


이렇게 추가해보았습니다. 하지만 위에서 코드는 문제가 있습니다.

일단 displayUpdate 메소드를 보면 공통된 부분이고, 실제 디스플레이를 구체화 시켰기 때문에

WeatherData를 수정하지 않고는 디스플레이의 확장이 불가능합니다.


따라서 느슨한 결합인 (Loose Coupling)을 통해 서로 잘 모르는 상태로 만들어야합니다.


필요한 5가지

1. subject(주제)가 옵저버에 대해 아는부분은 옵져버 인터페이스를 통해 구현한다는점

2. 옵저버는 언제든지 추가가 가능하다는점

3. 옵저버가 추가되도 subject(주제)는 변경할 필요가 없다는점

4. 재사용이 가능하다는점

5. 변경이 되더라도 서로에게 영향을 미치면 안된다는점 

이 점을 이용해 느슨하게 만드는 디자인을 사용해야 합다.



여튼 본론으로 돌아가게 되면 옵저버 패턴을 이용해서 WeatherData를 바꾸려면 옵저버 패턴을 이용하면 되는데

옵저버 패턴이란 일대다(one to many) 의존성을 정의하므로 WeatherData는 일(one) 이고 디스플레이 항목은 (다) 를 만듭니다.



interface Subject {
fun registerObserver(o: Observer)
fun removeObserver(o: Observer)
fun notifyObservers()
}

실제 주제의 객체가 될 인터페이스를 구현하고 각각 구독과 해지를 할 매소드 그리고 변경을 알려줄 메소드를 구현합니다.


interface Observer {
fun update(temp: Float, humidity: Float, pressure: Float)
}

옵저버 인터페이스는 아까 옵저버에게 전달할 상태값을 정의합니다.


interface DisplayElement {
fun display()
}

디스플레이를 구현할 인터페이스도 만들어줍니다.


이 구현한 Subject를 통해 WeatherData를 구현해줍니다.


class WeatherData(
var observers: ArrayList<Observer> = arrayListOf(),
var temperature: Float = 0.0f,
var humidity: Float = 0.0f,
var pressure: Float = 0.0f
) : Subject {

//구독
override fun registerObserver(o: Observer) {
observers.add(o)
}
//구독 제거
override fun removeObserver(o: Observer) {
observers.remove(o)
}
//옵저버에게 데이터 변화를 알려주는부분
override fun notifyObservers() {
observers.forEach {
it.update(temperature, humidity, pressure)
}
}
//가상 스테이션으로부터 받은 데이터를 옵저버에게 알림
fun measurementsChanged() {
notifyObservers()
}
//강제적인 데이터변경을 테스트하기 위한 메솓,
fun setMeasurements(temperature: Float, humidity: Float, pressure: Float) {
this.temperature = temperature
this.humidity = humidity
this.pressure = pressure
measurementsChanged()
}
}


위와같이 구현해주고


class CurrentConditionDisplay(weatherData: Subject) : Observer, DisplayElement {
private var temperature: Float = 0.0f
private var humidity: Float = 0.0f
private var weatherData: Subject

init {
this.weatherData = weatherData
weatherData.registerObserver(this)
}

override fun update(temp: Float, humidity: Float, pressure: Float) {
this.humidity = humidity
this.temperature = temp
display()
}

override fun display() {
println("CurrentConditionDisplay ${temperature} F degress and humidity ${humidity} %")
}
}

class StatisticsDisplay(weatherData: Subject) : Observer, DisplayElement {
private var temperature: Float = 0.0f
private var humidity: Float = 0.0f
private var weatherData: Subject

init {
this.weatherData = weatherData
weatherData.registerObserver(this)
}

override fun update(temp: Float, humidity: Float, pressure: Float) {
this.humidity = humidity
this.temperature = temp
display()
}
override fun display() {
println("StatisticsDisplay ${temperature} F degress and humidity ${humidity} %")
}
}..............


구현한것을 이용하기 위해 Display를 구현합니다.


이렇게 구현을 하고나서


fun main() {
val weatherData: WeatherData = WeatherData()
val currentDisplay: CurrentConditionDisplay = CurrentConditionDisplay(weatherData)
val statisticsDisplay: StatisticsDisplay = StatisticsDisplay(weatherData)
val forecastDisplay: ForecastDisplay = ForecastDisplay(weatherData)

weatherData.setMeasurements(80f, 65f, 30.4f) // 데이터가 바뀌는걸 주입하기 위한 함수
weatherData.setMeasurements(82f, 70f, 38.2f) // 데이터가 바뀌는걸 주입하기 위한 함수
weatherData.setMeasurements(88f, 90f, 29.2f) // 데이터가 바뀌는걸 주입하기 위한 함수
}


실제 구현을 하게되면 정상적으로 동작하는것을 알 수 있습니다.


한가지 알아야할점.

옵저버 패턴에서는 두가지 방식이 존재합니다. 

1. 데이터를 발행하는 주체가 데이터를 보내는방식  (푸시 (Push)방식)

2. 데이터를 구독하는쪽에서 데이터를 가져가는 방식  (풀 (Pull)방식


자바와 자바를 이용한 코틀린에서는 java.util.Observable , java.util.Observer를 통해 풀방식과 푸시방식 두가지를 가능하도록 합니다.


class WeatherDataWithObservable() : Observable() {
var temperature: Float = 0.0f
var humidity: Float = 0.0f
var pressure: Float = 0.0f

//가상 스테이션으로부터 받은 데이터를 옵저버에게 알림
fun measurementsChanged() {
setChanged()
notifyObservers()
}

//강제적인 데이터변경을 테스트하기 위한 메솓,
fun setMeasurements(temperature: Float, humidity: Float, pressure: Float) {
this.temperature = temperature
this.humidity = humidity
this.pressure = pressure
measurementsChanged()
}
//코틀린에서는 자동으로 getMethod 생성
}

class DisplayWithObservable : java.util.Observer, DisplayElement {
lateinit var observable: Observable
var temperature: Float = 0.0f
var humidity: Float = 0.0f

override fun update(obs: Observable?, p1: Any?) {
(obs as WeatherData).apply {
this@DisplayWithObservable.temperature = this.temperature
this@DisplayWithObservable.humidity = this.humidity
display()
}
}
override fun display() {
//println
}
}


이런형태로 구현이 가능합니다.


Java의 Observable의 치명적인 단점

사실 Java의 Observable을 이용하게 되면 치명적인 단점이 존재합니다. 

- Observable은 인터페이스가 아니고 클래스이기때문에 다른 클래스를 확장하고 있는 클래스에는 사용할 수 없습니다. 그래서 재사용성의 제약이 있습니다.

따라서 상황에 따라 맞춰써야합니다.



오늘은 옵저버 패턴에 대해서 알아봤습니다.

오늘은 맛집의 총평처럼 핵심요약을 해보려고합니다.


핵심요약

1. 옵저버 패턴을 통해 객체의 일대다의 의존성을 정의

2. 디자인패턴을 통해 느슨한 결합을 이용해야 한다는점

3. 옵저버 패턴에는 푸시방식(발행주체가 알려주는) 과 풀방식(구독주체가 데이터를 받는) 두가지 방식이 존재한다.

4. Java Util 클래스에 있는 Observable은 클래스이기 때문에 재사용성의 제약이 있으므로 상황에 맞춰서 옵저버를 따로 구현해야 할 수도 있다.