한 객체의 상태가 바뀌면 그것에 의존하는 객체들에게 자동으로 알리는 패턴.
interface Observer { void update(Event e); }
class Subject {
private final List<Observer> observers = new ArrayList<>();
void subscribe(Observer o) { observers.add(o); }
void unsubscribe(Observer o) { observers.remove(o); }
protected void notifyAll(Event e) {
for (Observer o : observers) o.update(e); // 누가 듣는지 모른 채 알린다
}
}
무엇을 얻나
발행자가 구독자를 몰라도 된다
- 구독자를 추가·제거해도 발행자 코드가 안 바뀐다 (OCP)
- 결합이 끊긴다
어디에 있나
-
UI 이벤트 리스너
-
반응형 스트림 (구독하면 값이 흐른다)
-
메시지 발행/구독
-
프레임워크의 애플리케이션 이벤트
-
MVC 의 모델 → 뷰 갱신
-
"리스너" · "콜백" · "구독" 이라는 말이 붙으면 대개 이 패턴이다
실무에서 데는 곳
-
① 구독 해제를 잊는다
- 발행자가 구독자 참조를 계속 들고 있어 메모리 누수
- 화면이 사라져도 리스너가 남는 것이 대표적
- 약한 참조를 쓰거나, 생명주기에 맞춰 자동 해제되게 만든다
-
② 순회 중 구독 해제
- notifyAll 도중에 목록이 바뀌면 ConcurrentModificationException
- 복사본을 순회하거나 CopyOnWriteArrayList 를 쓴다
-
③ 통지가 연쇄된다
- A 알림 → B 가 상태를 바꿈 → 또 알림 → ... 무한 루프
- 순환을 만들지 않도록 설계하거나 재진입을 막는다
-
④ 순서에 의존한다
- 구독 순서에 따라 결과가 달라지면 깨지기 쉽다
push와 pull
- push — 알릴 때 데이터를 함께 넘긴다 — 간단하지만 필요 없는 데이터도 보낸다
- pull — "바뀌었다" 만 알리고 구독자가 필요한 것을 가져간다 — 유연하지만 왕복이 는다
동기와 비동기
- 동기 통지 — 발행자 스레드에서 구독자가 실행된다
-
구독자 하나가 느리면 발행자가 막힌다
-
예외가 나면 발행자까지 영향을 받는다 비동기 통지 큐를 두고 다른 스레드에서 처리한다
-
격리되지만 순서·유실을 신경 써야 한다
-
면접 함정
- ❌ "Observer는 항상 비동기" → 기본은 동기 호출이다.
- ❌ "구독만 하면 끝" → 해제를 잊으면 누수다. 이게 실무 버그의 대부분이다.
이벤트 버스로 확장되면
Observer 를 전역화한 형태가 이벤트 버스다
- 장점 — 발행자와 구독자가 서로를 전혀 모른다
- 단점 — '누가 이 이벤트를 듣는지' 를 코드에서 추적할 수 없다
- 흐름이 사라진다. 디버깅이 어려워진다
경계: 모듈 안에서는 직접 호출, 모듈 경계에서만 이벤트
순서 보장이 필요하면
// 구독자에 우선순위를 준다
observers.sort(Comparator.comparingInt(Observer::order));
// 다만 순서에 의존하는 설계 자체가 냄새다
// → 순서가 중요하면 명시적 호출이나 파이프라인이 낫다
예외 처리 전략
for (Observer o : new ArrayList<>(observers)) { // 복사본을 순회한다
try { o.update(e); }
catch (Exception ex) { log.error("구독자 실패", ex); } // 하나가 실패해도 계속
}
구독자 하나의 실패가 나머지를 막지 않게 하는 것이 실무의 기본이다 — 다만 실패를 삼키기만 하면 조용히 동작이 빠지므로 반드시 로그를 남긴다.