← 프리뷰 목록으로

Postgres LISTEN/NOTIFY를 위한 Notifier 패턴

게시일: 2025년 11월 22일 | 원문 작성일: 2024년 5월 5일 | 저자: Brandur Leach | 원문 보기

핵심 요약

  • Notifier 패턴은 프로그램당 단 하나의 Postgres 연결로 여러 토픽을 구독하고 알림을 분배하는 효율적인 아키텍처예요
  • 단순한 접근(토픽당 연결)과 달리, 연결 수를 극적으로 줄여서 Postgres의 소중한 연결 리소스를 절약해요
  • PgBouncer와 함께 쓸 때: LISTEN용 단 하나의 세션 연결만 유지하고, 나머지 앱은 모두 트랜잭션 풀링 모드를 활용할 수 있어요
  • 버퍼링된 채널과 non-blocking send로 graceful degradation을 구현 - 느린 구독자가 전체를 멈추지 못하게 해요
  • 실전 활용: 피처 플래그 즉시 반영, 실시간 알림, 작업 큐 wake-up (River 같은 곳에서 실제로 사용 중)

문제: LISTEN/NOTIFY는 연결을 아껴야 해요

Postgres의 __CODE_UNDER_0__/__CODE_UNDER_1__는 아주 강력한 pub/sub 기능이에요. 하지만 단순하게 구현하면 곧 문제에 부딪혀요.

기본 원리:

  • 하나의 연결로 여러 토픽을 LISTEN 할 수 있어요
  • Postgres 연결은 여전히 소중하고 제한적인 리소스라, 아껴야 해요
  • LISTEN은 특정 세션에 바인딩되어 있어서, 알림은 그 세션에만 전달돼요

단순한 접근은 “토픽당 하나의 연결”이에요. 앱에서 3개의 토픽을 구독하면? 3개의 연결. 10개 서버가 돌고 있으면? 30개의 연결. 이건 금방 비효율적으로 변해요.

해결책: Notifier 패턴

Notifier의 역할은 이거예요:

  • 프로세스당 단 하나의 Postgres 연결을 유지해요
  • 프로그램 안의 여러 컴포넌트가 이 연결로 원하는 만큼의 토픽을 구독할 수 있게 해줘요
  • 알림을 기다렸다가, 받으면 각 구독자에게 분배해줘요
__MERMAID_UNDER_0__

이 패턴을 쓰면 프로그램당 연결 수가 1개로 고정돼요. 토픽이 100개든, 구독자가 50개든 상관없이요.

구현 디테일

1. 토픽당 한 번만 LISTEN

연결이 하나니까, Notifier는 각 토픽에 대해 딱 한 번만 __CODE_UNDER_2__을 보내면 돼요.

내부적으로 Notifier는 구독을 토픽별로 정리해요. 새 구독 요청이 오면:

  • 토픽이 이미 LISTEN 중이면? → 그냥 구독자 리스트에 추가만 해요
  • 처음 보는 토픽이면? → __CODE_UNDER_3__ 보내고 구독자 추가해요

2. 버퍼링된 채널과 Non-blocking Send

구독자는 버퍼링된 채널을 받아요 (예: Go의 __CODE_UNDER_4__).

알림이 들어오면 Notifier는 non-blocking send로 채널에 넣어요:

__FENCED_UNDER_0__

왜 이렇게 할까요? 느린 구독자 하나가 전체를 막지 못하게 하려는 거예요. 버퍼가 꽉 차면 알림을 버리는 게, 모든 구독자를 블락하는 것보다 나아요.

접근 방식 Blocking Send Non-blocking Send (Notifier)
느린 구독자 있을 때 모든 구독자가 멈춰요 그 구독자만 알림 누락, 나머지는 정상
장점 알림 절대 안 잃어버려요 시스템 전체가 견고해요 (graceful degradation)
단점 단 하나의 느린 구독자가 DoS처럼 작동 구독자가 처리 못 하면 알림 누락 가능

버퍼 크기는 튜닝 가능해요. 너무 작으면 쉽게 누락되고, 너무 크면 메모리 낭비예요. 보통 100~1000 정도가 합리적이에요.

3. Interruptible Receives

Notifier는 알림을 기다리면서도 종료 가능해야 해요. 실전에서는 타임아웃을 걸어요:

__FENCED_UNDER_1__

이렇게 하면:

  • 알림이 오면 즉시 처리해요
  • 30초 동안 아무것도 안 오면 루프를 돌면서 종료 신호를 체크할 수 있어요
  • Graceful shutdown이 가능해져요

PgBouncer 호환성

여기서 중요한 포인트가 하나 있어요: PgBouncer는 LISTEN과 잘 안 맞아요.

왜냐면:

  • __CODE_UNDER_5__은 세션에 바인딩되어 있어요
  • PgBouncer의 트랜잭션 풀링 모드는 요청마다 다른 백엔드 연결을 쓸 수 있어요
  • 그래서 __CODE_UNDER_6__은 세션 풀링 모드에서만 작동해요

하지만 Notifier 패턴을 쓰면 양쪽의 장점을 다 누려요:

하이브리드 아키텍처

  • LISTEN용 연결 1개: PgBouncer 없이 직접 Postgres로 (또는 세션 풀링 모드)
  • 나머지 모든 앱 트래픽: PgBouncer 트랜잭션 풀링 모드로 효율적으로

앱에서 LISTEN 전용 연결 하나를 Notifier가 관리하고, 나머지는 전부 PgBouncer를 통해 트랜잭션 풀링 쓰면 돼요. 최고의 연결 효율성을 얻는 거죠.

__MERMAID_UNDER_1__

실전 활용 사례

1. 피처 플래그 즉시 반영

인프로세스 캐시로 피처 플래그를 관리하면 빠르지만, 업데이트 반영이 늦어질 수 있어요.

Notifier 패턴으로 해결:

  • 피처 플래그 테이블에 트리거를 걸어요
  • INSERT/UPDATE/DELETE 시 __CODE_UNDER_7__ 보내기
  • 앱의 Notifier가 알림 받으면 → 즉시 캐시 무효화

결과: 밀리초 안에 모든 서버로 플래그 변경이 전파돼요. 캐시의 속도 + 즉각적인 업데이트.

2. 작업 큐 Wake-up (River)

Brandur가 만든 River job queue가 실제로 이 패턴을 써요.

작동 방식:

  • 새 작업이 INSERT되면 → 트랜잭션 안에서 __CODE_UNDER_8__ 보내기
  • 워커 풀이 Notifier로 알림 받음
  • 즉시 워커가 깨어나서 작업을 처리

장점: Postgres의 NOTIFY는 트랜잭션 안에서 동작해요. 작업이 커밋되는 순간 워커가 깨어나요. 폴링 없이, 즉시요.

3. 실시간 알림

채팅 앱, 협업 툴, 대시보드 같은 곳에서:

  • 데이터 변경 시 __CODE_UNDER_9__ 보내기
  • WebSocket 서버가 Notifier로 받아서 클라이언트에 푸시

폴링이나 외부 메시지 브로커 없이, Postgres만으로 실시간성을 얻어요.

테스트 팁

Notifier를 테스트할 때 타이밍 이슈를 조심해야 해요:

__FENCED_UNDER_2__

Ready 채널을 기다리지 않으면 race condition이 생겨요. NOTIFY가 LISTEN보다 먼저 보내지면 알림을 놓쳐버려요.

구현 참고

직접 구현하고 싶다면:

마무리

Notifier 패턴은 단순하지만 강력한 아이디어예요:

  • 연결 효율성: 프로그램당 1개 연결로 무한대 토픽 구독
  • PgBouncer 호환: LISTEN 1개 + 나머지는 트랜잭션 풀링
  • Graceful degradation: Non-blocking send로 견고성 확보
  • 실시간성: 폴링 없이 밀리초 단위 반응

Postgres를 쓰고 있고 실시간 알림이 필요하다면, 이 패턴을 고려해보세요. 이미 갖고 있는 인프라로 pub/sub을 얻는 거예요.

저자 소개: Brandur Leach는 Crunchy Data의 엔지니어이자, River job queue의 공동 창시자예요. Postgres와 Go 생태계에서 오랫동안 활동해왔어요.

참고: 이 글은 Brandur Leach가 자신의 블로그에 게시한 아티클을 번역하고 요약한 것입니다.

원문: The Notifier Pattern for Applications That Use Postgres - Brandur Leach (2024년 5월 5일)

생성: Claude (Anthropic)

총괄: (디노이저denoiser)