Postgres를 사용하는 애플리케이션을 위한 알림자 패턴
핵심 요약
- Notifier 패턴은 프로그램당 단 하나의 Postgres 연결로 여러 토픽을 구독하고 알림을 분배하는 효율적인 아키텍처예요
- 단순한 접근(토픽당 연결)과 달리, 연결 수를 극적으로 줄여서 Postgres의 소중한 연결 리소스를 절약해요
- PgBouncer와 함께 쓸 때: LISTEN용 단 하나의 세션 연결만 유지하고, 나머지 앱은 모두 트랜잭션 풀링 모드를 활용할 수 있어요
- 버퍼링된 채널과 non-blocking send로 graceful degradation을 구현 - 느린 구독자가 전체를 멈추지 못하게 해요
- 실전 활용: 피처 플래그 즉시 반영, 실시간 알림, 작업 큐 wake-up (River 같은 곳에서 실제로 사용 중)
문제: LISTEN/NOTIFY는 연결을 아껴야 해요
Postgres의 LISTEN/NOTIFY는 아주 강력한 pub/sub 기능이에요. 하지만 단순하게 구현하면 곧 문제에 부딪혀요.
기본 원리:
- 하나의 연결로 여러 토픽을 LISTEN 할 수 있어요
- Postgres 연결은 여전히 소중하고 제한적인 리소스라, 아껴야 해요
- LISTEN은 특정 세션에 바인딩되어 있어서, 알림은 그 세션에만 전달돼요
단순한 접근은 “토픽당 하나의 연결”이에요. 앱에서 3개의 토픽을 구독하면? 3개의 연결. 10개 서버가 돌고 있으면? 30개의 연결. 이건 금방 비효율적으로 변해요.
해결책: Notifier 패턴
Notifier의 역할은 이거예요:
- 프로세스당 단 하나의 Postgres 연결을 유지해요
- 프로그램 안의 여러 컴포넌트가 이 연결로 원하는 만큼의 토픽을 구독할 수 있게 해줘요
- 알림을 기다렸다가, 받으면 각 구독자에게 분배해줘요
이 패턴을 쓰면 프로그램당 연결 수가 1개로 고정돼요. 토픽이 100개든, 구독자가 50개든 상관없이요.
구현 디테일
1. 토픽당 한 번만 LISTEN
연결이 하나니까, Notifier는 각 토픽에 대해 딱 한 번만 LISTEN을 보내면 돼요.
내부적으로 Notifier는 구독을 토픽별로 정리해요. 새 구독 요청이 오면:
- 토픽이 이미 LISTEN 중이면? → 그냥 구독자 리스트에 추가만 해요
- 처음 보는 토픽이면? →
LISTEN보내고 구독자 추가해요
2. 버퍼링된 채널과 Non-blocking Send
구독자는 버퍼링된 채널을 받아요 (예: Go의 make(chan string, 100)).
알림이 들어오면 Notifier는 non-blocking send로 채널에 넣어요:
// Go 예시
select {
case subscriber.ch <- notification:
// 성공!
default:
// 채널이 꽉 찼어요 → 그냥 버려요
}
왜 이렇게 할까요? 느린 구독자 하나가 전체를 막지 못하게 하려는 거예요. 버퍼가 꽉 차면 알림을 버리는 게, 모든 구독자를 블락하는 것보다 나아요.
| 접근 방식 | Blocking Send | Non-blocking Send (Notifier) |
|---|---|---|
| 느린 구독자 있을 때 | 모든 구독자가 멈춰요 | 그 구독자만 알림 누락, 나머지는 정상 |
| 장점 | 알림 절대 안 잃어버려요 | 시스템 전체가 견고해요 (graceful degradation) |
| 단점 | 단 하나의 느린 구독자가 DoS처럼 작동 | 구독자가 처리 못 하면 알림 누락 가능 |
버퍼 크기는 튜닝 가능해요. 너무 작으면 쉽게 누락되고, 너무 크면 메모리 낭비예요. 보통 100~1000 정도가 합리적이에요.
3. Interruptible Receives
Notifier는 알림을 기다리면서도 종료 가능해야 해요. 실전에서는 타임아웃을 걸어요:
// 예: 30초 타임아웃으로 기다리기
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
notification, err := conn.WaitForNotification(ctx)
이렇게 하면:
- 알림이 오면 즉시 처리해요
- 30초 동안 아무것도 안 오면 루프를 돌면서 종료 신호를 체크할 수 있어요
- Graceful shutdown이 가능해져요
PgBouncer 호환성
여기서 중요한 포인트가 하나 있어요: PgBouncer는 LISTEN과 잘 안 맞아요.
왜냐면:
LISTEN은 세션에 바인딩되어 있어요- PgBouncer의 트랜잭션 풀링 모드는 요청마다 다른 백엔드 연결을 쓸 수 있어요
- 그래서
LISTEN은 세션 풀링 모드에서만 작동해요
하지만 Notifier 패턴을 쓰면 양쪽의 장점을 다 누려요:
하이브리드 아키텍처
- LISTEN용 연결 1개: PgBouncer 없이 직접 Postgres로 (또는 세션 풀링 모드)
- 나머지 모든 앱 트래픽: PgBouncer 트랜잭션 풀링 모드로 효율적으로
앱에서 LISTEN 전용 연결 하나를 Notifier가 관리하고, 나머지는 전부 PgBouncer를 통해 트랜잭션 풀링 쓰면 돼요. 최고의 연결 효율성을 얻는 거죠.
실전 활용 사례
1. 피처 플래그 즉시 반영
인프로세스 캐시로 피처 플래그를 관리하면 빠르지만, 업데이트 반영이 늦어질 수 있어요.
Notifier 패턴으로 해결:
- 피처 플래그 테이블에 트리거를 걸어요
- INSERT/UPDATE/DELETE 시
pg_notify(‘feature_flags’, flag_id)보내기 - 앱의 Notifier가 알림 받으면 → 즉시 캐시 무효화
결과: 밀리초 안에 모든 서버로 플래그 변경이 전파돼요. 캐시의 속도 + 즉각적인 업데이트.
2. 작업 큐 Wake-up (River)
Brandur가 만든 River job queue가 실제로 이 패턴을 써요.
작동 방식:
- 새 작업이 INSERT되면 → 트랜잭션 안에서
NOTIFY보내기 - 워커 풀이 Notifier로 알림 받음
- 즉시 워커가 깨어나서 작업을 처리
장점: Postgres의 NOTIFY는 트랜잭션 안에서 동작해요. 작업이 커밋되는 순간 워커가 깨어나요. 폴링 없이, 즉시요.
3. 실시간 알림
채팅 앱, 협업 툴, 대시보드 같은 곳에서:
- 데이터 변경 시
NOTIFY보내기 - WebSocket 서버가 Notifier로 받아서 클라이언트에 푸시
폴링이나 외부 메시지 브로커 없이, Postgres만으로 실시간성을 얻어요.
테스트 팁
Notifier를 테스트할 때 타이밍 이슈를 조심해야 해요:
// 1. Notifier에게 LISTEN 지시
subscription := notifier.Subscribe("test_topic")
// 2. LISTEN이 완료될 때까지 기다려요!
<-subscription.Ready // 이 채널이 close되면 LISTEN 성공
// 3. 이제 NOTIFY 보내기
db.Exec("SELECT pg_notify('test_topic', 'hello')")
// 4. 알림 받기
notification := <-subscription.Notifications
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)