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 연결을 유지해요
- 프로그램 안의 여러 컴포넌트가 이 연결로 원하는 만큼의 토픽을 구독할 수 있게 해줘요
- 알림을 기다렸다가, 받으면 각 구독자에게 분배해줘요
이 패턴을 쓰면 프로그램당 연결 수가 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보다 먼저 보내지면 알림을 놓쳐버려요.
구현 참고
직접 구현하고 싶다면:
- River의 notifier 구현 (github.com/riverqueue/river): 프로덕션 품질의 Go 구현
- Jon Brown의 가이드 (Go and Postgres LISTEN/NOTIFY): Brandur의 글을 기반으로 한 자세한 구현 가이드
마무리
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)