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>があなたの問題を解決するわけでもありません。

design-patternobserverpub-subjavaspring-boot