Menu
Coddy logo textTech

옵저버 패턴

Coddy Swift 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 57개 중 44번째.

observer 패턴에서는 객체(subject)가 옵저버 목록을 유지하고, 어떤 일이 발생하면 모든 옵저버에게 알립니다. subject는 옵저버가 무엇을 하는지 알 필요가 없습니다.

final class Button {
    private var handlers: [(String) -> Void] = []

    func onTap(_ handler: @escaping (String) -> Void) {
        handlers.append(handler)
    }

    func tap() {
        for h in handlers { h("tapped") }
    }
}

let b = Button()
b.onTap { print("logger saw \($0)") }
b.onTap { print("counter saw \($0)") }
b.tap()

출력:

logger saw tapped
counter saw tapped

옵저버는 프로토콜을 준수하는 객체일 수도 있습니다. 주체는 등록된 각 옵저버에서 프로토콜 메서드를 호출합니다:

protocol StockObserver: AnyObject {
    func priceChanged(_ symbol: String, to price: Int)
}

final class Stock {
    let symbol: String
    private var observers: [StockObserver] = []
    var price: Int { didSet { observers.forEach { $0.priceChanged(symbol, to: price) } } }
    init(symbol: String, price: Int) { self.symbol = symbol; self.price = price }
    func add(_ o: StockObserver) { observers.append(o) }
}

final class Alert: StockObserver {
    func priceChanged(_ s: String, to p: Int) { if p > 100 { print("\(s) above 100: \(p)") } }
}

let apple = Stock(symbol: "AAPL", price: 90)
apple.add(Alert())
apple.price = 95
apple.price = 120

출력:

AAPL above 100: 120

옵저버는 보통 수신을 중지할 방법이 필요합니다. 등록 시 id(또는 토큰)를 반환하면 호출자가 나중에 자신의 옵저버를 제거할 수 있습니다:

final class EventBus {
    private var listeners: [Int: (String) -> Void] = [:]
    private var nextId = 0

    func subscribe(_ f: @escaping (String) -> Void) -> Int {
        nextId += 1
        listeners[nextId] = f
        return nextId
    }
    func unsubscribe(_ id: Int) { listeners[id] = nil }
    func publish(_ event: String) {
        for id in listeners.keys.sorted() { listeners[id]!(event) }
    }
}

let bus = EventBus()
let a = bus.subscribe { print("A got \($0)") }
_ = bus.subscribe { print("B got \($0)") }
bus.publish("login")
bus.unsubscribe(a)
bus.publish("logout")

출력:

A got login
B got login
B got logout

@escaping은 클로저가 저장되어 이를 전달받은 함수가 반환된 후 나중에 호출됨을 나타냅니다. 옵저버 패턴은 주체와 옵저버를 느슨하게 연결합니다. 따라서 주체를 변경하지 않고도 새로운 옵저버를 추가할 수 있습니다.

func onTap(_ handler: @escaping (String) -> Void) {
    handlers.append(handler)   // 저장됨: @escaping이어야 함
}
challenge icon

챌린지

쉬움

Newsletter 클래스를 완성하세요. 이 클래스는 구독자에게 호를 보내는 주체입니다. subscribe(name:handler:)는 이름 아래에 클로저를 저장하고(같은 이름으로 다시 구독하면 기존 클로저를 대체합니다), unsubscribe(name:)는 해당 클로저를 제거하며, publish(_ title:)는 이름의 알파벳순으로 모든 핸들러를 호출하고 알림을 받은 구독자 수를 반환합니다.

제공된 코드는 sub <name>, unsub <name> 또는 pub <title> 명령을 읽습니다. 각 구독자의 핸들러는 <name> reads <title>를 출력하며, 각 pub 이후 코드가 sent to 2를 출력합니다.

코드는 Newsletter.swift에 작성하세요. main.swift에는 제공된 입력/출력 코드가 들어 있으며 편집할 수 없습니다.

직접 해보기

// 제공된 입력/출력 코드: 그대로 유지하세요
var input: [String] = []
while let line = readLine() { input.append(line) }
let news = Newsletter()
for cmd in input {
    let p = cmd.split(separator: " ", maxSplits: 1).map(String.init)
    switch p[0] {
    case "sub":
        let name = p[1]
        news.subscribe(name: name) { print("\(name) reads \($0)") }
    case "unsub": news.unsubscribe(name: p[1])
    default: print("sent to \(news.publish(p[1]))")
    }
}
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨

직접 연습해 보세요: Swift 플레이그라운드