본문 바로가기
Database2026년 8월 17일6분 읽기

LISTEN/NOTIFY — PostgreSQL만으로 실시간 알림 구현하기

YS
김영삼
조회 5
LISTEN/NOTIFY — PostgreSQL만으로 실시간 알림 구현하기

LISTEN/NOTIFY는 PostgreSQL에 내장된 발행-구독(pub/sub) 메시징 기능입니다. 한 세션이 LISTEN 채널명으로 특정 채널을 구독해 두면, 다른 세션이 NOTIFY 채널명, '메시지'를 보내는 순간 구독자에게 페이로드가 밀려 옵니다. 폴링 없이, 별도 메시지 브로커 없이, DB만으로 "무언가 바뀌었다"를 실시간에 가깝게 전달할 수 있다는 게 핵심입니다.

몇 번 데인 뒤에야 이 기능의 진짜 쓸모와 한계를 알았습니다. 캐시 무효화나 백그라운드 워커 깨우기 같은 데는 기가 막히게 잘 맞고, 반대로 "절대 유실되면 안 되는 이벤트 큐"로 쓰면 언젠가 사고가 납니다.

기본 동작

-- 세션 A: 채널 구독
LISTEN cache_invalidation;
-- 세션 B: 알림 발행 (페이로드는 텍스트)
NOTIFY cache_invalidation, 'user:42';
-- 트리거·함수 안에서는 함수 형태가 편하다
SELECT pg_notify('cache_invalidation', 'user:' || NEW.id::text);

중요한 특성 하나. NOTIFY는 트랜잭션에 묶입니다. 트랜잭션 안에서 NOTIFY를 호출해도 실제 알림은 COMMIT 시점에만 나갑니다. 롤백하면 알림도 없던 일이 됩니다. 이 덕분에 "데이터 변경과 알림이 원자적으로 함께 나간다"는 아주 유용한 보장이 생깁니다. 그래서 보통은 트리거 안에서 pg_notify를 부릅니다.

CREATE FUNCTION notify_order_change() RETURNS trigger AS $$
BEGIN
  PERFORM pg_notify('orders', json_build_object(
    'op', TG_OP, 'id', NEW.id, 'status', NEW.status
  )::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_order_change
AFTER INSERT OR UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION notify_order_change();

애플리케이션에서 받기 (Node.js 예)

드라이버가 알림 이벤트를 노출합니다. node-postgres 기준으로는 이렇게 짧습니다.

const client = new Client();
await client.connect();
await client.query('LISTEN orders');
client.on('notification', (msg) => {
  const evt = JSON.parse(msg.payload);
  console.log('order changed:', evt.id, evt.status);
});

주의: LISTEN은 연결(세션)에 붙습니다. 커넥션 풀 뒤에서 매번 다른 연결을 받으면 구독이 유지되지 않습니다. 그래서 구독 전용으로 풀을 거치지 않는 상시 연결을 하나 따로 두는 게 정석입니다. 저는 이걸 몰라서 "알림이 왔다 안 왔다 한다"로 한참 헤맸습니다.

언제 쓰고, 언제 쓰지 말지

잘 맞는 곳피해야 할 곳
캐시 무효화 신호유실되면 안 되는 이벤트 큐
워커 깨우기(폴링 대체)대용량 데이터 페이로드 전송
설정·기능 플래그 리로드구독자가 오프라인일 때 보관 필요

핵심 한계는 알림은 저장되지 않는다는 점입니다. 구독자가 그 순간 연결돼 있지 않으면 그 알림은 그냥 사라집니다. 재연결 사이에 온 알림도 못 받습니다. 그래서 견고한 패턴은 "NOTIFY는 '테이블 보러 와'라는 가벼운 신호로만 쓰고, 진짜 데이터·순서·유실 방지는 테이블(아웃박스)에서 처리"하는 것입니다. 페이로드도 8000바이트 제한이 있어 큰 데이터를 실어 보낼 곳이 아닙니다.

패턴: 변경을 outbox 테이블에 INSERT → 트리거가 pg_notify('outbox','')로 워커를 깨움 → 워커는 테이블에서 미처리 행을 FOR UPDATE SKIP LOCKED로 집어 처리. 알림을 놓쳐도 워커가 주기적으로 테이블을 훑으면 유실이 없습니다.

자주 묻는 질문

NOTIFY는 얼마나 빠른가요?

보통 밀리초 단위로 전달됩니다. 다만 "실시간 보장"이 아니라 "매우 빠른 베스트 에포트"입니다. 서버 부하나 구독자 처리 지연이 있으면 밀릴 수 있습니다.

같은 페이로드를 여러 번 NOTIFY하면 중복 전달되나요?

한 트랜잭션 안에서 완전히 동일한 채널+페이로드 NOTIFY는 커밋 시 하나로 합쳐집니다(중복 제거). 트랜잭션이 다르면 각각 전달됩니다. 이 동작에 의존하기보다 구독자 쪽을 멱등하게 만드는 편이 안전합니다.

커넥션 풀(PgBouncer)에서 왜 문제가 되나요?

트랜잭션·문장 단위 풀링에서는 연결이 계속 바뀌어 LISTEN이 유지되지 않습니다. 구독은 세션 단위 전용 연결로 유지하거나, PgBouncer를 session pooling으로 두거나, 풀을 우회한 직결을 써야 합니다.

알림 유실을 완전히 막을 수 있나요?

NOTIFY 자체로는 못 막습니다. 위의 아웃박스 패턴처럼 상태를 테이블에 남기고 NOTIFY는 깨우기 신호로만 쓰면, 알림을 놓쳐도 폴링 백업으로 결국 처리되게 만들 수 있습니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록