옵저버 패턴
Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 89번째.
옵저버 패턴(Observer pattern)은 객체 간의 일대다(one-to-many) 관계를 정의합니다. 한 객체(주체, subject)의 상태가 변경되면, 그 객체에 의존하는 모든 객체들(옵저버, observers)이 자동으로 알림을 받습니다. 이는 이벤트 시스템, 알림 기능 또는 여러 컴포넌트가 변경 사항에 반응해야 하는 모든 시나리오에 이상적입니다.
Go에서 우리는 옵저버를 위한 인터페이스와 구독을 관리하는 주체(subject) 구조체를 사용하여 이 패턴을 구현합니다:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}Observer 인터페이스를 구현하는 모든 타입은 알림을 받기 위해 구독할 수 있습니다:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}Subject는 구체적인 타입을 알지 못해도 모든 관찰자(observer)에게 알림을 보냅니다:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// user@example.com으로 이메일 전송: Order placed
// 로그: Order placed이 패턴은 주체(subject)와 관찰자(observer)를 분리하여, 기존 코드를 수정하지 않고도 새로운 관찰자 유형을 쉽게 추가할 수 있게 합니다. 이는 이벤트 기반 아키텍처, GUI 프레임워크, 그리고 발행/구독(pub/sub) 메시징 시스템에서 흔히 사용됩니다.
챌린지
쉬움옵저버(Observer) 패턴을 사용하여 주가 알림 시스템을 구축해 봅시다! 주가가 변경될 때마다 여러 알림 서비스가 자동으로 알림을 받는 시스템을 만들게 됩니다. 이는 주체(Subject)가 관찰자(Observer)의 구체적인 타입을 알지 못해도 이벤트에 어떻게 반응할 수 있는지 보여주는 완벽한 예시입니다.
코드는 세 개의 파일로 구성됩니다:
observer.go: 옵저버 인터페이스와 구체적인 옵저버 타입을 정의합니다.가격이 변경될 때 주식 정보를 받는
Update(stockName string, price float64)메서드를 가진Observer인터페이스를 생성하세요.두 가지 옵저버 타입을 구현하세요:
PriceAlert:Threshold필드(float64)를 가집니다. 업데이트 시 가격이 임계값(threshold)보다 높으면ALERT: [stockName] at $[price] crossed threshold $[threshold]를 반환하고, 낮으면WATCHING: [stockName] at $[price] (threshold: $[threshold])를 반환합니다.PriceLogger:LogName필드(string)를 가집니다. 업데이트 시[logName] recorded [stockName]: $[price]를 반환합니다.
두 옵저버 모두 형식화된 응답 문자열을 반환하는 메서드를 가져야 합니다.
stock.go: 옵저버를 관리하고 가격 변경을 알리는 주체(Subject)를 생성합니다.Name(string),Price(float64), 그리고 옵저버 슬라이스를 필드로 가지는Stock구조체를 만드세요. 다음 메서드들을 추가하세요:Subscribe(o Observer): 옵저버를 추가합니다.SetPrice(price float64) []string: 가격을 업데이트하고 모든 옵저버에게 알린 뒤, 그들의 모든 응답을 슬라이스로 반환합니다.
또한
NewStock(name string, initialPrice float64) *Stock생성자를 만드세요.main.go: 옵저버와 함께 주식을 설정하고 가격 변화를 시뮬레이션합니다.주식 이름과 초기 가격을 읽습니다. 그런 다음 생성할 옵저버의 수를 읽습니다. 각 옵저버에 대해 타입(
alert또는logger)과 설정값(알림의 경우 임계값, 로거의 경우 로그 이름)을 읽습니다. 각 옵저버를 주식에 구독(Subscribe)시킵니다.마지막으로 새 가격을 읽고, 주식을 업데이트한 후 각 옵저버의 응답을 한 줄씩 출력합니다.
다음과 같은 입력이 제공됩니다:
- 1행: 주식 이름
- 2행: 초기 가격
- 3행: 옵저버 수
- 각 옵저버당: 타입(
alert또는logger), 그 다음 설정값 - 마지막 행: 새로 설정할 가격
모든 가격은 소수점 둘째 자리까지 표시하세요.
예를 들어, 다음과 같이 입력되면:
GOOG
150.00
3
alert
155.00
logger
TradeLog
alert
140.00
160.50출력은 다음과 같아야 합니다:
ALERT: GOOG at $160.50 crossed threshold $155.00
TradeLog recorded GOOG: $160.50
ALERT: GOOG at $160.50 crossed threshold $140.00그리고 다음과 같이 입력되면:
AAPL
180.00
2
logger
DailyLog
alert
200.00
175.25출력은 다음과 같아야 합니다:
DailyLog recorded AAPL: $175.25
WATCHING: AAPL at $175.25 (threshold: $200.00)Stock이 알림(alert)을 알리는지 로거(logger)를 알리는지 알지 못하고, 단지 각 옵저버의 Update를 호출할 뿐이라는 점에 주목하세요. 이러한 결합 해제(decoupling) 덕분에 Stock 코드를 수정하지 않고도 새로운 옵저버 타입을 쉽게 추가할 수 있습니다!
치트 시트
옵저버 패턴(Observer pattern)은 주체(subject)의 상태가 변경될 때 여러 옵저버(observer)에게 자동으로 알림을 보내는 일대다 관계를 정의합니다. 이는 주체와 옵저버 간의 결합도를 낮추어 이벤트 시스템 및 알림 서비스에 이상적입니다.
기본 구조
옵저버 인터페이스와 구독을 관리하는 주체를 정의합니다:
type Observer interface {
Update(event string)
}
type Subject struct {
observers []Observer
}
func (s *Subject) Subscribe(o Observer) {
s.observers = append(s.observers, o)
}
func (s *Subject) Notify(event string) {
for _, observer := range s.observers {
observer.Update(event)
}
}옵저버 구현하기
Observer 인터페이스를 구현하는 모든 타입은 구독할 수 있습니다:
type EmailAlert struct{ Address string }
func (e EmailAlert) Update(event string) {
fmt.Printf("Email to %s: %s\n", e.Address, event)
}
type Logger struct{}
func (l Logger) Update(event string) {
fmt.Println("Log:", event)
}사용법
옵저버를 구독하고 변경 사항을 알립니다:
subject := &Subject{}
subject.Subscribe(EmailAlert{Address: "user@example.com"})
subject.Subscribe(Logger{})
subject.Notify("Order placed")
// Email to user@example.com: Order placed
// Log: Order placed주체는 구체적인 타입을 알지 못해도 모든 옵저버에게 알림을 보낼 수 있으므로, 기존 코드를 수정하지 않고도 새로운 옵저버 타입을 쉽게 추가할 수 있습니다.
직접 해보기
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 주식 이름 읽기
stockName, _ := reader.ReadString('\n')
stockName = strings.TrimSpace(stockName)
// 초기 가격 읽기
initialPriceStr, _ := reader.ReadString('\n')
initialPrice, _ := strconv.ParseFloat(strings.TrimSpace(initialPriceStr), 64)
// 관찰자(observer) 수 읽기
numObserversStr, _ := reader.ReadString('\n')
numObservers, _ := strconv.Atoi(strings.TrimSpace(numObserversStr))
// TODO: NewStock 생성자를 사용하여 새로운 Stock 생성
// TODO: 각 관찰자(observer)를 순회
for i := 0; i < numObservers; i++ {
// 관찰자 타입 읽기 ("alert" 또는 "logger")
observerType, _ := reader.ReadString('\n')
observerType = strings.TrimSpace(observerType)
// 설정값 읽기
configValue, _ := reader.ReadString('\n')
configValue = strings.TrimSpace(configValue)
// TODO: 타입에 따라 적절한 관찰자 생성
// - "alert"의 경우: configValue를 float64 임계값으로 파싱하여 PriceAlert 생성
// - "logger"의 경우: configValue를 LogName으로 사용하여 PriceLogger 생성
// TODO: 관찰자를 주식에 구독(subscribe)시키기
}
// 새로운 가격 읽기
newPriceStr, _ := reader.ReadString('\n')
newPrice, _ := strconv.ParseFloat(strings.TrimSpace(newPriceStr), 64)
// TODO: 새로운 가격을 설정하고 응답 받기
// TODO: 각 응답을 개별 줄에 출력
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
8에러 처리와 OOP
error 인터페이스사용자 정의 에러 타입에러 래핑 (fmt.Errorf)센티넬 에러errors.Is()와 errors.As()Panic, Defer, Recover요약 - 파일 파서