Observer 패턴과 Pub/Sub의 차이점
1. 개요
Observer와 Pub/Sub는 모두 ’이벤트가 발생하면 여러 곳에서 그것을 알아야 한다’는 문제를 해결합니다. 많은 개발자가 두 용어를 혼용하는데, 아이디어는 비슷하지만 구현 방식과 coupling 수준에서는 다릅니다. 이 글은 두 개념을 명확히 구분하고, 각각에 대한 Java 코드 예제를 제공하며, 순수한 Observer를 선택해야 할 때와 Pub/Sub를 message broker로 구현해야 할 때를 알려줍니다.
2. Observer 패턴이란
Observer는 GoF의 고전적인 행동 패턴(behavioral pattern) 중 하나입니다. 개념은 다음과 같습니다: Subject(또는 Publisher)가 Observer 목록을 관리하고, 상태가 변경되면 Subject가 등록된 각 Observer의 메서드를 직접 호출합니다.
public interface OrderObserver {
void onOrderPaid(Order order);
}
public class Order {
private final List<OrderObserver> observers = new ArrayList<>();
private String status = "NEW";
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void markAsPaid() {
this.status = "PAID";
for (OrderObserver observer : observers) {
observer.onOrderPaid(this);
}
}
}
설명:
observers는 관심 있는 객체들에 대한 직접적인 참조 목록입니다.markAsPaid()는 같은 thread 내에서 각 observer의onOrderPaid()를 순서대로 동기적으로 호출합니다.Order는 정확히 누가 자신을 구독하고 있는지 알고 있습니다 — 이것이 Pub/Sub와 구분하는 핵심 포인트입니다.
public class EmailNotifier implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
System.out.println("주문 결제 확인 이메일 발송");
}
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
주문 결제 확인 이메일 발송
2.1. Observer의 핵심 특징
- 직접 Coupling: Subject가 Observer에 대한 참조를 유지하며, 서로의 interface를 import해야 합니다.
- 동기: notify 호출은 모든 observer의 처리가 완료될 때까지 차단됩니다.
- 같은 프로세스 내: 일반적으로 하나의 JVM 내에서 실행되며, 네트워크를 거치지 않습니다.
- Subject가 observer를 알고 있음: removeObserver()를 호출하지 않으면, Subject가 더 이상 사용하지 않는 observer에 대한 ‘활성’ 참조를 유지하기 때문에 메모리 누수가 발생할 수 있습니다.
주의: 고전적인 Observer 패턴은 안전한 unsubscribe 메커니즘이 기본으로 제공되지 않습니다. 오래 실행되는 애플리케이션(예: UI 컴포넌트, listener 등록 후 제거)에서 사용하는 경우, 객체가 소멸할 때
removeObserver()를 호출하여 메모리 누수를 방지해야 합니다.
3. Pub/Sub란
Pub/Sub(Publish/Subscribe)는 Publisher와 Subscriber를 중간 계층(broker, event bus 또는 topic이라고 부름)을 통해 완전히 분리합니다. Publisher는 누가 구독하고 있는지 알지 못하고, 단지 topic이나 channel에 메시지를 ’발행’할 뿐입니다.
public record OrderPaidEvent(String orderId, double amount) {}
public class OrderService {
private final EventBus eventBus;
public OrderService(EventBus eventBus) {
this.eventBus = eventBus;
}
public void payOrder(String orderId, double amount) {
// 결제 처리...
eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
}
}
설명:
OrderService는EmailNotifier나InventoryService가 존재하는지 알지 못합니다.order.paid토픽에 발행하기만 하고, 나머지는 broker의 책임입니다.- Subscriber는 broker에 등록하며,
OrderService에 직접 등록하지 않습니다.
3.1. Spring Events를 이용한 프로세스 내 Pub/Sub
Spring Boot는 ApplicationEventPublisher를 사용하는 간단한 event bus를 기본으로 제공합니다. 이것은 애플리케이션 수준의 Pub/Sub이며, Kafka/RabbitMQ 같은 외부 broker가 필요하지 않습니다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
public class OrderPaidEvent extends ApplicationEvent {
private final String orderId;
public OrderPaidEvent(String orderId) {
super(orderId);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void payOrder(String orderId) {
publisher.publishEvent(new OrderPaidEvent(orderId));
}
}
@Component
public class EmailEventListener {
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("주문 " + event.getOrderId() + "에 이메일 발송");
}
}
설명:
OrderService는ApplicationEventPublisher를 통해 event를 발행합니다 — Spring이 관리하는 bean으로, 내부 broker의 역할을 합니다.EmailEventListener는OrderService가 알지 못합니다. Spring이@EventListener가 있는 메서드를 자동으로 스캔하고 type에 따라 올바른 handler로 event를 라우팅합니다.- 기본적으로
@EventListener는 publisher와 같은 thread에서 동기적으로 실행됩니다 — 이것은 Pub/Sub가 항상 비동기라고 생각하는 사람들이 혼동하기 쉬운 부분입니다.
진정한 비동기 실행을 원한다면 @Async를 추가하세요:
@Component
public class EmailEventListener {
@Async
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("주문 " + event.getOrderId() + "에 이메일 발송");
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
@EnableAsync는 proxy를 활성화하여 Spring이@Async메서드를 별도의 thread pool에서 실행하도록 합니다.- 이제
payOrder()는 즉시 반환되며,EmailEventListener의 처리가 완료될 때까지 기다리지 않습니다.
3.2. Message Broker를 통한 Pub/Sub (Kafka)
더 큰 규모에서는 ’broker’가 더 이상 JVM 내의 bean이 아니라 독립적인 시스템(Kafka, RabbitMQ, SQS 등)입니다. Publisher와 subscriber는 서로 다른 service, process, 또는 다른 머신에 있을 수 있습니다.
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
@Service
public class OrderService {
private final KafkaTemplate<String, OrderPaidEvent> kafkaTemplate;
public OrderService(KafkaTemplate<String, OrderPaidEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void payOrder(String orderId, double amount) {
kafkaTemplate.send("order-paid-topic", new OrderPaidEvent(orderId, amount));
}
}
@Component
public class EmailConsumer {
@KafkaListener(topics = "order-paid-topic", groupId = "email-service")
public void consume(OrderPaidEvent event) {
System.out.println("이메일 서비스가 주문 " + event.orderId() + " 처리");
}
}
OrderService와EmailConsumer는 완전히 다른 microservice에 있을 수 있으며, 독립적으로 deploy됩니다.- Kafka는 메시지를 토픽에 보관하고 consumer가 자신의 속도로 읽습니다 — 이것이 진정한 Pub/Sub이며, buffer, replay, 그룹별 독립적인 여러 subscriber를 지원합니다.
4. 직접 비교
| 기준 | Observer | Pub/Sub |
|---|---|---|
| Coupling | Subject가 Observer를 직접 알고 있음 (interface를 통해) | Publisher가 Subscriber의 존재를 모름 |
| 중간 계층 | 없음, 메서드 직접 호출 | broker/event bus가 중간에 있음 |
| 동기/비동기 | 기본적으로 동기, 같은 thread | 동기(Spring Event) 또는 비동기(Kafka, RabbitMQ) 가능 |
| 범위 | 하나의 process/module 내 | process 내(event bus) 또는 여러 service/process에 걸쳐 |
| 메시지 지속성 | 없음, Subject가 죽으면 메시지도 손실됨 | 외부 broker(Kafka)가 저장 가능, replay 허용 |
| Publisher-Subscriber 수 | 일반적으로 1 Subject - 여러 Observer | 여러 Publisher - 여러 topic을 통한 여러 Subscriber |
가장 중요한 차이점: Observer는 두 객체 간의 직접적인 관계이고, Pub/Sub는 익명의 중간 계층을 통한 관계입니다. 본질적으로 Pub/Sub는 broker를 삽입하여 직접적인 의존성을 제거함으로써 Observer 패턴이 ‘느슨해진’ 버전이라고 볼 수 있습니다.
5. Observer와 Pub/Sub: 언제 사용할 것인가
Observer를 사용하는 경우:
- logic이 같은 module/class 내에 있고, observer의 수가 고정되어 거의 변경되지 않는 경우
- 즉각적인 동기 응답이 필요한 경우(예: 저장 전 데이터 검증)
- 과도한 decoupling이 필요 없는 경우 — 예를 들어 UI 컴포넌트가 model의 변경을 감시하는 경우
class ValidationObserver implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
if (order.getStatus() == null) {
throw new IllegalStateException("Order에 유효한 status가 없습니다");
}
}
}
Pub/Sub를 사용하는 경우:
- 여러 독립적인 service가 하나의 이벤트를 알아야 하지만 서로 의존하면 안 되는 경우
- publisher의 code를 변경하지 않고 subscriber를 추가/제거할 수 있어야 하는 경우
- 메시지 지속성이 필요한 경우(subscriber가 다운되어 있어도 메시지가 기다렸다가 online이 될 때 전달)
- 시스템이 microservices이고 네트워크를 통해 통신하는 경우
주의: 같은 JVM 내의
List<Observer>를 대체하기 위해서만 Kafka/RabbitMQ를 사용하지 마세요 — broker 운영 비용이 작은 문제에는 맞지 않습니다. 반대로 cross-service logic을 순수한 Observer 패턴에 밀어넣으려고 하지 마세요. 왜냐하면 broker가 이미 제공하는 network, retry, serialization 메커니즘을 직접 다시 구현해야 하기 때문입니다.
6. Observer 행동 테스트
class OrderObserverTest {
@Test
void shouldNotifyAllObserversWhenOrderIsPaid() {
Order order = new Order();
List<String> notified = new ArrayList<>();
order.addObserver(o -> notified.add("email"));
order.addObserver(o -> notified.add("inventory"));
order.markAsPaid();
assertThat(notified).containsExactly("email", "inventory");
}
}
설명:
- interface가 메서드 하나만 가지고 있으므로
OrderObserver를 구현하는 class 대신 lambda를 사용합니다 — test를 작성할 때 더 간결합니다. assertThat(...).containsExactly(...)는 observer 호출 순서가 등록 순서와 일치하는지 확인합니다 — 이것은 동기 Observer의 중요한 특징이며, broker를 통한 Pub/Sub는 보장할 수 없습니다(Kafka는 여러 consumer instance에서 병렬로 처리할 수 있음).
7. 자주 발생하는 실수
- Spring @EventListener를 비동기라고 착각하기: 기본적으로 동기적으로 실행되고, listener의 오류는 처리되지 않으면 publisher로 exception이 전파됩니다.
- Observer 참조를 유지하면서 제거를 잊음: 메모리 누수 발생, 특히 객체 생명주기가 짧은(request-scope) 애플리케이션에서 singleton에 등록한 경우
- 일관된 transaction이 필요한 logic에 Pub/Sub을 남용하기: publisher와 subscriber가 함께 성공하거나 함께 실패해야 하는(atomic) 경우, 비동기 Pub/Sub는 적합하지 않습니다 — Saga pattern이나 transactional outbox를 다시 검토하세요.
- Topic 이름을 명확하게 지정하지 않기: 큰 시스템에서
"event1"같은 모호한 이름의 topic을 사용하면 누가 무엇을 구독하는지 알 수 없어, decoupling의 모든 이점을 잃게 됩니다.
8. 결론
Observer와 Pub/Sub는 같은 목표를 공유합니다 — 여러 이해관계자에게 변경을 알림 — 하지만 coupling 수준과 작동 범위가 다릅니다. Observer는 직접적이고 동기적인 관계, 같은 process 내에서 적합합니다. Pub/Sub는 publisher와 subscriber를 완전히 분리해야 하고, 특히 메시지 지속성과 각 consumer별 독립적인 확장성이 필요한 microservices 시스템에 적합합니다. 이 차이를 명확히 이해하면 올바른 도구를 선택하는 데 도움이 됩니다: Kafka가 항상 필요한 것은 아니고, List<Observer>가 모든 문제의 해결책도 아닙니다.