← Posts

GoF · Behavioral · Observer

Observer

상태가 바뀔 때마다 관심 있는 것들을 일일이 불러야 할 때. 구독한 여럿에게 자동으로 통지하는 행위 패턴.

목차
  1. 바뀔 때마다 일일이 알려야 한다
  2. 옵서버 = 구독하고 통지받는다
  3. Subject와 Observer
  4. 구독과 해지
  5. 통지
  6. 느슨한 결합
  7. 밀기 vs 당기기
  8. 실무: 스프링 이벤트와 리액티브
  9. 정리

바뀔 때마다 일일이 알려야 한다

주식 가격이 바뀌면, 그 가격을 보고 있던 여러 곳이 함께 갱신돼야 한다. 화면의 시세판, 목표가 알림, 로그 기록. 가격을 들고 있는 객체가 이걸 직접 챙기면 이렇게 된다.

java
public class Stock {
    private int price;
    private PriceDisplay display;
    private PriceAlert alert;
    private PriceLogger logger;

    public void setPrice(int price) {
        this.price = price;
        display.refresh(price);   // 화면 갱신
        alert.check(price);       // 알림 확인
        logger.log(price);        // 로그 기록
    }
}

문제가 보인다. Stock자기를 지켜보는 것들을 전부 알고 있다. 화면·알림·로그를 필드로 들고, 이름으로 하나하나 부른다. 여기에 “차트 위젯”을 하나 더 붙이려면 Stock을 열어 필드를 추가하고 setPrice에 호출을 끼워야 한다. 가격을 관리하는 객체가 자기와 상관없는 것들의 존재까지 떠안는 강한 결합이다.

옵서버 = 구독하고 통지받는다

옵서버 패턴이 이걸 푼다.

상태를 가진 객체(Subject)가 관심 있는 객체들(Observer)의 목록을 들고, 상태가 바뀌면 그 목록 전부에게 자동으로 통지한다.

가격을 보고 싶은 쪽이 “나도 알려줘”라고 구독을 걸어 두면, 가격이 바뀔 때 Subject가 구독자 전부에게 한 번에 알린다. Subject는 구독자를 이름으로 알 필요가 없다. 그저 목록에 대고 “바뀌었어”라고 외칠 뿐이다. 유튜브 채널을 구독해 두면 새 영상이 올라올 때 알림이 오는 것 - 그 구조다.

Subject와 Observer

역할을 둘로 나눈다.

  • Subject(주체) - 상태를 가진 쪽. 구독을 받고, 변화 시 통지한다. 여기선 Stock.
  • Observer(관찰자) - 통지를 받는 쪽. 알림이 오면 자기 할 일을 한다. 여기선 화면·알림·로그.

먼저 Observer의 규격을 인터페이스로 세운다.

java
public interface Observer {
    void update(int price);   // 통지를 받으면 호출된다
}

Subject는 구체적인 화면·알림·로그를 아는 게 아니라, 이 Observer 인터페이스만 안다. “가격이 바뀌면 update를 불러 주겠다”는 계약만 있으면 된다.

구독과 해지

Subject는 Observer 목록을 들고, 등록·해지 창구를 연다.

java
public class Stock {
    private final List<Observer> observers = new ArrayList<>();
    private int price;

    public void subscribe(Observer o) {
        observers.add(o);
    }

    public void unsubscribe(Observer o) {
        observers.remove(o);
    }
    // ...
}

관심 있는 쪽이 subscribe로 목록에 들어오고, 관심이 끝나면 unsubscribe로 빠진다. 구독자가 누구인지는 실행 중에 정해지고 언제든 바뀐다. Stock의 코드는 그대로다.

통지

상태가 바뀌면 목록을 돌며 전부에게 알린다.

java
public void setPrice(int price) {
    this.price = price;
    notifyObservers();
}

private void notifyObservers() {
    for (Observer o : observers) {
        o.update(price);   // 누구인지 모른 채, 계약대로 부른다
    }
}

각 Observer는 자기 방식대로 반응한다.

java
public class PriceDisplay implements Observer {
    public void update(int price) { /* 시세판 다시 그림 */ }
}
public class PriceAlert implements Observer {
    public void update(int price) {
        if (price >= target) notifyUser();   // 목표가 도달 시 알림
    }
}

쓰는 쪽은 구독만 걸어 두면 된다.

java
Stock stock = new Stock();
stock.subscribe(new PriceDisplay());
stock.subscribe(new PriceAlert());

stock.setPrice(72000);   // 구독자 전부에게 자동 통지
diagramdiagram

느슨한 결합

값어치는 결합에서 나온다. Stock이 아는 것은 Observer라는 인터페이스뿐이다. 화면인지 알림인지 로그인지, 몇 개나 붙어 있는지 모른다.

그래서 “차트 위젯”을 새로 붙여도 Stock한 줄도 고치지 않는다. Observer를 구현해 subscribe로 등록하기만 하면 된다. 통지하는 쪽과 통지받는 쪽이 인터페이스 하나로만 이어져, 서로의 변화에 흔들리지 않는다. 처음의 강결합이 여기서 풀린다.

밀기 vs 당기기

통지할 때 데이터를 얼마나 실어 보내느냐로 두 방식이 갈린다.

  • 밀기(push) - Subject가 바뀐 값을 통지에 실어 보낸다. 위 예제의 update(int price)가 그렇다. Observer는 받은 값을 바로 쓴다. 간단하지만, Subject가 “무엇을 보낼지” 미리 정해야 한다.
  • 당기기(pull) - Subject는 “바뀌었다”만 알리고, Observer가 필요한 값을 직접 물어온다.
java
void update(Stock stock) {          // 값 대신 Subject 자신을 넘긴다
    int price = stock.getPrice();   // 필요한 것만 당겨온다
}

당기기는 Observer마다 필요한 게 달라도 각자 알아서 가져오니 유연하다. 대신 Subject 참조를 넘겨야 해 결합이 조금 는다. 무엇이 옳다기보다, 통지에 데이터를 실을지 신호만 보낼지의 선택이다.

실무: 스프링 이벤트와 리액티브

옵서버는 프레임워크 곳곳에 녹아 있다. 자바의 이벤트 리스너(버튼 클릭에 리스너를 등록하는 것)가 전형적인 옵서버다.

Spring에서는 **ApplicationEvent@EventListener**가 이 패턴이다. 어떤 일이 벌어졌을 때 이벤트를 발행하면(publishEvent), 그 이벤트를 구독한 리스너들이 자동으로 호출된다.

java
// 발행하는 쪽 (Subject 역할)
publisher.publishEvent(new OrderPlacedEvent(order));

// 구독하는 쪽 (Observer 역할) - 서로를 직접 알지 못한다
@EventListener
public void onOrderPlaced(OrderPlacedEvent event) {
    // 메일 발송, 재고 차감 등
}

주문 서비스는 메일 서비스나 재고 서비스를 직접 부르지 않는다. 이벤트만 발행하고, 관심 있는 쪽이 알아서 반응한다. 발행자와 구독자가 서로를 모르는 이 느슨함이 옵서버의 핵심 그대로다. 리액티브 스트림(Publisher/Subscriber)도 같은 발상을 흐름 위에 얹은 것이다.

주의도 있다. 구독만 하고 해지하지 않으면 Subject가 죽은 Observer를 계속 붙들어 메모리 누수가 난다. 그리고 통지가 연쇄를 타면 흐름을 따라가기 어려워지니, 구독 관계가 지나치게 얽히지 않게 관리해야 한다.

정리

옵서버란상태 변화를 구독한 여럿에게 자동으로 통지한다
문제Subject가 관심 객체를 다 알고 직접 불러 강하게 결합된다
고침Subject는 Observer 인터페이스만 알고, 목록에 통지한다
구독subscribe/unsubscribe로 실행 중에 구독자가 드나든다
효과느슨한 결합 - 새 구독자를 붙여도 Subject를 안 고친다
밀기/당기기값을 실어 보내거나(push), 신호만 주고 당겨오게(pull)
실무이벤트 리스너, 스프링 ApplicationEvent/@EventListener, 리액티브

바뀌는 쪽은 목록에 외치기만 하고, 관심 있는 쪽이 구독해 자동으로 반응하는 것 - 그게 옵서버다.

관련 글