Computer Science/디자인패턴

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

Jinhwan 2024. 2. 25. 22:06

정의

한 객체가 상태가 바뀌면 그 객체에 의존하는 다른 객체에 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의하는 패턴
주체는 상태가 바뀌는 객체이고 옵저버는 이러한 주체의 상태를 관찰하는 객체이다. 주체가 상태가 갱신이 되었을 때 옵저버가 수동적으로 주체한테 정보를 받게 되면 푸시방식, 옵저버가 필요할 때 주체의 상태를 가져오게 되면 풀 방식으로 불린다.

장점

  •  느슨한 결합 : 두 객체가 상호작용을 할 때 서로에 대해서 강하게 결합이 되지 않게 되면 객체의 변화가 다른 객체로 전파되지 않는다. 옵저버 패턴을 사용하면 주제 객체와 옵저버 객체들은 상호작용을 하긴 하지만 서로 강하게 결합되지 않아 
  • 동적인 구독 관리 : 옵저버는 구독하고 있는 객체의 목록을 관리하기 때문에 옵저버가 추가되거나 탈퇴하는 경우 객체 목록만 갱신하면 구독 목록을 관리할 수 있게 된다.
  • 이벤트 기반 프로그래밍 : 어플리케이션에서 발생하는 다양한 이벤트에 대응하는 코드를 작성하기 용이해진다.

단점

  • 알림 순서 보장 안됨 : 주체는 옵저버 목록을 순회하면서 알림을 보내게 된다. 따라서 특정 옵저버가 우선순위를 가지게 하려면 추가적인 로직이 필요하게 되고 이러한 경우 주체와 옵저버 사이에 결합이 높아지게 된다.
  • 성능 문제 : 옵저버 목록을 순회하는 부분에서 O(n)의 시간복잡도를 가지기 때문에 옵저버의 수가 늘어나게 됨에 따라 오버헤드가 발생하게 된다.

다이어그램

구현

 다음은 Observer패턴을 위해 특정 주식 종목에 대해서 가격이 변화면 투자자들에게 알람이 가는 코드를 작성해보았다.
주식 종목이 바뀌게 된다면 투자자들은 즉시 이를 알고 싶어할 것이기 때문에 푸시방식을 이용해서 코드를 작성하였다.

1. Subject, Observer 인터페이스

 go언어는 객체지향 언어가 아니기 때문에 클래스가 없지만 구조체와 인터페이스가 존재한다. 이를 이용해서 Interface를 다음과 같이 정의해보자.

package observer

// Observer interface
type IObserver interface {
	Update(stockSymbol string, price float64)
}

Observer는 주식 가격에 변동사항이 있으면 해당 종목의 이름과 가격을 주체로 부터 전달 받게 된다.

package subject

import (
	"github.com/RicardoKim/Study/DesignPattern/Observer/observer"
)

type ISubject interface {
	RegisterObserver(o observer.IObserver)
	RemoveObserver(o observer.IObserver)
	NotifyObservers()
}

 

Subject는 옵저버 등록, 탈퇴, 푸시 알림 등의 메소드가 존재하는 것을 알 수 있다.

2. Observer구현체 investor

package observer

import "fmt"

// ConcreteObserver
type Investor struct {
	Name string
}

func (i *Investor) Update(stockSymbol string, price float64) {
	fmt.Printf("Investor %s notified: Stock %s is now $%.2f\n", i.Name, stockSymbol, price)
}

Investor들은 주식가격을 전달받으면 이를 출력한다.

3. Subject 구현체 StockerSticker

package subject

import (
	"github.com/RicardoKim/Study/DesignPattern/Observer/observer"
)

type StockTicker struct {
	//Observer들은 동적으로 변화하기 때문에 slice객체로 선언한다.
	observers   []observer.IObserver
	stockSymbol string
	//price또한
	price float64
}

func (s *StockTicker) RegisterObserver(o observer.IObserver) {
	//slice의 append는 append(붙일 배열, 추가할 값들)로 선언하게 된다.
	s.observers = append(s.observers, o)
}

func (s *StockTicker) RemoveObserver(o observer.IObserver) {
	for i, observer := range s.observers {
		if observer == o {
			//i-1번째 뒤에 i + 1 배열을 붙임으로써 삭제를 진행한다.
			s.observers = append(s.observers[:i], s.observers[i+1:]...)
			break
		}
	}
}

func (s *StockTicker) NotifyObservers() {
	//순회하면서 stocke의 이름과 가격을 전달한다.
	for _, observer := range s.observers {
		observer.Update(s.stockSymbol, s.price)
	}
}

//stockeSymbol은 소문자로 시작하기 때문에 같은 패키지에서만 접근이 가능하다.
//따라서 setter를 만들어 수정이 되게끔 만든다.
func (s *StockTicker) SetStockSymbol(stockSymbol string) {
	s.stockSymbol = stockSymbol
}

func (s *StockTicker) SetPrice(price float64) {
	//push방식으로 동작하는 것을 알 수 있다.
	s.price = price
	s.NotifyObservers()
}

4. main.go

package main

import (
	"github.com/RicardoKim/Study/DesignPattern/Observer/observer"
	"github.com/RicardoKim/Study/DesignPattern/Observer/subject"
)

func main() {
	ticker := &subject.StockTicker{}
	ticker.SetStockSymbol("Samsung")

	investor1 := &observer.Investor{Name: "John"}
	investor2 := &observer.Investor{Name: "Doe"}

	ticker.RegisterObserver(investor1)
	ticker.RegisterObserver(investor2)

	ticker.SetPrice(1500.0)
	ticker.SetPrice(1550.0)
}

실행 결과

❯ go run main.go
Investor John notified: Stock Samsung is now $1500.00
Investor Doe notified: Stock Samsung is now $1500.00
Investor John notified: Stock Samsung is now $1550.00
Investor Doe notified: Stock Samsung is now $1550.00

 


긴 글 읽어주셔서 감사합니다.

 

틀린 부분이 있으면 댓글을 달아주시면 감사하겠습니다.

 

📧 : realhwan1202@gmail.com

🔗 : https://github.com/RicardoKim