PostgreSQL LISTEN/NOTIFY: 어디까지 쓰며 언제 메시지 큐로 넘어가나
LISTEN/NOTIFY는 “깨우기 신호"로는 훌륭하고, “메시지 큐"로는 부적합합니다. 이 한 줄이 결론이고, 이 글은 그 근거입니다. PostgreSQL에는 별도 인프라 없이 쓰는 pub/sub이 내장되어 있어서, 폴링 없이 이벤트를 실시간으로 밀어 줄 수 있습니다. 문제는 이것을 큐로 오용할 때 나는 사고들이고, 경계선을 정확히 알면 아주 유용한 도구입니다.
동작: 트랜잭션과 한 몸인 알림 #
-- 듣는 쪽 (세션 A)
LISTEN order_events;
-- 보내는 쪽 (세션 B)
BEGIN;
INSERT INTO orders (user_id, amount_krw) VALUES (42, 89000);
SELECT pg_notify('order_events', '1204'); -- 새 주문의 id만 보냅니다
COMMIT; -- 이 순간 알림이 전달됩니다핵심 성질은 알림이 트랜잭션 커밋 시점에, 커밋되어야만 나간다는 것입니다. ROLLBACK되면 알림도 없던 일이 됩니다. 외부 메시지 브로커와 DB 쓰기를 함께 쓸 때 겪는 “DB는 롤백됐는데 메시지는 나간” 정합성 문제가 구조적으로 없습니다. 이것이 LISTEN/NOTIFY의 가장 큰 매력이고, 같은 채널을 여러 세션이 LISTEN하면 전원에게 전달되는 팬아웃도 됩니다.
한계 세 가지: 크기, 휘발성, 규모 #
- 8000바이트 한도. 페이로드는 8000바이트가 상한입니다. 애초에 데이터를 실어 나르라고 만든 통로가 아니므로, 정석은 본문은 테이블에, 알림에는 키만입니다. 위 예시가 주문 전체(JSON)가 아니라 id만 보낸 이유입니다. 받은 쪽은 키로 SELECT해서 최신 상태를 읽습니다(알림 도착 시점에는 상태가 이미 더 바뀌었을 수 있으므로, 어차피 다시 읽는 것이 맞습니다).
- 휘발성. 알림은 저장되지 않습니다. LISTEN 중인 세션이 없으면 그냥 사라지고, 리스너가 재접속하는 동안의 알림도 유실됩니다. 즉 “반드시 처리되어야 하는 작업"의 전달 수단이 될 수 없습니다. 내구성 있는 전달이 필요하면 데이터 자체가 테이블에 있어야 합니다.
- 규모의 병목. 알림은 커밋 순서 보장을 위해 인스턴스 전역의 단일 큐를 거칩니다. 발행이 많아지면 커밋 경로 자체가 이 직렬화에 물려 DB 전체의 커밋이 느려지는 병목이 되고, 실제 대규모 서비스의 장애 사례들이 보고되어 있습니다(개선 작업이 진행 중이지만 현행 안정판 기준으로는 실재하는 한계입니다). 고빈도 발행 채널로는 쓰지 말라는 뜻입니다.
정석 조합: 알림은 깨우고, 큐는 테이블이 맡습니다 #
세 한계를 종합하면 설계가 하나로 수렴합니다. 작업 큐의 본체는 FOR UPDATE SKIP LOCKED 테이블 큐가 맡고, LISTEN/NOTIFY는 “새 작업 있음"을 워커에게 알리는 깨우기 신호로만 씁니다.
작업 등록: INSERT INTO jobs ... ; pg_notify('jobs', ''); -- 페이로드도 필요 없습니다
워커 루프: 알림 대기 → 깨어나면 SKIP LOCKED로 집을 수 있는 만큼 처리 → 다시 대기
(+ 알림 유실 대비, 수십 초 간격의 폴링을 안전망으로)이 조합에서 알림이 유실되어도 안전망 폴링이 작업을 줍고, 워커가 없던 사이의 작업도 테이블에 그대로 남아 있습니다. 알림은 “폴링 간격을 밀리초로 줄여 주는 가속기"일 뿐, 정확성은 전부 테이블 큐가 책임집니다. 캐시 무효화 브로드캐스트, 관리 화면 실시간 갱신, 설정 리로드 신호처럼 “놓쳐도 다음 기회에 따라잡는” 통지가 LISTEN/NOTIFY 단독의 알맞은 자리입니다.
주의점 하나. LISTEN은 세션에 상태를 남기는 기능이므로 PgBouncer transaction 모드를 통과하지 못합니다. 리스너 커넥션은 풀러를 우회해 DB에 직접, 전용으로 붙이는 것이 표준입니다(워커 수는 보통 소수라 부담이 없습니다).
진짜 메시지 큐로 가는 기준 #
DB 안에서 해결하는 위 조합의 수비 범위는 넓지만, 다음 신호가 보이면 전용 메시지 시스템(SQS·RabbitMQ·Kafka 등)의 자리입니다. 처리량이 커서 큐 테이블의 쓰기·VACUUM이 DB 본업을 압박할 때, 여러 서비스가 구독하는 이벤트 스트림(재생·보존 요구)이 필요할 때, 발행자와 소비자가 서로 다른 DB·팀·시스템일 때. 반대로 단일 서비스 안의 백그라운드 작업이라면, 인프라를 하나 늘리기 전에 테이블 큐 + 알림 조합이 먼저입니다. SQS 계열과의 비교에서 다룬 관리형 큐의 가치는 그 선을 넘은 뒤의 이야기입니다.
정리 #
- LISTEN/NOTIFY의 매력은 트랜잭션 정합성입니다. 커밋되어야만 나가고, 롤백되면 알림도 없습니다.
- 한계는 8000바이트, 휘발성, 대량 발행 시 커밋 직렬화 병목입니다. 데이터 전달·보장 전달·고빈도 채널에는 부적합합니다.
- 정석은 역할 분담입니다. 큐의 본체는 SKIP LOCKED 테이블, 알림은 깨우기 신호 + 안전망 폴링입니다.
- 알림에는 키만 싣고, 받은 쪽이 다시 읽습니다. 리스너 커넥션은 PgBouncer transaction 모드를 우회해 직접 붙입니다.
- 처리량·다중 구독·시스템 경계가 커지면 전용 메시지 큐로 넘어갑니다. 그 전까지는 DB 안 조합이 운영 부담 최소의 답입니다.