옵저버 패턴 가이드: 상태 변화에 느슨하게 반응하는 구조
Dev
마지막 업데이트

옵저버 패턴 가이드: 상태 변화에 느슨하게 반응하는 구조


많은 시스템에는 상태가 바뀌는 순간 여러 곳이 함께 반응해야 하는 중심 객체나 서비스가 있습니다.

예를 들어 주문이 결제 완료가 되면:

  • 사용자 알림을 보내고
  • 분석 지표를 업데이트하고
  • 감사 로그를 남기고
  • 대시보드를 새로고침해야 할 수 있습니다

이때 중심 객체가 이 후속 작업들을 전부 직접 알고 호출하기 시작하면 결합도가 빠르게 커집니다. 옵저버 패턴은 바로 이 압력을 줄이기 위해 자주 쓰이는 구조입니다.

이 글에서는 아래 내용을 다룹니다.

  • 옵저버 패턴이 정확히 무엇인지
  • 왜 상태 변화 알림 흐름에 잘 맞는지
  • callback이나 pub-sub와는 무엇이 다른지
  • 어떤 상황에서 잘 맞는지
  • 옵저버가 많아졌을 때 어떤 관리 비용이 생기는지

짧게 요약하면 이렇습니다. 옵저버 패턴은 상태를 가진 주체가 공통 계약을 통해 관심 있는 리스너들에게 변화를 알리게 해서, 중심 객체가 downstream 반응 세부를 덜 알도록 만드는 구조입니다.

왜 상태 변화 알림이 결합도 문제가 되는가

기능의 첫 버전은 대개 단순합니다.

주문 상태가 바뀌면 이메일 하나 보내면 끝일 수 있습니다.

하지만 시간이 지나면 같은 변화에 대해 추가로 해야 할 일이 늘어납니다.

  • 지표 업데이트
  • Slack 알림
  • 캐시 무효화
  • 결제 후속 처리

이 모든 것을 주문 서비스가 직접 알고 호출하기 시작하면 중심 서비스에 여러 관심사가 몰립니다.

그러면 이런 문제가 생깁니다.

  • 한 곳의 책임이 과도하게 커지고
  • 새 반응을 추가할 때마다 코어 코드를 수정해야 하고
  • 테스트가 점점 시끄러워지고
  • 상태 변화 흐름을 따라가기가 어려워집니다

옵저버 패턴은 중심 객체가 “무슨 변화가 일어났는지 알리는 책임”만 갖고, 구체적인 반응은 각 리스너에게 맡기게 해서 이 문제를 줄입니다.

옵저버 패턴이 정확히 무엇인가

높은 수준에서 보면 이 패턴은 세 가지 요소로 구성됩니다.

  • 상태 변화가 중요한 subject
  • 그 변화를 알고 싶어 하는 하나 이상의 observer
  • 둘을 잇는 알림 메커니즘

subject는 observer가 구독할 수 있는 방법을 제공하고, 의미 있는 변화가 생기면 공통 인터페이스를 통해 알립니다.

그래서 subject는 아래를 몰라도 됩니다.

  • 구체적인 observer 타입 각각
  • 각 observer가 내부에서 무슨 일을 하는지
  • 현재 observer가 몇 개인지

그저 “이 계약을 따르는 대상들에게 알린다”만 알면 됩니다.

실전 TypeScript 예시

주문 객체가 상태 변경 시 여러 리스너에게 알려야 하는 상황을 생각해 봅시다.

interface Observer {
  update(status: string): void;
}

class EmailNotifier implements Observer {
  update(status: string): void {
    console.log(`Send email for status: ${status}`);
  }
}

class AuditLogger implements Observer {
  update(status: string): void {
    console.log(`Write audit log: ${status}`);
  }
}

class Order {
  private observers: Observer[] = [];

  subscribe(observer: Observer): void {
    this.observers.push(observer);
  }

  changeStatus(status: string): void {
    this.observers.forEach((observer) => observer.update(status));
  }
}

const order = new Order();
order.subscribe(new EmailNotifier());
order.subscribe(new AuditLogger());
order.changeStatus('PAID');

핵심은 문법이 아니라 경계입니다.

  • order는 Observer 계약만 알고 있고
  • 새로운 반응은 order를 다시 쓰지 않고도 붙일 수 있습니다

이 지점에서 느슨한 결합이 생깁니다.

가장 중요한 감각

옵저버 패턴은 책임 경계로 보면 더 잘 이해됩니다.

subject의 책임은:

  • 의미 있는 변화가 일어났음을 알고
  • 관심 있는 observer들에게 알리는 것

각 observer의 책임은:

  • 그 알림을 받아 자기 일을 수행하는 것

이 분리가 중요한 이유는 subject가 “관련된 곳 다 호출하는 거대한 허브”가 되는 것을 막아 주기 때문입니다.

동시에 반응 로직이 시간이 지나며 더 독립적으로 자랄 수 있게 해 줍니다.

어떤 상황에서 특히 잘 맞나

하나의 변화가 여러 후속 동작으로 이어지는데, 중심 상태 소유자가 그 반응을 전부 하드코딩하지 않기를 원할 때 옵저버는 매력적입니다.

대표적인 예시는:

  • 모델 변경에 반응하는 UI 컴포넌트
  • 비즈니스 워크플로우 안의 도메인 이벤트
  • 알림 시스템
  • 감사 로그 훅
  • 메트릭 업데이트
  • 캐시 무효화 반응

리스너 집합이 시간이 지나며 늘어날 수 있을 때 특히 장점이 큽니다.

이게 옵저버 패턴의 큰 장점 중 하나입니다. 새로운 반응을 붙여도 subject 자체는 거의 바꾸지 않아도 되는 경우가 많습니다.

Callback, Pub-Sub와는 어떻게 다른가

이 개념들은 서로 닮아 있어서 초보자에게는 자주 섞여 보입니다.

Callback은 보통 나중에 실행할 행동을 직접 넘겨주는 방식입니다.

  • 한 함수가 다른 함수에 행동을 건네줍니다

Observer는 상태 변화 주위를 둘러싼 구독 관계에 더 가깝습니다.

  • 하나의 변화 원천에 여러 리스너가 붙을 수 있습니다

Publish-Subscribe는 비슷한 철학을 가지지만, 더 넓은 메시지 브로커나 채널 기반의 간접 계층이 들어가는 경우가 많습니다.

  • 발행자와 구독자가 서로를 아예 모를 수도 있습니다

초보자 관점에서 가장 중요한 구분은 이 정도입니다.

  • callback = 직접적인 행동 전달
  • observer = subject 중심의 구독 관계
  • pub-sub = 더 느슨한 메시지 분배 모델

실무에서는 이들이 섞일 수 있지만, 강조하는 구조는 다릅니다.

동기 옵저버와 비동기 반응

옵저버는 항상 비동기라고 오해하는 경우가 많습니다.

그렇지 않습니다.

옵저버는:

  • 즉시 알리는 동기식일 수도 있고
  • 큐를 거쳐 나중에 처리하는 비동기식일 수도 있습니다

이 선택은 아래에 영향을 줍니다.

  • 지연 시간
  • 실패 처리
  • 실행 순서 보장
  • 디버깅 난이도

동기 옵저버 흐름은 처음에는 더 이해하기 쉽습니다. 비동기 흐름은 더 확장 가능할 수 있지만, 운영 복잡도도 함께 올라갑니다.

그래서 옵저버 패턴은 커맨드 패턴 가이드이벤트 루프 가이드와도 의외로 잘 이어집니다.

옵저버가 결합도를 줄이는 방식

옵저버가 결합도를 완전히 없애는 것은 아닙니다. 결합의 모양을 바꿉니다.

패턴이 없으면 subject는 종종 아래를 알아야 합니다.

  • 어떤 downstream 서비스가 존재하는지
  • 각각 어떤 메서드를 노출하는지
  • 어떤 순서로 호출해야 하는지

옵저버를 쓰면 subject는 대개 아래만 알면 됩니다.

  • 구독자가 있다는 사실
  • 각 구독자가 알림 계약을 지원한다는 점

이 변화 덕분에 확장이 쉬워집니다.

나중에 새로운 observer가 추가돼도 subject는 전혀 바뀌지 않을 수 있습니다. 시스템이 커질수록 이런 차이가 꽤 크게 체감됩니다.

숨은 비용도 있다

옵저버는 강력하지만, 실패 패턴도 분명합니다.

observer 수가 많아질수록 팀은 종종 이런 문제를 겪습니다.

  • 제어 흐름이 눈에 잘 안 보이고
  • 예상치 못한 연쇄 부작용이 생기고
  • 중복 구독이 발생하고
  • unsubscribe 누락으로 메모리 누수가 생기고
  • 실행 순서 보장이 불명확해집니다

그래서 “낮은 결합도”가 곧 “이해하기 쉬움”을 뜻하는 것은 아닙니다.

간접 반응이 너무 많아지면, 소규모의 명시적 직접 호출보다 오히려 디버깅이 어려워질 수도 있습니다.

좋은 옵저버 사용은 알림 경계를 분명히 유지하는 것이지, 모든 걸 보이지 않는 부작용으로 바꾸는 것이 아닙니다.

언제는 쓰지 않는 편이 낫나

보통 아래 조건이면 옵저버 패턴이 굳이 필요 없을 수 있습니다.

  • downstream 반응이 하나로 안정적이고
  • 관계가 작고 거의 늘어날 가능성이 없고
  • 직접 호출이 더 명확하고 디버깅하기 쉽고
  • 간접화가 유연성보다 혼란을 더 키울 때

이때는 이런 질문이 유용합니다.

“이 상태 변화가 현실적으로 여러 독립 반응으로 커질 가능성이 있는가?”

아니라면 직접 조합이 더 깨끗할 수 있습니다.

자주 하는 실수

팀들이 옵저버 구조를 망가뜨리는 경우는 주로 이렇습니다.

  • 사소한 알림까지 전부 observer로 만드는 것
  • 부작용이 어디서 일어나는지 안 보이게 만드는 것
  • unsubscribe나 라이프사이클 정리를 놓치는 것
  • event 스타일 코드면 자동으로 좋은 설계라고 착각하는 것
  • observer, event bus, message queue를 같은 것으로 취급하는 것

또 다른 흔한 문제는 observer들이 다시 서로를 연쇄적으로 자극하면서, 겉으로는 느슨해 보여도 실제로는 깨지기 쉬운 거미줄 같은 흐름이 되는 것입니다.

빠르게 보는 체크리스트

옵저버 패턴을 고려할 만한 경우는 보통 이렇습니다.

  1. 하나의 상태 변화가 여러 후속 동작을 일으킨다
  2. 리스너 집합이 시간이 지나며 늘어날 수 있다
  3. 중심 subject가 concrete downstream 동작을 몰라야 한다
  4. 알림을 위한 공통 계약이 필요하다
  5. 직접 호출 때문에 한 모듈 책임이 너무 커지고 있다

대부분 해당하지 않으면 직접 호출이 더 단순할 수 있습니다.

FAQ

Q. 옵저버와 pub-sub는 같은 건가요?

완전히 같지는 않습니다. 철학은 비슷하지만, pub-sub는 더 넓은 메시지 분배 계층이 들어가는 경우가 많습니다.

Q. 옵저버는 항상 비동기인가요?

아닙니다. 시스템 설계에 따라 동기일 수도 있고 비동기일 수도 있습니다.

Q. 옵저버를 쓰면 항상 복잡도가 줄어드나요?

항상 그렇지는 않습니다. 어떤 결합은 줄지만, 너무 많은 간접 반응은 디버깅 난이도를 높일 수 있습니다.

Q. 초보자는 언제 먼저 떠올리면 좋을까요?

하나의 상태 변화가 여러 독립 후속 동작을 만들기 시작하고, 직접 호출 때문에 코어 모듈이 복잡해질 때입니다.

Q. 특히 무엇을 조심해야 하나요?

숨은 부작용과 구독 생명주기 문제, 예를 들면 중복 구독이나 cleanup 누락을 조심하는 것이 좋습니다.

먼저 읽어볼 가이드

검색 유입이 많은 핵심 글부터 이어서 보세요.

광고