지난 포스팅에서 행동패턴 은 "객체의 통신" 에 보다 밀접한 관련이 있다라고 생각했다.
이 행동패턴이 가지고 있는 컨셉을 가지고 옵저버 패턴을 확인해 보자.
옵저버 패턴 이란?
객체의 상태 변화를 관찰하는 관찰자들, 즉 옵저버들의 목록을 객체에 등록하여, 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 목록의 각 옵저버에게 통지하도록 하는 디자인패턴 이다. "발행 구독 모델로 알려져 있기도 하다." - 위키-
여러 객체에 자신이 관찰 중인 객체에 발생하는 모든 이벤트에 대하여 알리는 구독 메커니즘을 정의할 수 있도록 하는 행동 디자인 패턴이다. - 구루-
위 두 사이트에서 공통적으로 말하고자 하는 바가 있다. "구독", "상태변화, 이벤트 알림" 이 가장 중복되는 의미로 작성되어 있다.
옵저버 패턴을 사용하면 하나의 관리자가 있고 그 관리자는 여러 개의 옵저버 들을 관리할 수 있는 경우를 말하는구나라고 이해하고 넘어갈 수 있다.
왜 사용하는가?
- 관리자와 관리자에게 관리당하는 객체 들 간의 관계성에서 시작된다. 관리자는 interface를 관리하게 된다. 이 말은 즉 관리자는 관리당하는 객체가 누구인지 모르지만 "interface에서 정의한 행동"은 한다라고 만 알고 있는 상태가 되어버린다.
이는 다시 말해 느슨한 결합이 되고, 느슨한 결합은 코드의 확장에 있어 엄청난 이점을 가져다준다.
- 주제는 옵저버가 특정 인터페이스를 구현한다는 사실만 압니다.
- 옵저버는 언제든지 새로 추가할 수 있습니다.
- 새로운 형식의 옵저버를 추가할 때도 주제를 변경할 필요가 전혀 없습니다.
- 주제와 옵저버는 서로 독립적으로 재사용할 수 있습니다.
- 주제나 옵저버가 달라져도 서로에게 영향을 미치지 않습니다.(인터페이스 수정이 아닌 다른 부분)
느슨하게 결합된다면 변경사항 이 생겨도 유연한 객체지향 시스템을 구축할 수 있다.
구조
두 개의 구조가 상당히 유사하다. 당연히 비슷해야 한다. 옵저버 패턴의 구조니 깐
Subject는 Publisher와 , Observer는 Subscriber와 각각 대응된다.
위에서 말한 관리자 즉 subject, publisher는 옵저버 들을 특정 자료구조에 저장하고 저장된 자료구조를 loop를 돌면서 구현된 인터페이스의 함수를 이용해 객체 간의 통신이 진행된다.
위의 구조가 상당히 익숙한데 채팅을 메시지-큐 없이 사용할 때 유저의 관리와 소켓의 데이터 발행과 수신을 위해 저렇게 loop를 돌면서 방접속자 혹은 채팅 접속자에게 메시지를 보내는 경우가 해당된다.
이건 전략패턴의 기본 구조중 하나이다. 구조 자체는 상당히 비슷하다.
인터페이스의 추상화를 기본 골대로 가져 실제 구현된 사항들을 캡슐화 하여 클라이언트에게 제공한다. 그러나 동작하는 목적의 자체가 다르다.
전략패턴
알고리즘을 캡슐화하고, 이를 동적으로 교환할 수 있는 구조를 제공하며, 클라이언트는 전략인터페이스를 통해 알고리즘을 호출한다.
클라이언트 즉 전략패턴의 소유권자는 어떤 전략 객체를 사용할지 선택하는 주체이다.
(전략패턴 또한 전략 객체와 컨택스트는 느슨하게 결합되어 있다. 컨택스트 는 특정 전략 객체를 호출하는 것이 아닌 인터페이스를 통해 호출한다.)
옵저버패턴
클라이언트 즉 옵저버들의 관리자 와 옵저버 간의 상호작용을 나타내는 패턴이다. 옵저버 들의 관리자(소유권자)는 소유권자의 상태 변화에 따라 옵저버 들에게 이벤트를 알려주고, 이러한 이벤트에 따라 각각의 알고리즘 혹은 작업을 수행한다.
소유권자, 관리자는 옵저버 들이 누구인지 알지 못하며, 이는 느슨한 결합을 의미한다.
(옵저버 패턴에서 유달리 느슨한 결합이 강조되는 이유는 "주체" 와 "옵저버들" 간의 유연성과 확장성을 강조하기 때문)
각각의 패턴이 지향하고자 하는 바는 명확하게 다르다.
옵저버 패턴 사용시기
- 한 객체의 상태가 변화에 여러 객체가 반응해야 하는 경우
- 상호작용(위의 케이스) 하는 객체들이 느슨한 결합이 필요할 때(언제든지 확장의 가능성이 있을 때)
- 이벤트 기반 시스템에서 주체 객체와 구독 객체로 시스템 구성을 해야 할 때
구현해 보기
WeatherDate의 관리자는 현재날씨 화면, 통계, 예보 화면 등에 대해 관리하고 있으며 notify를 통해 변경된 날씨에 따라 각각의 화면이 반응하게 된다.
당연히 새로운 옵저버가 추가되던, 화면 노출이 아닌 다른 로직이 생기더라도 기존 코드의 수정없이 업데이트 가능하다. WeatherDate 또한 동일하다.
package observer
import "fmt"
type Subject interface {
registerObserver(o Observer2)
removeObserver(o Observer2)
notifyObserver()
}
type Observer2 interface {
update()
}
type DisplayElement interface {
display()
}
type WeatherData struct {
observers []Observer2
temp, humidity, pressure float32
}
func (w *WeatherData) registerObserver(o Observer2) {
if w.observers == nil {
w.observers = make([]Observer2, 0)
}
w.observers = append(w.observers, o)
}
func (w *WeatherData) removeObserver(o Observer2) {
for i, v := range w.observers {
if v == o {
if i != len(w.observers)-1 {
// 일반 슬라이스
tmp := w.observers[:i]
w.observers = append(tmp, w.observers[i+1:]...)
} else {
w.observers = w.observers[:len(w.observers)-1]
}
}
}
}
func (w *WeatherData) notifyObserver() {
for _, v := range w.observers {
v.update()
}
}
func (w *WeatherData) getTemperature() {
}
func (w *WeatherData) getHumidity() {
}
func (w *WeatherData) getPressure() {
}
func (w *WeatherData) measurementChanged() {
w.notifyObserver()
}
func (w *WeatherData) setMeasurement(tmp, humidity, pressure float32) {
w.temp = tmp
w.humidity = humidity
w.pressure = pressure
w.measurementChanged()
}
var _ Subject = (*WeatherData)(nil)
type CurrentConditionDisplay struct {
temperature, humidity float32
weatherData *WeatherData
}
func (c *CurrentConditionDisplay) update() {
c.temperature = c.weatherData.temp
c.humidity = c.weatherData.humidity
c.display()
}
func (c *CurrentConditionDisplay) display() {
fmt.Printf("현재 상태 온도 : %f, 습도 : %f\n", c.temperature, c.humidity)
}
func NewCurrentConditionDisplay(w *WeatherData) *CurrentConditionDisplay {
ob := &CurrentConditionDisplay{weatherData: w}
w.registerObserver(ob)
return ob
}
type StatisticDisplay struct {
average, highest, lowest float32
weatherData *WeatherData
}
func (s *StatisticDisplay) update() {
temp := s.weatherData.temp
s.average = (s.average + temp) / 2
if s.highest < temp {
s.highest = temp
}
if s.lowest > temp {
s.lowest = temp
}
s.display()
}
func (s *StatisticDisplay) display() {
fmt.Printf("평균/최고/최저 온도 : %f / %f / %f \n", s.average, s.highest, s.lowest)
}
func NewStatisticDisplay(w *WeatherData) *StatisticDisplay {
ob := &StatisticDisplay{weatherData: w}
w.registerObserver(ob)
return ob
}
type ForecastDisplay struct {
prevTemp, prevHumidity, prevPressure float32
announcement string
weatherData *WeatherData
}
func (f *ForecastDisplay) update() {
temp := f.weatherData.temp
humidity := f.weatherData.humidity
pressure := f.weatherData.pressure
if temp > f.prevTemp && humidity > f.prevHumidity && pressure > f.prevPressure {
f.announcement = "날씨가 무척 더워질 예정입니다. 조심하세요"
} else if temp < f.prevTemp && humidity < f.prevHumidity && pressure < f.prevPressure {
f.announcement = "날씨가 무척 추워질 예정입니다. 조심하세요"
} else {
f.announcement = "어제와 유사한 날씨 입니다."
}
f.prevTemp = temp
f.prevHumidity = humidity
f.prevPressure = pressure
f.display()
}
func (f *ForecastDisplay) display() {
fmt.Println(f.announcement)
}
func NewForecastDisplay(w *WeatherData) *ForecastDisplay {
ob := &ForecastDisplay{weatherData: w}
w.registerObserver(ob)
return ob
}
func Test_02(t *testing.T) {
w := &WeatherData{}
cur := NewCurrentConditionDisplay(w)
stat := NewStatisticDisplay(w)
fore := NewForecastDisplay(w)
fmt.Println(cur, stat, fore)
w.setMeasurement(80, 22.2, 32.7)
w.setMeasurement(60, 20.2, 30.7)
w.setMeasurement(90, 22.2, 32.9)
}
=== RUN Test_02
&{0 0 0x1400006c540} &{0 0 0 0x1400006c540} &{0 0 0 0x1400006c540}
현재 상태 온도 : 80.000000, 습도 : 22.200001
평균/최고/최저 온도 : 40.000000 / 80.000000 / 0.000000
날씨가 무척 더워질 예정입니다. 조심하세요
현재 상태 온도 : 60.000000, 습도 : 20.200001
평균/최고/최저 온도 : 50.000000 / 80.000000 / 0.000000
날씨가 무척 추워질 예정입니다. 조심하세요
현재 상태 온도 : 90.000000, 습도 : 22.200001
평균/최고/최저 온도 : 70.000000 / 90.000000 / 0.000000
날씨가 무척 더워질 예정입니다. 조심하세요
테스트의 결과 위와 같이 나오고 있으며 매번 변경되는 값이 생길 때마다 관리되고 있는 옵저버 들은 변경된 값에 대해 응답을 하게 된다.
여기서 궁금증이 생긴다.
옵저버 패턴을 적용하여, 주체객체에 생각보다 많은 옵저버들이 등록되어 관리되고 있다면 어떻게 처리할까?
고라는 언어특성상 비동기처리 하기 매우 쉽다. 고 루틴과 채널링을 이용해 처리해버리면 된다.
옵저버들은 각각의 고루틴을 가지고 채널을 열어서 대기하며 주체 객체 에서 발생된 이벤트에 대해 각 옵저버 별로 채널에 값을 넣어주면 된다. 마치 채팅의 메시지 수신 후 채팅방 구성원들에게 메시지를 뿌려주듯이
최근 동시성 프로그래밍과 채팅 관련해서 공부하다 보니 생각보다 옵저버 패턴과 유사하게 구현된 부분이 많아 생각보다 반가운 패턴이었다. 구조가 같다는 게 아니라 동작하는 방식이 유사하다는 뜻이다.
'Go > Design Pattern' 카테고리의 다른 글
[Design Pattern] 행동패턴-커맨드 패턴 (Command Pattern) (0) | 2023.07.14 |
---|---|
[Design Pattern] 구조패턴-데코레이터 패턴 (Decorator Pattern) (0) | 2023.07.10 |
[Design Pattern] 행동패턴-전략패턴 (Strategy Pattern) (0) | 2023.07.02 |
[Design Pattern] 생성패턴 (Singleton Pattern) (0) | 2023.05.10 |
[Design Pattern] 생성패턴 (Prototype Pattern) (0) | 2023.05.08 |