소프트웨어 개발에서 ‘상태 변화에 따른 알림’과 ‘이벤트 처리’는 매우 흔한 요구사항입니다. 이를 구현하기 위해 우리는 옵저버(Observer) 패턴, 발행-구독(Pub/Sub) 패턴, 이벤트 리스너(Event Listener)와 같은 개념들을 사용합니다. 이들은 모두 ‘어떤 일이 발생하면, 관련된 다른 곳에 알려준다’는 공통점을 가지고 있어 종종 혼용되거나 혼동을 일으키곤 합니다.
하지만 성공적인 시스템 설계를 위해서는 이들의 미묘하지만 결정적인 차이를 명확히 이해해야 합니다. 이 글에서는 세 가지 개념의 정의, 구조, 그리고 핵심적인 차이점을 심도 있게 분석하여 언제 무엇을 사용해야 하는지에 대한 명확한 가이드를 제공합니다.
1. 옵저버(Observer) 패턴: 객체 간의 긴밀한 관찰
✅ 정의: 한 객체의 상태가 변할 때, 그 객체에 의존하는 다른 객체들에게 변화를 자동으로 통지하고 업데이트하는 디자인 패턴입니다.
가장 단순하게 표현하면, “내가 널 지켜보고 있다가, 변하면 바로 알려줘!” 라는 구조입니다.
🎯 핵심 구성 요소:
- Subject (주제, 관찰 대상): 상태 변화가 일어나는 객체입니다. 자신을 관찰하는 Observer들의 목록을 직접 관리합니다.
- Observer (관찰자): Subject의 상태 변화에 관심이 있는 객체입니다. Subject로부터 통지를 받으면 특정 동작을 수행합니다.
- Notify (알림): Subject가 상태 변경 시, 등록된 모든 Observer에게 변경 사실을 알리는 행위입니다.
🧵 구조와 예시 코드 (Java):
옵저버 패턴의 핵심은 Subject가 Observer 목록을 직접 알고 있고, 직접 호출한다는 점입니다.
// Observer 인터페이스 (관찰자의 규격)
interface Observer {
void update(String message);
}
// Subject 클래스 (관찰 대상)
class Subject {
// 자신을 관찰하는 Observer 목록을 직접 관리
private List<Observer> observers = new ArrayList<>();
// 관찰자 등록
public void addObserver(Observer obs) {
observers.add(obs);
}
// 등록된 모든 관찰자에게 직접 알림
public void notifyObservers(String message) {
for (Observer obs : observers) {
obs.update(message);
}
}
// 상태 변경을 유발하는 메서드
public void changeSomething() {
// ... 상태 변경 로직 ...
// 변경 후 즉시 알림
notifyObservers("상태가 변경되었습니다!");
}
}
- 주 사용처: 주로 단일 애플리케이션 내에서 객체 간의 상태를 동기화할 때 사용됩니다. (예: 모델(Model)의 데이터가 변경되면 뷰(View)가 자동으로 리프레시되는 MVC 패턴)
2. 발행-구독(Pub/Sub) 패턴: 익명의 메시지 중계
✅ 정의: 발행자(Publisher)가 특정 채널(토픽)에 메시지를 보내면, 해당 채널을 구독(Subscribe)하고 있는 모든 구독자(Subscriber)에게 메시지가 전달되는 비동기 메시징 패턴입니다.
핵심은 발행자와 구독자가 서로를 전혀 알지 못하며, 오직 ‘메시지 브로커(Broker)’라는 중재자를 통해 통신한다는 점입니다.
항목 | 옵저버 패턴 (Observer Pattern) | 발행-구독 패턴 (Pub/Sub Pattern) |
---|---|---|
연결 방식 | Subject가 Observer를 직접 참조 | Publisher와 Subscriber 간 직접적인 연결 없음 |
알림 전달 | Subject → Observer 직접 메서드 호출 | Publisher → Broker → Subscriber (메시지 중계) |
결합도 | 약한 결합 (Loosely Coupled) | 완전 분리 (Decoupled) |
주 통신 방식 | 동기적 (Synchronous) | 비동기적 (Asynchronous) |
구현 범위 | 주로 로컬 메모리 내 객체 간 통신 | 네트워크 기반의 분산 시스템 간 통신 |
대표 예시 | UI 버튼 클릭 이벤트 처리 | Kafka, Redis Pub/Sub, RabbitMQ, MQTT |
옵저버 패턴이 “내가 너에게 알려줄게”라면, Pub/Sub 패턴은 “관심 있는 사람은 여기서 소식 가져가”에 가깝습니다. 이러한 완전한 분리 덕분에 MSA(마이크로서비스 아키텍처)와 같은 분산 환경에서 서비스 간의 의존성을 최소화하는 데 매우 효과적입니다.
3. 이벤트 리스너(Event Listener): 특정 이벤트에 대한 응답
✅ 정의: 특정 이벤트(예: 클릭, 키 입력, 메시지 도착)가 발생했을 때 실행되도록 등록된 콜백(Callback) 함수 또는 메서드입니다.
이벤트 리스너는 옵저버 패턴을 구현하는 구체적인 방법 중 하나로 볼 수 있습니다. “이벤트가 발생하면 이 함수를 실행해줘”라고 약속하는 방식입니다.
✅ 예시 (JavaScript):
// 'button' 요소에서 'click' 이벤트가 발생하면,
// 등록된 콜백 함수가 실행된다.
const button = document.getElementById('myButton');
button.addEventListener('click', () => {
console.log('버튼이 클릭되었습니다!');
});
- 주 사용처: UI 프레임워크(React, Vue), 브라우저 DOM, Node.js의 EventEmitter 등 이벤트 기반 시스템에서 널리 사용됩니다. 내부적으로는 옵저버 패턴과 유사한 구조를 가집니다.
핵심 차이 요약 및 선택 가이드
개념 | 주 대상 | 실행 시점 | 구조적 특징 | 선택해야 할 때 |
---|---|---|---|---|
옵저버 패턴 | 코드 내부 객체 | 즉시, 동기적 | 직접 참조 및 호출 | 한 애플리케이션 내에서 객체 간 상태를 동기화하고 싶을 때 |
이벤트 리스너 | UI/이벤트 객체 | 이벤트 발생 시, 비동기적 | 콜백 함수 등록 | 사용자의 상호작용이나 특정 시스템 이벤트에 반응해야 할 때 |
Pub/Sub 패턴 | 마이크로서비스, 분산 시스템 | 비동기, 메시지 큐잉 | 중간 브로커를 통한 완전 분리 | 여러 독립적인 시스템 간에 데이터를 안정적으로 전달하고 싶을 때 |
장점과 단점
- 장점:
- 느슨한 결합(Low Coupling): 각 컴포넌트의 독립성이 높아져 유지보수와 확장이 용이합니다.
- 관심사 분리(Separation of Concerns): 상태를 변경하는 로직과 그 변화에 반응하는 로직을 명확히 분리할 수 있습니다.
- 단점:
- 디버깅의 어려움: 특히 Pub/Sub 패턴에서는 메시지가 어디서 왔고 어디로 가는지 흐름을 추적하기 어려울 수 있습니다.
- 이벤트 지옥(Event Hell): 너무 많은 이벤트와 리스너가 복잡하게 얽히면, 코드의 흐름을 이해하기 매우 어려워집니다.
결론적으로, 이 세 가지 개념은 ‘알림’이라는 공통된 목표를 가지지만, 그 대상과 구조, 통신 방식에서 명확한 차이를 보입니다. 만들고자 하는 시스템의 범위(단일 앱인가, 분산 시스템인가)와 통신 방식(동기인가, 비동기인가)을 고려하여 가장 적합한 패턴을 선택하는 것이 성공적인 아키텍처 설계의 첫걸음입니다.