TIL

[C++] 객체지향 프로그래밍: 옵저버 패턴(Observer Pattern) 구현

think95592 2026. 6. 2. 19:53

1. 오늘 배운 개념 (What I Learned)

오늘은 디자인 패턴 중 가장 널리 쓰이는 옵저버 패턴(Observer Pattern)을 C++로 직접 구현해 보았다.

옵저버 패턴이란?

  • 주체(Subject)의 상태 변화를 관찰자(Observer)들에게 자동으로 통지하는 디자인 패턴이다.
  • 일대다(1:N) 의존 관계를 가지며, 클래스 간의 결합도를 낮춰(Loose Coupling) 유지보수와 확장성을 높이는 데 기여한다.
  • 이번 실습에서는 배송 회사(DeliveryService = Subject)의 배송 상태가 바뀔 때마다 등록된 고객들(Customer = Observer)에게 자동으로 알림이 가도록 설계했다.

2. 핵심 구현 포인트 & 코드 리뷰

💡 주요 개선 사항

  1. 다형성(Polymorphism) 활용: Customer 부모 클래스를 순수 가상 함수(update())를 가진 인터페이스로 만들고, 이를 상속받는 일반/VIP/비즈니스 고객 클래스들을 구현하여 각자 고유한 알림 메시지를 출력하도록 했다.
  2. 동적 삭제 처리: removeCustomer 구현 시, 단순히 맨 뒤 요소를 지우는 pop_back() 대신 반복자(Iterator)를 활용해 원하는 특정 고객 객체만 안전하게 리스트에서 제거하도록 수정했다.
  3. 가상 소멸자(Virtual Destructor): 다형성을 안전하게 활용하기 위해 부모 클래스(Customer)에 virtual ~Customer() {}를 추가하여 메모리 누수(Memory Leak)를 방지했다.

💻 최종 완성 코드

C++
 
#include <iostream>
#include <vector>
#include <string>

using namespace std;

// [Observer] 고객 인터페이스
class Customer {
public:
    virtual void update(const string& status) = 0; // 순수 가상 함수
    virtual ~Customer() {} // 가상 소멸자
};

// 일반 고객
class RegularCustomer : public Customer {
private:
    string name;
public:
    RegularCustomer(const string& name) : name(name) {}
    void update(const string& status) override {
        cout << "Regular customer " << name << " received update: " << status << endl;
    }
};

// VIP 고객
class VIPCustomer : public Customer {
private:
    string name;
public:
    VIPCustomer(const string& name) : name(name) {}
    void update(const string& status) override {
        cout << "VIP customer [" << name << "] received VIP update: " << status << endl;
    }
};

// 비즈니스 고객
class BusinessCustomer : public Customer {
private:
    string name;
public:
    BusinessCustomer(const string& name) : name(name) {}
    void update(const string& status) override {
        cout << "Business customer [" << name << "] received Business update: " << status << endl;
    }
};

// [Subject] 배송 회사 클래스
class DeliveryService {
private:
    vector<Customer*> customers; // 관찰자 명단
    string currentStatus;        // 현재 배송 상태

public:
    void addCustomer(Customer* customer) {
        customers.push_back(customer);
    }

    void removeCustomer(Customer* customer) {
        for (auto it = customers.begin(); it != customers.end(); ++it) {
            if (*it == customer) {
                customers.erase(it);
                break;
            }
        }
    }

    // 등록된 모든 고객(Observer)에게 전파
    void notifyCustomers() {
        for (Customer* customer : customers) {
            customer->update(currentStatus); 
        }
    }

    // 상태 변경 및 알림 트리거
    void updateStatus(string text) {
        currentStatus = text;
        notifyCustomers(); 
    }
};

int main() {
    DeliveryService service;

    Customer* customer1 = new RegularCustomer("Alice");
    Customer* customer2 = new VIPCustomer("Bob");
    Customer* customer3 = new BusinessCustomer("CompanyX");

    service.addCustomer(customer1);
    service.addCustomer(customer2);
    service.addCustomer(customer3);

    cout << "Updating status: 배송 준비 중" << endl;
    service.updateStatus("배송 준비 중");

    cout << "\nUpdating status: 배송 완료" << endl;
    service.updateStatus("배송 완료");

    // 메모리 해제
    delete customer1;
    delete customer2;
    delete customer3;

    return 0;
}

3. 실행 결과 (Result)

 

4. 오늘의 회고 (Retrospective)

  • 느낀 점: DeliveryService 클래스는 대소문자나 출력 형식이 다른 개별 고객 클래스들의 내부 사정을 전혀 몰라도, 오직 부모 타입인 Customer* 인터페이스만을 바라보고 통지할 수 있다는 점이 놀라웠다. 이래서 객체지향 프로그래밍에서 "인터페이스에 의존하라"고 하는지 몸소 깨달을 수 있었다.
  • 주의할 점: C++에서 상속과 포인터를 다룰 때는 반드시 부모 클래스의 가상 소멸자(virtual)를 잊지 말아야겠다. 또한 vector에서 특정 요소를 지울 때는 인덱스나 pop_back의 오용을 조심하고 iterator 기반으로 정확히 접근해야 안전하다는 것을 배웠다.