개발

[디자인 패턴] 3. 옵저버 패턴

2025년 08월 22일
50

지난 글까지 우리는 전략 패턴으로 핵심 로직을 유연하게 만들고, 팩토리 패턴으로 객체 생성의 책임을 분리했습니다. 이제 결제 시스템은 꽤 견고해 보입니다.

하지만 실무에서는 "결제 완료"라는 핵심 기능이 끝난 직후에 수많은 부가 기능들이 따라붙곤 합니다. 기획팀에서 이런 요청들이 들어오기 시작하죠.

결제 성공하면 사용자에게 이메일 보내주세요."
"아, 재고 시스템에도 수량 차감 요청 보내야 하고요."
"마케팅팀 슬랙 채널에 알림도 띄워주세요.

배보다 배꼽이 더 커지는 코드

이 요구사항들을 있는 그대로 구현하면, 우리의 소중한 PaymentProcessor는 다시 지저분해지기 시작합니다.

public class PaymentProcessor {

    private PaymentStrategy strategy;
    private EmailService emailService;
    private InventoryService inventoryService;
    private SlackService slackService;
    private AnalyticsService analyticsService;

    public PaymentProcessor(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void executePayment(int amount) {
        // 1. 핵심 결제 로직
        strategy.pay(amount);

        // 2. 덕지덕지 붙는 후속 처리들
        emailService.sendEmail("결제 성공!");
        inventoryService.decreaseStock();
        slackService.postMessage(amount + "원 결제 발생!");
        analyticsService.trackEvent("purchase");
        // 새로운 요구사항이 올 때마다 계속 수정됨
    }
}

결제 로직은 하나인데, 그 뒤에 붙은 후속 처리가 4~5개가 넘어갑니다. 문제는 여기서 발생합니다. 만약 '슬랙 알림' 기능에 버그가 생겨서 에러가 나면, 잘 수행된 결제 로직까지 롤백되어야 할까요? 아니면 이메일 서비스가 잠깐 죽었다고 결제가 안 되어야 할까요?

핵심 비즈니스 로직과 부가적인 후속 처리는 서로 강하게 결합되어선 안 됩니다. 이때 필요한 것이 바로 **옵저버 패턴(Observer Pattern)**입니다.

"구독과 좋아요, 알림 설정까지"

옵저버 패턴의 원리는 유튜브 구독 시스템과 같습니다. 유튜버(Subject)가 영상을 올리면, 구독자(Observer)들에게 자동으로 알림이 갑니다. 유튜버는 누가 구독했는지 구체적으로 알 필요가 없습니다. 그저 "새 영상이 올라왔다"고 외치기만 하면 되죠.

코드로 구현해 보겠습니다.

1. 구독자(Observer) 인터페이스 정의

이벤트가 발생했을 때 행동할 구독자들의 공통 규약입니다.

public interface Observer {
    void update(PaymentEvent event);
}

실무 Java에서는 Object 대신 의미 있는 이벤트 객체를 사용하는 것이 일반적입니다.

public class PaymentEvent {

    private final int amount;
    private final String status;

    public PaymentEvent(int amount, String status) {
        this.amount = amount;
        this.status = status;
    }

    public int getAmount() {
        return amount;
    }

    public String getStatus() {
        return status;
    }
}

2. 구체적인 구독자들 구현

이제 후속 처리 로직들을 각각의 클래스로 나눕니다.

// 이메일 발송
public class EmailSender implements Observer {

    @Override
    public void update(PaymentEvent event) {
        System.out.println(
            "[이메일 발송] " + event.getAmount() + "원 결제 내역을 고객에게 전송합니다."
        );
    }
}

// 재고 관리
public class InventoryManager implements Observer {

    @Override
    public void update(PaymentEvent event) {
        System.out.println("[재고 관리] 상품 재고를 차감합니다.");
    }
}

// Slack 알림
public class SlackNotifier implements Observer {

    @Override
    public void update(PaymentEvent event) {
        System.out.println(
            "[Slack 알림] 관리자 채널에 결제 정보를 공유합니다."
        );
    }
}

3. 발행자(Subject) 구현

이제 PaymentProcessor에 구독자 관리 기능을 추가합니다.

class PaymentProcessor {
  private observers: Observer[] = [];

  // 구독자 등록 (구독)
  import java.util.ArrayList;
import java.util.List;

public class PaymentProcessor {

    private final PaymentStrategy strategy;
    private final List<Observer> observers = new ArrayList<>();

    public PaymentProcessor(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    // 구독
    public void subscribe(Observer observer) {
        observers.add(observer);
    }

    // 구독 취소
    public void unsubscribe(Observer observer) {
        observers.remove(observer);
    }

    // 알림 전파
    private void notifyObservers(PaymentEvent event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }

    public void executePayment(int amount) {
        // 1. 핵심 결제 로직
        strategy.pay(amount);

        // 2. 후속 처리는 옵저버에게 위임
        notifyObservers(new PaymentEvent(amount, "SUCCESS"));
    }
}

확장이 얼마나 쉬워졌을까?

이제 클라이언트 코드는 이렇게 변합니다.

public class PaymentClient {

    public static void main(String[] args) {

        PaymentProcessor processor =
                new PaymentProcessor(new CreditCardStrategy());

        // 레고 조립하듯 기능 추가
        processor.subscribe(new EmailSender());
        processor.subscribe(new InventoryManager());
        processor.subscribe(new SlackNotifier());

        // 결제 실행
        processor.executePayment(30_000);
    }
}

만약 내일 기획팀이 "이제 슬랙 알림은 끄고, 카카오톡 알림을 추가해주세요"라고 한다면? PaymentProcessor 코드는 단 한 줄도 수정할 필요가 없습니다. 그저 SlackNotifier 구독을 빼고 KakaoTalkNotifiersubscribe 해주면 끝입니다.

이 패턴은 Node.js의 EventEmitter나 프론트엔드의 Redux, RxJS 같은 라이브러리의 근간이 되는 매우 중요한 개념입니다.

마치며

옵저버 패턴을 통해 우리는 핵심 로직(결제)과 부가 로직(알림, 재고 등)의 결합도를 끊어냈습니다. 이제 시스템은 서로에게 영향을 주지 않으면서 각자의 역할을 수행합니다.

그런데 코드를 작성하다 보니, DatabaseConnection이나 ConfigManager 처럼 시스템 전반에서 딱 하나만 존재해야 하고, 어디서든 공유해서 써야 하는 객체들이 보이기 시작합니다. 이걸 매번 new로 생성하면 메모리 낭비이자 데이터 불일치가 발생할 텐데요.

다음 글에서는 애플리케이션 전체에서 단 하나의 인스턴스만 존재하도록 보장하는 **싱글톤 패턴(Singleton Pattern)**에 대해 알아보겠습니다.


댓글을 불러오는 중...