봄맞이 세일

옵서버 패턴

다음 이름으로도 불립니다: 이벤트 구독자, 경청자, Observer

의도

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

옵서버 디자인 패턴

문제

Customer​(손님) 및 Store​(가게)​라는 두 가지 유형의 객체들이 있다고 가정합니다. 손님은 곧 매장에 출시될 특정 브랜드의 제품​(예: 새 아이폰 모델)​에 매우 관심이 있습니다.

손님은 매일 매장을 방문하여 제품 재고를 확인할 수 있으나, 제품이 매장에 아직 운송되는 동안 이러한 방문 대부분은 무의미합니다.

매장 방문 vs. 스팸 발송

매장 방문 vs. 스팸 발송

반면 매장에서는 새로운 제품이 출시될 때마다 모든 고객에게 스팸으로 간주할 수 있는 수많은 이메일을 보낼 수 있습니다. 이 수많은 이메일은 일부 고객들을 신제품 출시 확인을 위한 잦은 매장 방문으로부터 구출해낼 수 있으나, 동시에 신제품 출시에 관심이 없는 다른 고객들을 화나게 할 것입니다.

여기서 충돌이 발생합니다. 손님들이 신제품 출시 확인을 위해 시간을 낭비하든지, 매장들이 알림을 원하지 않는 고객들에게 신제품 출시를 알리며 자원을 낭비해야 합니다.

해결책

시간이 지나면 변경될 수 있는 중요한 상태를 가진 객체가 있다고 가정해봅시다. 이 객체는 종종 ​(subject)라고 불립니다. 그러나 위 예시의 경우 이 객체는 자신의 상태에 대한 변경에 대해 다른 객체들에 알림을 보내는 역할도 맡을 것이니 해당 객체를 라고 부르겠습니다.

옵서버 패턴은 출판사 클래스에 개별 객체들이 그 출판사로부터 오는 이벤트들의 알림들을 구독 또는 구독 취소할 수 있도록 구독 메커니즘을 추가할 것을 제안합니다. 두려워하지 마세요. 그리 복잡하지 않습니다. 실제로 이 메커니즘은 1) 구독자 객체들에 대한 참조의 리스트를 저장하기 위한 배열 필드와 2) 그 리스트에 구독자들을 추가하거나 제거할 수 있도록 하는 여러 공개된​(public) 메서드들로 구성됩니다.

구독 메커니즘

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

이제 출판사에 중요한 이벤트가 발생할 때마다 구독자 리스트를 참조한 후 그들의 객체들에 있는 특정 알림 메서드를 호출합니다.

실제 앱에는 같은 출판사 클래스의 이벤트들을 추적하는 데 관심이 있는 수십 개의 서로 다른 구독자 클래스들이 있을 수 있습니다. 당신은 출판사를 이러한 모든 클래스에 결합하고 싶지 않을 것입니다. 게다가 당신은 당신의 출판사 클래스가 다른 사람들에 의해 사용되어야 한다면 이러한 구독자 클래스 중 일부는 미리 알지 못할 수도 있습니다.

그러므로 모든 구독자가 같은 인터페이스를 구현하고 출판사가 오직 그 인터페이스를 통해서만 구독자들과 통신하는 것이 매우 중요합니다. 이 인터페이스는 출판사가 알림과 어떤 콘텍스트 데이터를 전달하는 데 사용할 수 있는 매개변수들의 집합과 알림 메서드를 선언해야 합니다.

알림 메서드들

출판사는 특정 알림 메서드를 구독자들의 객체들에서부터 호출하여 그들에게 알림을 보냅니다.

당신의 앱에 여러 유형의 출판사가 있고 이들을 구독자들과 호환되도록 하려면 당신은 더 나아가 모든 출판사가 같은 인터페이스를 따르도록 할 수 있습니다. 이 인터페이스는 몇 가지 구독 메서드들만 설명하면 됩니다. 이 인터페이스를 통해 구독자들은 출판자들의 상태들을 그들의 구상 클래스들과 결합하지 않고 관찰할 수 있습니다.

실제상황 적용

잡지 및 신문 구독

잡지 및 신문 구독.

당신이 신문이나 잡지를 구독한다면 다음 호가 있는지 확인하러 가게에 갈 필요가 없습니다. 대신 출판사가 발행 직후나 사전에 새 발행물을 구독자의 우편함으로 직접 보냅니다.

출판사는 구독자 리스트를 유지 관리하고 구독자들이 어떤 잡지에 관심 있는지 알고 있습니다. 출판사가 새로운 잡지의 발행호들를 보내는 것을 중단시키고 싶다면 구독자들은 언제든지 이 리스트를 떠날 수 있습니다.

구조

옵서버 디자인 패턴 구조옵서버 디자인 패턴 구조
  1. 출판사는 다른 객체들에 관심 이벤트들을 발행합니다. 이러한 이벤트들은 출판사가 상태를 전환하거나 어떤 행동들을 실행할 때 발생합니다. 출판사들에는 구독 인프라가 포함되어 있으며, 이 인프라는 현재 구독자들이 리스트를 떠나고 새 구독자들이 리스트에 가입할 수 있도록 합니다.

  2. 새 이벤트가 발생하면 출판사는 구독자 리스트를 살펴본 후 각 구독자 객체의 구독자 인터페이스에 선언된 알림 메서드를 호출합니다.

  3. 구독자 인터페이스는 알림 인터페이스를 선언하며 대부분의 경우 단일 update 메서드로 구성됩니다. 이 메서드에는 출판사가 업데이트와 함께 어떤 이벤트의 세부 정보들을 전달할 수 있도록 하는 여러 매개변수가 있을 수 있습니다.

  4. 구상 구독자들은 출판사가 보낸 알림들에 대한 응답으로 몇 가지 작업을 수행합니다. 이러한 모든 클래스는 출판사가 구상 클래스들과 결합하지 않도록 같은 인터페이스를 구현해야 합니다.

  5. 일반적으로 구독자들은 업데이트를 올바르게 처리하기 위해 콘텍스트 정보가 어느 정도 필요로 합니다. 그러므로 출판사들은 종종 콘텍스트 데이터를 알림 메서드의 인수들로 전달합니다. 출판사는 자신을 인수로 전달할 수 있으며, 구독자가 필요한 데이터를 직접 가져오도록 합니다.

  6. 클라이언트는 출판사 및 구독자 객체들을 별도로 생성한 후 구독자들을 출판사 업데이트에 등록합니다.

의사코드

이 예시에서 옵서버 패턴은 텍스트 편집기 객체가 다른 서비스 객체들에 자신의 상태 변경에 대해 알릴 수 있도록 합니다.

옵서버 패턴 구조 예시

다른 객체들에 발생하는 이벤트에 대해 객체들에 알립니다.

구독자 리스트는 동적으로 컴파일됩니다. 당신이 앱이 원하는 행동에 따라 객체들은 런타임 때 알림들을 받는 것을 시작하거나 중단할 수 있습니다.

이 구현에서 편집기 클래스는 자체적으로 구독 리스트를 유지 관리하지 않습니다. 편집기 클래스는 이 작업을 해당 작업을 전담하는 특수 도우미 객체에 위임합니다. 이 객체를 중앙 집중식 이벤트 디스패처 역할을 하도록 업그레이드하여 모든 객체가 출판사 역할을 하도록 할 수 있습니다.

앱에 새 구독자들을 추가할 때 기존 출판사 클래스들이 같은 인터페이스를 통해 모든 구독자와 작업하는 한 기존 출판사 클래스들은 변경할 필요가 없습니다.

// 기초 출판사 클래스에는 구독 관리 코드 및 알림 메서드들이 포함됩니다.
class EventManager is
    private field listeners: hash map of event types and listeners

    method subscribe(eventType, listener) is
        listeners.add(eventType, listener)

    method unsubscribe(eventType, listener) is
        listeners.remove(eventType, listener)

    method notify(eventType, data) is
        foreach (listener in listeners.of(eventType)) do
            listener.update(data)

// 구상 출판사는 일부 구독자에게 흥미로운 실제 비즈니스 논리를 포함합니다. 우리는
// 이 클래스를 기초 출판사로부터 파생시킬 수 있습니다. 그러나 이는 현실에서 항상
// 가능하지 않습니다. 왜냐하면 구상 클래스가 이미 자식 클래스일 수 있기
// 때문입니다. 이 경우 여기에서 했던 것처럼 합성 관계 속으로 구독 논리를 덧붙여
// 넣을 수 있습니다.
class Editor is
    public field events: EventManager
    private field file: File

    constructor Editor() is
        events = new EventManager()

    // 비즈니스 로직의 메서드들은 구독자들에게 변경 사항에 대해 알릴 수 있습니다.
    method openFile(path) is
        this.file = new File(path)
        events.notify("open", file.name)

    method saveFile() is
        file.write()
        events.notify("save", file.name)

    // …


// 여기 구독자 인터페이스가 있습니다. 사용 중인 프로그래밍 언어가 함수형 타입을
// 지원하는 경우 전체 구독자 계층구조를 함수들의 집합으로 바꿀 수 있습니다.
interface EventListener is
    method update(filename)

// 구상 구독자들은 자신이 연결된 출판사가 발행한 업데이트에 반응합니다.
class LoggingListener implements EventListener is
    private field log: File
    private field message: string

    constructor LoggingListener(log_filename, message) is
        this.log = new File(log_filename)
        this.message = message

    method update(filename) is
        log.write(replace('%s',filename,message))

class EmailAlertsListener implements EventListener is
    private field email: string
    private field message: string

    constructor EmailAlertsListener(email, message) is
        this.email = email
        this.message = message

    method update(filename) is
        system.email(email, replace('%s',filename,message))


// 앱은 런타임에 출판사들과 구독자들을 설정할 수 있습니다.
class Application is
    method config() is
        editor = new Editor()

        logger = new LoggingListener(
            "/path/to/log.txt",
            "Someone has opened the file: %s")
        editor.events.subscribe("open", logger)

        emailAlerts = new EmailAlertsListener(
            "admin@example.com",
            "Someone has changed the file: %s")
        editor.events.subscribe("save", emailAlerts)

적용

옵서버 패턴은 한 객체의 상태가 변경되어 다른 객체들을 변경해야 할 필요성이 생겼을 때, 그리고 실제 객체 집합들을 미리 알 수 없거나 이러한 집합들이 동적으로 변경될 때 사용하세요.

이런 문제는 GUI 클래스와 작업할 때 자주 경험할 수 있습니다. 예를 들어 당신이 사용자 정의 버튼 클래스들을 생성했고, 이제 클라이언트들이 사용자 정의 코드를 버튼에 연결하여 사용자가 버튼을 누를 때마다 실행되도록 하고 싶다고 가정합시다.

옵서버 패턴은 구독자 인터페이스를 구현하는 모든 객체가 출판사 객체의 이벤트 알림들에 구독할 수 있도록 합니다. 당신은 버튼에 구독 메커니즘을 추가할 수 있으며, 클라이언트들이 사용자 정의 구독자 클래스들을 통해 사용자 정의 코드를 연결하도록 할 수 있습니다.

이 패턴은 앱의 일부 객체들이 제한된 시간 동안 또는 특정 경우에만 다른 객체들을 관찰해야 할 때 사용하세요.

구독 리스트는 동적이므로 구독자들은 필요할 때마다 리스트에 가입하거나 탈퇴할 수 있습니다.

구현방법

  1. 당신의 앱의 비즈니스 로직을 살펴보고 두 부분으로 나누세요. 핵심 기능들은 다른 코드와 독립적이며 출판사 역할을 합니다. 나머지는 구독자 클래스들의 집합으로 바뀝니다.

  2. 구독자 인터페이스를 선언하세요. 이 인터페이스는 최소한 하나의 update 메서드를 선언해야 합니다.

  3. 출판사 인터페이스를 선언하고 구독자 객체를 구독자 리스트에 추가 및 제거하는 한 쌍의 메서드에 대해 기술하세요. 출판사들은 구독자 인터페이스를 통해서만 구독자들과 작업해야 합니다.

  4. 구독 메서드들의 구현과 실제 구독 리스트를 어디에 배치할지 결정하세요. 일반적으로 모든 유형의 출판사에서 이 코드는 실질적으로 유사하므로 출판사 인터페이스에서 직접 파생된 추상 클래스에 코드를 넣는 것이 가장 적합합니다. 구상 출판사들은 이 클래스를 확장하여 해당 클래스의 구독 행동을 상속합니다.

    그러나 기존 클래스 계층구조에 패턴을 적용하는 경우 합성에 기반한 접근 방식을 고려하세요. 구독 로직을 별도의 객체에 넣고 모든 실제 출판사들이 이를 사용하도록 하세요.

  5. 구상 출판사 클래스들을 만드세요. 출판사 내부에서 중요한 일이 발생할 때마다 모든 구독자에게 알림을 전달해야 합니다.

  6. 구상 구독자 클래스들에서 업데이트 알림 메서드들을 구현하세요. 대부분의 구독자는 이벤트에 대한 일부 콘텍스트 데이터가 필요하며, 이 데이터는 알림 메서드의 인수로 전달될 수 있습니다.

    그러나 다른 옵션이 있습니다. 알림을 받으면 구독자들이 알림에서 직접 모든 데이터를 가져오도록 하는 것입니다. 이 경우 출판사는 업데이트 메서드를 통해 자신을 전달해야 합니다. 유연성이 보다 떨어지는 옵션은 생성자를 통해 출판자를 구독자에 영구적으로 연결하는 것입니다.

  7. 클라이언트는 필요한 모든 구독자를 생성하고 적절한 출판사들과 등록시켜야 합니다.

장단점

  • / . 출판사의 코드를 변경하지 않고도 새 구독자 클래스들을 도입할 수 있습니다. (출판사 인터페이스가 있는 경우 그 반대로 구독자의 클래스들을 변경하지 않고 새 출판사 클래스들을 도입하는 것 역시 가능합니다).
  • 런타임에 객체 간의 관계들을 형성할 수 있습니다.
  • 구독자들은 무작위로 알림을 받습니다.

다른 패턴과의 관계

  • 커맨드, 중재자, 옵서버책임 연쇄 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룹니다.

    • 패턴은 잠재적 수신자의 동적 체인을 따라 수신자 중 하나에 의해 요청이 처리될 때까지 요청을 순차적으로 전달합니다.
    • 패턴은 발신자와 수신자 간의 단방향 연결을 설립합니다.
    • 패턴은 발신자와 수신자 간의 직접 연결을 제거하여 그들이 중재자 객체를 통해 간접적으로 통신하도록 강제합니다.
    • 패턴은 수신자들이 요청들의 수신을 동적으로 구독 및 구독 취소할 수 있도록 합니다.
  • 중재자옵서버 패턴의 차이는 종종 애매합니다. 대부분의 경우 두 패턴 중 하나를 구현할 수 있으나, 때로는 두 패턴을 동시에 적용할 수 있습니다. 이것이 어떻게 가능한지 살펴보겠습니다.

    의 주목적은 시스템 컴포넌트들의 집합 간의 상호 의존성을 제거하는 것입니다. 그러면 이러한 컴포넌트들은 대신 단일 중재자 객체에 의존하게 됩니다. 의 목적은 객체들 사이에 단방향 연결을 설정하는 것으로, 여기서 일부 객체는 다른 객체의 종속자 역할을 합니다.

    에 의존하는 패턴의 인기 있는 구현이 있습니다. 중재자 객체는 출판사의 역할을 맡고, 컴포넌트들은 중재자의 이벤트들을 구독 및 구독 취소하는 구독자들의 역할을 맡습니다. 가 이러한 방식으로 구현되면 과 매우 유사하게 보일 수 있습니다.

    만약 혼란스러우시다면 중재자 패턴을 다른 방법들로 구현할 수 있음을 기억하세요. 예를 들어 모든 컴포넌트를 영구적으로 같은 중재자 객체에 연결하는 방법이 있습니다. 이 구현은 패턴과 유사하지 않겠지만 여전히 중재자 패턴의 인스턴스일 것입니다.

    이제 모든 컴포넌트가 출판사가 되어 서로 간의 동적 연결을 허용하는 프로그램을 상상해 보세요. 중앙화된 중재자 객체는 없고 분산된 옵서버들의 집합만 있을 것입니다.

코드 예시

C#으로 작성된 옵서버 C++로 작성된 옵서버 Go로 작성된 옵서버 자바로 작성된 옵서버 PHP로 작성된 옵서버 파이썬으로 작성된 옵서버 루비로 작성된 옵서버 러스트로 작성된 옵서버 스위프트로 작성된 옵서버 타입스크립트로 작성된 옵서버