소프트웨어 디자인 패턴

옵저버 패턴 쉽게 이해하기

Enchantée 2026. 6. 20. 16:33
728x90
반응형

디자인 패턴을 공부하다 보면 전략 패턴과 함께 자주 등장하는 패턴이 옵저버 패턴입니다.

옵저버 패턴은 어떤 객체의 상태가 바뀌었을 때, 그 객체를 지켜보는 여러 객체에게 자동으로 알림을 보내는 구조입니다.

이벤트 처리, UI 갱신, 알림 시스템, 데이터 변경 통지, 게임 오브젝트 이벤트 등 다양한 곳에서 사용됩니다.

 

옵저버 패턴은 한 객체의 변경을 여러 객체에게 느슨하게 전달하기 위한 디자인 패턴입니다.

 

이번 글에서는 옵저버 패턴이 왜 필요한지, C++에서는 어떻게 구현할 수 있는지, 그리고 실무에서 어떤 점을 조심해야 하는지 정리해보겠습니다.

 


1. 옵저버 패턴은 무엇인가?

옵저버 패턴은 Observer Pattern이라고 부릅니다.

어떤 객체의 상태가 변하면, 그 객체에 등록된 여러 관찰자에게 변경 사실을 알려주는 패턴입니다.

여기서 상태를 가지고 있고 변경을 알리는 쪽을 Subject 또는 Observable이라고 부릅니다.

변경 알림을 받는 쪽을 Observer라고 부릅니다.

구성 요소 역할
Subject 상태를 가지고 있으며 Observer를 등록, 제거, 알림 처리
Observer Subject의 변경 알림을 받는 인터페이스
ConcreteSubject 실제 상태 변경이 일어나는 객체
ConcreteObserver 알림을 받아 자신의 동작을 수행하는 객체

옵저버 패턴의 핵심은 Subject가 구체적인 Observer의 동작을 몰라도 된다는 점입니다.

Subject는 Observer 인터페이스만 알고, 실제로 어떤 객체가 어떤 방식으로 반응하는지는 알 필요가 없습니다.

 


2. 왜 옵저버 패턴이 필요한가?

예를 들어 날씨 데이터를 관리하는 객체가 있다고 가정해보겠습니다.

온도가 바뀔 때마다 모바일 화면, 서버 로그, 알림 시스템, 통계 시스템을 모두 갱신해야 합니다.

단순하게 구현하면 WeatherData 클래스가 모든 대상을 직접 호출할 수 있습니다.

void WeatherData::setTemperature(int temperature) {
    temperature_ = temperature;

    mobileView.update(temperature_);
    logger.write(temperature_);
    alertService.send(temperature_);
    statistics.record(temperature_);
}

 

처음에는 간단해 보이지만, 시간이 지나면 문제가 생깁니다.

  1. 새로운 알림 대상이 추가될 때마다 WeatherData를 수정해야 한다.
  2. WeatherData가 너무 많은 구체 클래스에 의존한다.
  3. 일부 알림 대상만 테스트하기 어렵다.
  4. 알림 대상의 생명주기 관리가 복잡해진다.
  5. 변경 통지 로직과 실제 상태 관리 로직이 섞인다.

옵저버 패턴은 이 문제를 Observer 인터페이스로 분리합니다.

WeatherData는 누가 알림을 받는지 구체적으로 몰라도 되고, 등록된 Observer들에게만 변경 사실을 전달하면 됩니다.

 


3. 구조를 그림으로 보기

옵저버 패턴의 구조를 단순화하면 다음처럼 볼 수 있습니다.

- Observer Pattern 구조

Subject
   |
   | holds observers
   v
Observer interface
   ^
   |
   +----------------+----------------+
   |                |                |
   v                v                v
MobileView       Logger        AlertService

상태 변경 발생
   |
   v
Subject.notify()
   |
   +-- MobileView.update()
   +-- Logger.update()
   +-- AlertService.update()

 

Subject는 여러 Observer를 목록으로 가지고 있다가 상태가 바뀌면 notify를 호출합니다.

각 Observer는 같은 인터페이스를 구현하지만, 실제 반응은 서로 다를 수 있습니다.

이 구조 덕분에 Subject와 Observer 사이의 결합도를 낮출 수 있습니다.

 


4. C++로 옵저버 패턴 구현하기

먼저 가장 기본적인 형태로 옵저버 패턴을 구현해보겠습니다.

Observer 인터페이스를 만들고, Subject는 Observer 포인터 목록을 가지고 있다가 상태 변경 시 update를 호출합니다.

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

class Observer {
public:
    virtual ~Observer() = default;
    virtual void update(int temperature) = 0;
};

class WeatherData {
public:
    void attach(Observer* observer) {
        observers_.push_back(observer);
    }

    void detach(Observer* observer) {
        observers_.erase(
            std::remove(observers_.begin(), observers_.end(), observer),
            observers_.end()
        );
    }

    void setTemperature(int temperature) {
        temperature_ = temperature;
        notify();
    }

private:
    void notify() {
        for (Observer* observer : observers_) {
            observer->update(temperature_);
        }
    }

    int temperature_ = 0;
    std::vector<Observer*> observers_;
};

class MobileDisplay : public Observer {
public:
    void update(int temperature) override {
        std::cout << "[mobile] temperature: " << temperature << '\n';
    }
};

class LogWriter : public Observer {
public:
    void update(int temperature) override {
        std::cout << "[log] weather changed to " << temperature << '\n';
    }
};

int main() {
    WeatherData weather;
    MobileDisplay mobile;
    LogWriter logger;

    weather.attach(&mobile);
    weather.attach(&logger);

    weather.setTemperature(27);

    weather.detach(&logger);
    weather.setTemperature(30);
}

 

WeatherData는 MobileDisplay와 LogWriter의 구체 타입을 알지 않습니다.

그저 Observer 인터페이스를 구현한 객체에게 update를 호출합니다.

새로운 관찰자가 필요하면 Observer를 상속한 클래스를 추가하고 attach하면 됩니다.

이것이 옵저버 패턴의 기본 구조입니다.

 


5. 옵저버 패턴의 장점

옵저버 패턴의 가장 큰 장점은 변경을 받는 대상이 늘어나도 Subject의 핵심 로직을 크게 바꾸지 않아도 된다는 점입니다.

WeatherData 예제에서 새로운 알림 대상이 필요하면 새로운 Observer를 만들고 등록하면 됩니다.

구분 직접 호출 방식 옵저버 패턴
알림 대상 추가 Subject 코드 수정 Observer 구현 추가 후 등록
결합도 Subject가 구체 클래스를 앎 Subject는 Observer 인터페이스만 앎
테스트 여러 대상이 한 함수에 섞이기 쉬움 Observer별 테스트가 쉬움
확장성 알림 대상이 늘수록 복잡해짐 등록/해제 구조로 확장 가능

이 구조는 UI와 데이터 모델을 분리할 때도 유용합니다.

데이터가 바뀌면 여러 화면 요소가 갱신될 수 있지만, 데이터 객체가 각 화면 요소를 직접 알아야 할 필요는 없습니다.

이벤트 기반 시스템에서도 같은 관점이 적용됩니다.

 


6. 생명주기 문제를 조심해야 한다

앞의 예제는 개념을 설명하기 위한 단순한 코드입니다.

하지만 실무에서 raw pointer로 Observer를 관리하면 생명주기 문제가 생길 수 있습니다.

Subject가 Observer*를 가지고 있는데 Observer 객체가 먼저 사라지면, Subject에는 더 이상 유효하지 않은 포인터가 남습니다.

이 상태에서 notify를 호출하면 dangling pointer 문제가 발생할 수 있습니다.

C++에서는 이 문제를 피하기 위해 소유권과 관찰 관계를 명확히 분리해야 합니다.

한 가지 방법은 Subject가 Observer를 소유하지 않고 weak_ptr로 관찰하는 방식입니다.

#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class EventObserver {
public:
    virtual ~EventObserver() = default;
    virtual void onEvent(const std::string& message) = 0;
};

class EventSource {
public:
    void subscribe(const std::shared_ptr<EventObserver>& observer) {
        observers_.push_back(observer);
    }

    void publish(const std::string& message) {
        removeExpiredObservers();

        for (const auto& weakObserver : observers_) {
            if (auto observer = weakObserver.lock()) {
                observer->onEvent(message);
            }
        }
    }

private:
    void removeExpiredObservers() {
        observers_.erase(
            std::remove_if(
                observers_.begin(),
                observers_.end(),
                [](const std::weak_ptr<EventObserver>& observer) {
                    return observer.expired();
                }
            ),
            observers_.end()
        );
    }

    std::vector<std::weak_ptr<EventObserver>> observers_;
};

class ConsoleSubscriber : public EventObserver {
public:
    explicit ConsoleSubscriber(std::string name) : name_(std::move(name)) {}

    void onEvent(const std::string& message) override {
        std::cout << name_ << ": " << message << '\n';
    }

private:
    std::string name_;
};

int main() {
    EventSource source;

    auto first = std::make_shared<ConsoleSubscriber>("first");
    source.subscribe(first);

    {
        auto temporary = std::make_shared<ConsoleSubscriber>("temporary");
        source.subscribe(temporary);
        source.publish("event 1");
    }

    source.publish("event 2");
}

 

EventSource는 Observer를 소유하지 않습니다.

대신 weak_ptr로 관찰하고, publish 시점에 아직 살아 있는 Observer만 호출합니다.

temporary Observer는 블록을 벗어나면 사라지고, 다음 publish에서는 expired 상태가 되어 정리됩니다.

이 방식은 raw pointer보다 안전하지만, shared_ptr과 weak_ptr의 비용과 구조적 복잡도도 함께 고려해야 합니다.

 


7. 옵저버 패턴과 Publish-Subscribe는 같은가?

옵저버 패턴과 Publish-Subscribe 구조는 비슷해 보이지만 완전히 같지는 않습니다.

둘 다 어떤 이벤트를 여러 대상에게 전달한다는 점은 같습니다.

하지만 일반적으로 옵저버 패턴은 Subject와 Observer가 직접적인 등록 관계를 가집니다.

반면 Publish-Subscribe 구조는 중간에 message broker나 event bus가 들어가는 경우가 많습니다.

구분 Observer Pattern Publish-Subscribe
연결 방식 Subject가 Observer 목록을 가짐 중간 broker 또는 event bus를 통함
결합도 상대적으로 직접적 더 느슨한 편
사용 예시 UI 상태 갱신, 객체 내부 이벤트 메시지 큐, 도메인 이벤트, 분산 시스템
규모 프로세스 내부 객체 관계에 적합 서비스 간 통신에도 사용 가능

프로세스 내부에서 객체 간 상태 변경을 알리는 정도라면 옵저버 패턴이 자연스럽습니다.

서비스 간 이벤트 전달이나 비동기 메시징이 필요하다면 Publish-Subscribe 구조를 고려하는 편이 더 적합할 수 있습니다.

 


8. 언제 옵저버 패턴을 쓰면 좋을까?

옵저버 패턴은 다음 상황에서 유용합니다.

  1. 한 객체의 변경을 여러 객체가 알아야 한다.
  2. 알림 대상이 계속 추가되거나 제거될 수 있다.
  3. Subject가 구체적인 알림 대상을 몰라도 된다.
  4. UI 갱신, 이벤트 처리, 상태 변경 통지를 분리하고 싶다.
  5. 변경 발생 지점과 반응하는 로직을 느슨하게 연결하고 싶다.

예를 들어 문서 편집기에서 문서가 수정되면 저장 상태 표시, 미리보기, 자동 저장 모듈이 각각 반응할 수 있습니다.

게임에서는 캐릭터 체력이 바뀔 때 UI, 사운드, 업적 시스템이 각각 반응할 수 있습니다.

서버에서는 설정 변경이나 캐시 무효화 이벤트를 여러 컴포넌트에 전달할 수도 있습니다.

이처럼 하나의 변경에 여러 반응이 붙는 구조라면 옵저버 패턴을 떠올릴 수 있습니다.

 


9. 자주 하는 오해

9-1. 옵저버 패턴은 이벤트 시스템과 완전히 같다

옵저버 패턴은 이벤트 시스템을 구현하는 방법 중 하나입니다.

하지만 모든 이벤트 시스템이 옵저버 패턴인 것은 아닙니다.

메시지 큐, event bus, callback registry, signal-slot 구조처럼 다양한 방식이 있습니다.


9-2. Observer는 Subject를 절대 몰라야 한다

이상적으로는 Observer가 Subject에 대한 의존을 줄이는 것이 좋습니다.

하지만 실제 코드에서는 update 시점에 필요한 데이터를 Subject에서 가져오도록 설계하는 경우도 있습니다.

중요한 것은 의존 방향과 생명주기를 명확히 관리하는 것입니다.


9-3. 옵저버가 많아져도 항상 안전하다

Observer가 많아지면 notify 비용이 커질 수 있습니다.

또한 한 Observer의 처리 시간이 길면 전체 알림 흐름이 늦어질 수 있습니다.

실무에서는 동기 호출인지 비동기 호출인지, 실패한 Observer가 전체 흐름에 영향을 줘도 되는지까지 정해야 합니다.

 


10. 실무에서는 어떻게 볼까?

실무에서 옵저버 패턴을 사용할 때는 구조보다 운영상의 위험을 먼저 봐야 합니다.

특히 C++에서는 생명주기와 소유권이 중요합니다.

Subject가 Observer를 소유하는지, 단순히 참조만 하는지, Observer가 먼저 사라질 수 있는지 명확히 정해야 합니다.

또한 notify 중에 Observer 목록이 변경되는 경우도 조심해야 합니다.

  1. Observer 등록과 해제 시점이 명확한가?
  2. Subject가 Observer를 소유하는가, 관찰만 하는가?
  3. Observer가 notify 중에 자기 자신을 해제할 수 있는가?
  4. 한 Observer의 예외나 실패가 전체 알림을 멈춰도 되는가?
  5. 알림이 동기 호출이어야 하는가, 비동기 큐로 넘겨야 하는가?

옵저버 패턴은 결합도를 낮추는 데 도움이 되지만, 흐름이 눈에 덜 보이게 만들 수도 있습니다.

어떤 이벤트가 발생했을 때 어디까지 전파되는지 추적하기 어렵다면 로그, 이름 있는 이벤트 타입, 명확한 등록 위치가 필요합니다.

그래서 실무에서는 작은 객체 관계에는 옵저버 패턴을 쓰고, 규모가 커지면 event bus나 message queue 같은 구조로 확장하는 경우가 많습니다.

 


11. 면접 질문 예시

질문 1. 옵저버 패턴은 어떤 문제를 해결하나요?

옵저버 패턴은 한 객체의 상태 변경을 여러 객체에게 전달해야 할 때, Subject와 Observer 사이의 결합도를 낮추는 데 사용합니다.

Subject는 구체적인 Observer를 알 필요 없이 공통 인터페이스를 통해 변경을 통지합니다.

그 결과 알림 대상이 추가되어도 Subject의 핵심 로직 수정을 줄일 수 있습니다.


질문 2. 옵저버 패턴의 단점은 무엇인가요?

알림 흐름이 간접적이기 때문에 코드 추적이 어려워질 수 있습니다.

Observer가 많아지면 notify 비용이 커질 수 있고, 한 Observer의 실패가 전체 흐름에 영향을 줄 수도 있습니다.

C++에서는 Observer의 생명주기와 dangling pointer 문제도 반드시 고려해야 합니다.


질문 3. 옵저버 패턴과 Pub/Sub의 차이는 무엇인가요?

옵저버 패턴은 보통 Subject가 Observer 목록을 직접 관리하는 구조입니다.

Pub/Sub은 중간에 broker나 event bus를 두어 publisher와 subscriber를 더 느슨하게 분리하는 구조입니다.

작은 객체 관계에는 옵저버 패턴이 적합하고, 서비스 간 비동기 이벤트 전달에는 Pub/Sub 구조가 더 적합할 수 있습니다.

 


12. 정리

옵저버 패턴은 한 객체의 변경을 여러 객체에게 알리기 위한 디자인 패턴입니다.

핵심만 정리하면 다음과 같습니다.

  • Subject는 상태 변경을 알리는 객체다.
  • Observer는 변경 알림을 받는 객체다.
  • Subject는 구체 Observer가 아니라 Observer 인터페이스에 의존한다.
  • 알림 대상이 늘어나는 구조에서 결합도를 낮출 수 있다.
  • C++에서는 Observer 생명주기와 소유권 관리가 특히 중요하다.
  • 규모가 커지면 event bus나 Pub/Sub 구조를 함께 고려할 수 있다.

옵저버 패턴을 잘 이해하면 이벤트 기반 코드를 더 명확하게 볼 수 있습니다.

중요한 것은 패턴 이름이 아니라, 변경이 발생하는 곳과 그 변경에 반응하는 곳을 어떻게 느슨하게 연결할지입니다.

이 관점을 가지고 있으면 UI, 서버 이벤트, 게임 로직, 상태 관리 코드에서 옵저버 패턴을 더 현실적으로 활용할 수 있습니다.

 


728x90
반응형

'소프트웨어 디자인 패턴' 카테고리의 다른 글

Decorator Pattern 쉽게 이해하기  (0) 2026.06.27
전략 패턴 쉽게 이해하기  (0) 2026.06.20