꿈돌이랜드

[Design Pattern] 옵저버 패턴 (Observer Pattern) 본문

Programming/디자인패턴

[Design Pattern] 옵저버 패턴 (Observer Pattern)

loinsir 2023. 9. 16. 03:42
반응형
🔬

Refactoring Guru 사이트의 글을 번역한 것입니다.

옵저버 패턴

의도

  • 옵저버 패턴은 관찰하는 객체에 발생하는 모든 이벤트에 대해 여러 객체에 알리는 구독 메커니즘을 정의할 수 있는 행동 디자인 패턴이다.

예시

CustomerStore 라는 두 가지 유형의 객체가 있다고 상상해보자. customer는 곧 store에 출시될 특정 브랜드의 상품(예를 들어, iPhone의 새 모델)에 매우 흥미있다.

고객은 매일 매장을 방문하여 제품 재고를 확인할 수 있다. 그러나 제품이 아직 운송 중일 때는 이러한 확인은 무의미하다.

반면 상점은 새로운 제품이 출시될 때마다 모든 고객에게 스팸으로 간주될 수 있는 수많은 이메일을 보낼 수 있다. 이것은 상점에 계속해서 방문을 하는 방문객들을 줄일 수 있다. 하지만 동시에, 새로운 제품에 관해 관심이 없는 다른 고객들에게는 피해가 될 수 있다.

어떻게 해야할까? 고객이 일일이 제품의 가용성을 확인하는데 시간을 낭비하도록 하거나, 상점이 엉뚱한 고객들에게 이메일을 발송하는 소요를 발생하거나 한다.

해결방법

어떤 흥미로운 상태를 가진 객체를 보통 subject 라고 하지만 자신의 상태 변경에 대해서 다른 객체에도 알리기 시작한 시점 부터 publisher 라고 부를것이다. publisher 의 상태에 대한 변경 사항을 추적하려는 모든 다른 객체를 observer 라고 한다.

옵저버 패턴은 publisher(게시자) 클래스에 구독 메커니즘을 추가해서 개별 객체가 해당 게시자에서 오는 이벤트 스트림을 구독하거나 구독을 취소할 수 있도록 제안한다. 실제로, 이 메커니즘은 1) 구독자 객체에 대한 참조 목록을 저장하기 위한 배열 필드와 2) 해당 목록에 구독자를 추가하거나 제거할 수 있는 여러 공용 메서드로 구성된다.

구독 메커니즘을 통해 개별 객체가 이벤트 알림을 구독할 수 있다.

이제 Publisher에게 중요한 이벤트가 발생할 때마다 subscribers를 통해 객체에 대한 특정 알림 메서드를 호출한다.

실제 앱에는 동일한 Publisher 클래스의 이벤트를 추적하는 데 관심이 있는 수십 개의 서로 다른 Subscriber 클래스가 있을 수 있다. Publisher 를 이러한 모든 클래스에 연결하고 싶지는 않을 것이다. 게다가, Publisher 클래스가 다른 사람들에 의해 사용되어야 한다면 그들 전부에 대해서는 미리 알지 못할 수도 있다.

그것이 바로 모든 subscribers 들이 같은 인터페이스를 구현하고 Publisher가 해당 인터페이스를 통해서만 subscriber와 통신하는 것이 중요하다. 이 인터페이스는 Publisher 가 알림과 함께 일부 컨텍스트 데이터를 전달하는 데 사용할 수 있는 매개변수 집합과 함께 알림(노티피케이션) 메서드를 선언해야 한다.

Publisher는 그들의 객체의 구체적인 노티피케이션 메서드를 호출함으로써 구독자들에게 알린다.

앱에 여러 유형의 Publisher 가 있고 Observer 가 모든 Publisher 와 호환되도록 하려면, 모든 Publisher 가 동일한 인터페이스를 따르도록 할 수 있다. 이 인터페이스는 아마 몇가지 구독 메서드만을 설명하는 것이 필요할 것이다. 이 인터페이스를 통해 Observer는 구체적인 클래스에 연결하지 않고도 Publisher 의 상태를 관찰할 수 있을 것이다.

실생활에서

신문이나 잡지를 구독하면 더 이상 다음 호가 있는지 확인하기 위해 상점에 갈 필요가 없다. 대신 발행인은 발행 직후 또는 사전에 새로운 우편물을 구독자의 우편함으로 직접 보낸다.

발행인은 구독자 목록을 유지 관리하고 그들이 관심 있어하는 잡지를 알고 있다. 구독자는 발행인이 새 잡지 호를 보내는 것을 그만 받고 싶을 때, 언제든지 구독자 목록에서 나갈 수 있다.

구현방법

  1. 게시자(Publisher)는 다른 객체에 대한 관심 이벤트를 발행한다. 이러한 이벤트는 게시자가 자신의 상태를 변경하거나 일부 동작을 실행할 때 발생된다. 게시자에는 새 구독자가 가입하고 현재 구독자가 목록에서 나갈 수 있도록 하는 인프라가 포함되어 있다.
  1. 새 이벤트가 발생하면 게시자는 구독자 목록을 살펴보고 각 구독자 객체에의 구독자 인터페이스에 선언된 알림 메서드를 호출한다.
  1. 구독자(Subscriber) 인터페이스는 알림 인터페이스를 선언한다. 대부분의 경우 단일 update 메서드로 구성된다. 메서드에는 게시자가 업데이트와 함께 일부 이벤트 세부 정보를 전달할 수 있도록 하는 여러 매개 변수가 있을 수 있다.
  1. 구체 구독자는 게시자가 발행한 알림에 대한 응답으로 몇 가지 작업을 수행한다. 게시자가 구체적인 클래스에 연결되지 않도록 이러한 모든 클래스는 동일한 인터페이스를 구현해야 한다.
  1. 일반적으로 구독자는 업데이트를 올바르게 처리하기 위해서 몇 가지 컨텍스트 정보가 필요하다. 이러한 이유로 게시자는 종종 일부 컨텍스트 데이터를 알림 메서드의 인수로 전달한다. 게시자는 자신을 인수로 전달하여 구독자가 필요한 데이터를 직접 가져올 수 있다.
  1. 클라이언트는 게시자 및 구독자 객체를 별도로 생성한 다음 게시자 업데이트를 위해 구독자를 등록한다.

장점

  • 개방/폐쇄 원칙: 게시자의 코드를 변경하지 않고도 새 구독자 클래스를 도입시킬 수 있다.
  • 런타임에 객체 간의 관계를 설정 할 수 있다.

단점

  • 구독자는 무작위 순서로 알림을 받게 된다.

In Swift

개념적 사례

import XCTest

class Subject {

    // 모든 구독자에게 필수적인 Subject의 상태가 이 변수에 저장된다.
    var state: Int = { return Int(arc4random_uniform(10)) }()

    /// 구독자 목록. 실제로는 구독자 목록을 더 포괄적으로 저장할 수 있다.(이벤트 유형 등으로 분류)
    private lazy var observers = [Observer]()

    /// 구독 관리 메서드
    func attach(_ observer: Observer) {
        print("Subject: Attached an observer.\n")
        observers.append(observer)
    }

    func detach(_ observer: Observer) {
        if let idx = observers.firstIndex(where: { $0 === observer }) {
            observers.remove(at: idx)
            print("Subject: Detached an observer.\n")
        }
    }

    // 각 구독자들의 업데이트를 유발하는 메서드.
    func notify() {
        print("Subject: Notifying observers...\n")
        observers.forEach({ $0.update(subject: self)})
    }

    /// 일반적으로 구독 로직은 Subject가 실제로 할 수 있는 것의 극히 일부에 불과하다. Subject는 일반적으로 중요한 비즈니스 로직을 보유한다.
    /// 중요한 일이 일어나려고 할 때마다 알림 메시지를 트리거 한다.
    func someBusinessLogic() {
        print("\nSubject: I'm doing something important.\n")
        state = Int(arc4random_uniform(10))
        print("Subject: My state has just changed to: \(state)\n")
        notify()
    }
}

/// 관찰자 프로토콜은 Subject가 사용하는 업데이트 메서드를 선언한다.
protocol Observer: class {

    func update(subject: Subject)
}

/// 구체 Observer는 그들이 관찰하는 Subject가 발행한 업데이트에 반응한다.
class ConcreteObserverA: Observer {

    func update(subject: Subject) {

        if subject.state < 3 {
            print("ConcreteObserverA: Reacted to the event.\n")
        }
    }
}

class ConcreteObserverB: Observer {

    func update(subject: Subject) {

        if subject.state >= 3 {
            print("ConcreteObserverB: Reacted to the event.\n")
        }
    }
}

/// Let's see how it all works together.
class ObserverConceptual: XCTestCase {

    func testObserverConceptual() {

        let subject = Subject()

        let observer1 = ConcreteObserverA()
        let observer2 = ConcreteObserverB()

        subject.attach(observer1)
        subject.attach(observer2)

        subject.someBusinessLogic()
        subject.someBusinessLogic()
        subject.detach(observer2)
        subject.someBusinessLogic()
    }
}

실제 사례

import XCTest

class ObserverRealWorld: XCTestCase {

    // 클라이언트 코드
    func test() {

        let cartManager = CartManager()

        let navigationBar = UINavigationBar()
        let cartVC = CartViewController()

        cartManager.add(subscriber: navigationBar)
        cartManager.add(subscriber: cartVC)

        let apple = Food(id: 111, name: "Apple", price: 10, calories: 20)
        cartManager.add(product: apple)

        let tShirt = Clothes(id: 222, name: "T-shirt", price: 200, size: "L")
        cartManager.add(product: tShirt)

        cartManager.remove(product: apple)
    }
}

protocol CartSubscriber: CustomStringConvertible {

    func accept(changed cart: [Product])
}

protocol Product {

    var id: Int { get }
    var name: String { get }
    var price: Double { get }

    func isEqual(to product: Product) -> Bool
}

extension Product {

    func isEqual(to product: Product) -> Bool {
        return id == product.id
    }
}

struct Food: Product {

    var id: Int
    var name: String
    var price: Double

    /// Food-specific properties
    var calories: Int
}

struct Clothes: Product {

    var id: Int
    var name: String
    var price: Double

    /// Clothes-specific properties
    var size: String
}

class CartManager { // Publisher의 역할

    private lazy var cart = [Product]()
    private lazy var subscribers = [CartSubscriber]() //구독자 목록

    func add(subscriber: CartSubscriber) {
        print("CartManager: I'am adding a new subscriber: \(subscriber.description)")
        subscribers.append(subscriber)
    }

    func add(product: Product) { // 카트에 product가 담기면 구독자들에게 알린다.
        print("\nCartManager: I'am adding a new product: \(product.name)")
        cart.append(product)
        notifySubscribers()
    }

    func remove(subscriber filter: (CartSubscriber) -> (Bool)) {
        guard let index = subscribers.firstIndex(where: filter) else { return }
        subscribers.remove(at: index)
    }

    func remove(product: Product) { 카트에 product가 빠지면 구독자들에게 알린다.
        guard let index = cart.firstIndex(where: { $0.isEqual(to: product) }) else { return }
        print("\nCartManager: Product '\(product.name)' is removed from a cart")
        cart.remove(at: index)
        notifySubscribers()
    }

    private func notifySubscribers() {
        subscribers.forEach({ $0.accept(changed: cart) })
    }
}

extension UINavigationBar: CartSubscriber {

    func accept(changed cart: [Product]) {
        print("UINavigationBar: Updating an appearance of navigation items")
    }

    open override var description: String { return "UINavigationBar" }
}

class CartViewController: UIViewController, CartSubscriber {

    func accept(changed cart: [Product]) {
        print("CartViewController: Updating an appearance of a list view with products")
    }

    open override var description: String { return "CartViewController" }
}
출처: refactoring guru

Uploaded by N2T

반응형