ObserverとPub/Subの違いは?
1. 概要
ObserverとPub/Subは、「1つのイベントが発生して、複数の場所がそれを知る必要がある」という課題を解決します。多くの人がこの2つの用語を混同します。概念は似ていますが、実装方法とcouplingのレベルが異なります。この記事では、2つの概念を明確に区別し、各タイプのJavaコード例を示し、純粋なObserverを選ぶべき時期と、message brokerを使用したPub/Subを使うべき時期を理解するのに役立ちます。
2. Observer Patternとは
ObserverはGoFの古典的な行動パターン(behavioral pattern)の1つです。考え方:Subject(またはPublisher)はObserverのリストを保持し、stateが変わると、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()は順番に各observerのonOrderPaid()を呼び出し、同じスレッド内で同期的に実行します。Orderは正確に誰がそれをリッスンしているかを知っています—これがPub/Subとの区別の鍵です。
public class EmailNotifier implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
System.out.println("支払い済みOrderの確認メールを送信");
}
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
支払い済みOrderの確認メールを送信
2.1. Observerの本質的な特徴
- 直接的なcoupling:
SubjectはObserverへの参照を保持し、互いのインターフェースをインポートする必要があります。 - 同期的:
notify呼び出しはすべてのobserverが処理を完了するまでブロックします。 - 同じprocess内: 通常は1つのJVM内で実行され、ネットワークを経由しません。
- Subjectはobserverが存在することを知っている:
removeObserver()の呼び出しを忘れると、Subjectが使用されなくなったobserverへの「活きた」参照を保持しているため、メモリリークが発生しやすくなります。
注意: 古典的なObserverパターンには、安全なunsubscribeメカニズムが組み込まれていません。長期実行アプリケーション(UIコンポーネント、リスナーが登録されて破棄されるなど)で使用する場合は、オブジェクトが破棄されるときに
removeObserver()を呼び出すことを忘れずに、メモリリークを避けてください。
3. Pub/Subとは
Pub/Sub(Publish/Subscribe)は、PublisherとSubscriberを完全に分離し、broker、event bus、またはtopicと呼ばれる中間レイヤーを通じて通信します。Publisherは誰がsubscribeしているかを知らず、単にメッセージを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が存在するかどうかを知りません。- それは単にtopic
"order.paid"にpublishし、残りはbrokerの責任です。 - Subscriberはbrokerに登録し、
OrderServiceに直接登録するのではありません。
3.1. Spring EventsでのPub/Sub(同一process内)
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("Order " + event.getOrderId() + " にメールを送信");
}
}
説明:
OrderServiceはSpringが管理するbeanであるApplicationEventPublisherを通じてeventをpublishします—これは内部brokerの役割を果たします。EmailEventListenerはOrderServiceに認識されません。Springは@EventListenerを持つメソッドを自動的にスキャンし、イベントをタイプに基づいて正しいハンドラーにルーティングします。- デフォルトでは、
@EventListenerはpublisherと同じスレッドで同期的に実行されます—これは混乱しやすい点です。多くの人がPub/Subは常に非同期だと思っています。
真に非同期で実行するには、@Asyncを追加します:
@Component
public class EmailEventListener {
@Async
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("Order " + 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は異なる2つのサービス、異なる2つのプロセス、異なる2つのマシンにいることができます。
<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("メールサービスがorder " + event.orderId() + " を処理しています");
}
}
OrderServiceとEmailConsumerは、完全に異なる2つのmicroserviceに含まれ、独立してデプロイできます。- Kafkaはtopic内にメッセージを保持し、consumerは独自のペースで読み取ります—これは真のPub/Subで、バッファリング、リプレイ、グループ化された複数の独立したsubscriberがあります。
4. 直接比較
| 基準 | Observer | Pub/Sub |
|---|---|---|
| Coupling | Subjectは直接Observerを知っている(interfaceを通じて) | PublisherはSubscriberが存在することを知らない |
| 仲介者 | なし、メソッドを直接呼び出す | broker/event busが仲介 |
| 同期/非同期 | デフォルトは同期、同じスレッド | 同期(Spring Event)または非同期(Kafka、RabbitMQ)が可能 |
| 範囲 | 1つのprocess/module内 | Process内(event bus)または複数のservice/process間 |
| メッセージの永続性 | なし、Subjectが停止するとメッセージは失われる | 外部broker(Kafka)で保存可能、リプレイ対応 |
| Publisher-Subscriber数 | 通常1つのSubject - 複数のObserver | 複数のPublisher - 複数のSubscriber(複数のtopic) |
最も重要な違い:Observerは2つのオブジェクト間の直接的な関係であり、Pub/Subは匿名の仲介レイヤーを通じた関係です。本質的には、Pub/SubはObserverパターンをbrokerを挿入して直接的な依存関係を排除することで「緩くした」ものと考えることができます。
5. Observerを使う時期、Pub/Subを使う時期
Observerを使う場合:
- ロジックが同じ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を使う場合:
- 複数の独立したサービスが1つのイベントについて知る必要があるが、互いに依存すべきではない。
- publisherのコードを変更せずにsubscriberを追加/削除する機能が必要。
- メッセージの永続性が必要(subscriberがダウンしていても、メッセージはsubscriberがオンラインに戻るのを待つ)。
- システムがmicroservicesで、ネットワークを通じて通信する。
注意: 同じJVM内の
List<Observer>を置き換えるだけのためにKafka/RabbitMQを使用しないでください—brokerの運用コストは小さな問題には見合いません。一方、cross-serviceロジックを純粋なObserverパターンに無理やり詰め込まないでください。network、retry、serializationのメカニズム全体をbrokerが既に行っているので、自分で書き直さなければなりません。
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");
}
}
説明:
OrderObserverを実装するクラスではなくlambdaを使用します。interfaceには1つのメソッドしかないため、テストを書く時にはより簡潔です。assertThat(...).containsExactly(...)は、observerの呼び出し順序が登録順序と一致することを確認します—これはObserverの同期の重要な特性で、brokerを通じたPub/Subは保証しません(Kafkaは複数のconsumerインスタンスで並列処理できます)。
7. よくあるエラー
- Spring
@EventListenerを非同期と勘違いする: デフォルトは同期実行で、listenerのエラーはキャッチされなければpublisherに例外をスローします。 - Observerへの参照を保持したまま削除し忘れる: メモリリークが発生します。特に、オブジェクトのライフサイクルが短い(request-scope)のにsingletonに登録されているアプリケーションで。
- トランザクション一貫性が必要なロジックにPub/Subを誤用する: publisherとsubscriberが同時に成功するか同時に失敗する必要がある場合(atomic)、非同期Pub/Subは適切ではありません—Sagaパターンまたはtransactional outboxパターンを再検討してください。
- Topicの名前が不明確: 大規模なシステムでは、
"event1"のような曖昧な名前のtopicは、誰が何をsubscribeしているかを不明確にし、decoupleの利点を完全に失わせます。
8. 結論
ObserverとPub/Subは同じ目的を共有しています—変更を複数の利害関係者に通知することですが、couplingのレベルと動作範囲が異なります。Observerは直接的で同期的な関係に適しており、同じprocess内で機能します。Pub/Subは、publisherをsubscriberから完全にdecoupleする必要がある場合に適しており、特にメッセージの永続性と各consumerの独立したスケーラビリティが必要なmicroservicesシステムで有用です。この違いを明確に理解することで、正しいツールを選択できます。常にKafkaが必要なわけではありませんし、常にList<Observer>があなたの問題を解決するわけでもありません。