PostgreSQL LISTEN/NOTIFY를 활용한 실시간 애플리케이션: 경량 대안
Daniel Hayes
Full-Stack Engineer · Leapcell

PostgreSQL LISTEN/NOTIFY를 이용한 실시간 애플리케이션 구축
오늘날 빠르게 변화하는 디지털 세상에서 실시간 기능은 매력적인 사용자 경험의 초석이 되었습니다. 협업 문서 편집부터 즉석 채팅 애플리케이션 및 실시간 대시보드에 이르기까지, 변경 사항에 즉시 반응하는 능력은 매우 중요합니다. 전통적으로 개발자들은 이러한 반응성을 달성하기 위해 Redis Pub/Sub 또는 Kafka와 같은 전문 메시징 시스템을 사용해 왔습니다. 이러한 도구들은 강력하고 확장 가능하며 널리 채택되었지만, 추가적인 인프라, 유지보수 오버헤드 및 복잡성을 야기합니다. 기존의 데이터베이스 중심 애플리케이션에 직접 실시간 기능을 통합할 더 간단하고 가벼운 방법이 있다면 어떨까요? 이 글에서는 PostgreSQL의 종종 과소평가되는 LISTEN/NOTIFY 메커니즘을 심층적으로 다루며, 외부 메시징 브로커 없이 실시간 기능을 구축하기 위한 강력하고 우아한 대안으로 사용하는 것에 대한 설득력 있는 주장을 제시할 것입니다.
핵심 개념 이해하기
실용적인 내용으로 들어가기 전에, 이 접근 방식의 핵심인 PostgreSQL 기능에 대한 명확한 이해를 확립해 봅시다.
LISTEN: 이 SQL 명령은 데이터베이스 클라이언트가 특정 "채널"에 대한 알림을 받도록 자신을 등록하는 데 사용됩니다. 클라이언트는 여러 채널을 동시에 수신 대기할 수 있습니다.NOTIFY: 이 SQL 명령은 지정된 채널에서 현재LISTEN중인 모든 클라이언트에 알림을 보냅니다. 선택적으로 추가 데이터를 전달하는 "페이로드" 문자열(PostgreSQL 9.0 이상에서는 최대 8000바이트)을 포함할 수 있습니다.- 트리거: 데이터베이스 트리거는 테이블에서 특정 이벤트(예:
INSERT,UPDATE,DELETE)가 발생할 때 자동으로 실행되는 특수한 유형의 저장 프로시저입니다. 관련 데이터가 변경될 때마다 알림을 자동으로 보내는 데 트리거를 활용할 것입니다. - 채널: 알림이 전송 및 수신되는 명명된 "주제" 또는 "카테고리"입니다. 클라이언트는 채널을
LISTEN하고,NOTIFY는 해당 채널에 메시지를 보냅니다.
본질적으로 LISTEN/NOTIFY는 PostgreSQL 내에서 직접 동기식 채널 기반 메시징 시스템을 제공합니다. NOTIFY 명령이 실행되면, 현재 연결되어 해당 채널을 LISTEN 중인 모든 클라이언트는 다음 쿼리 처리 중에 또는 비동기 알림 핸들러가 호출될 때 알림을 받게 됩니다.
작동 원리
핵심 아이디어는 관련 실시간 기능 데이터가 변경될 때마다 데이터베이스 트리거를 사용하여 클라이언트에 자동으로 NOTIFY하는 것입니다.
- 데이터 수정: 애플리케이션이 테이블에
INSERT,UPDATE또는DELETE작업을 수행합니다. - 트리거 발생: 해당 테이블의 미리 정의된
AFTER트리거가 변경 사항을 감지합니다. - 알림 전송: 트리거는
NOTIFY명령을 실행하여 특정 채널로 메시지를 보내며, 변경 사항(예: 영향을 받은 레코드의 ID, 작업 유형)에 대한 세부 정보를 포함할 수 있습니다. - 클라이언트 수신: 해당 채널을 PostgreSQL 연결에서
LISTEN중인 모든 애플리케이션 클라이언트가 알림을 받습니다. - 클라이언트 반응: 클라이언트는 알림을 처리하며, UI 구성 요소를 새로 고치거나 캐시를 무효화하거나 추가 작업을 시작할 수 있습니다.
이는 데이터 변경과 알림 전달을 긴밀하게 결합하여 실시간 업데이트가 권위 있는 데이터 소스에서 직접 구동되도록 보장합니다.
실용적인 구현
간단한 예시로 이를 설명해 봅시다. 새로운 제품 추가를 표시하는 실시간 대시보드를 구축하는 것입니다.
1. 데이터베이스 설정
먼저 products 테이블을 생성합니다:
CREATE TABLE products ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, price DECIMAL(10, 2) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP );
다음으로, 새 제품이 삽입된 후 실행될 트리거 함수를 생성합니다:
CREATE OR REPLACE FUNCTION notify_new_product() RETURNS TRIGGER AS $$ BEGIN PERFORM pg_notify('new_product_channel', NEW.id::text); RETURN NEW; END; $$ LANGUAGE plpgsql;
여기서 pg_notify는 NOTIFY 명령에서 사용하는 기반 함수입니다. new_product_channel 채널로 새 제품의 id를 페이로드로 보냅니다.
마지막으로 INSERT 작업을 위해 이 트리거 함수를 products 테이블에 연결합니다:
CREATE TRIGGER product_insert_trigger AFTER INSERT ON products FOR EACH ROW EXECUTE FUNCTION notify_new_product();
2. 클라이언트 측 구현 (Python 예시)
이제 Python 클라이언트가 이러한 알림을 어떻게 LISTEN할 수 있는지 살펴봅시다. 인기 있는 Python용 PostgreSQL 어댑터인 psycopg2 라이브러리를 사용하겠습니다.
import psycopg2 import select import json import time # 데이터베이스 연결 정보 DB_PARAMS = { 'host': 'localhost', 'database': 'your_database', 'user': 'your_user', 'password': 'your_password' } def listen_for_notifications(): conn = None try: conn = psycopg2.connect(**DB_PARAMS) conn.autocommit = True # LISTEN/NOTIFY에 중요 cursor = conn.cursor() # 채널 수신 대기 cursor.execute("LISTEN new_product_channel;") print("새 제품 알림 수신 대기 중...") while True: # 알림 확인. timeout=1은 1초마다 확인함을 의미합니다. if select.select([conn], [], [], 1) == ([conn], [], []): conn.poll() while conn.notifies: # 첫 번째 알림 검색 notify = conn.notifies.pop(0) product_id = notify.payload print(f"채널 '{notify.channel}'에서 페이로드: '{product_id}'로 알림 수신") # 실제 앱에서는 제품 세부 정보를 가져와 UI 업데이트 fetch_product_details(product_id) # select.select가 완전히 차단되지 않은 경우 버스 대기를 방지하기 위해 약간의 지연 추가 time.sleep(0.1) except Exception as e: print(f"오류 발생: {e}") finally: if conn: conn.close() print("연결 종료.") def fetch_product_details(product_id): # 이 함수는 일반적으로 새 제품의 세부 정보를 위해 데이터베이스를 쿼리합니다. # 그런 다음 웹소켓 또는 다른 메커니즘을 통해 프론트엔드로 푸시합니다. print(f" --> 제품 ID: {product_id}에 대한 세부 정보 가져오기 및 대시보드 업데이트 중...") # 예: 실제 애플리케이션에서는 전체 제품 객체를 쿼리할 수 있습니다: # with psycopg2.connect(**DB_PARAMS) as conn: # with conn.cursor() as cur: # cur.execute("SELECT name, price FROM products WHERE id = %s;", (product_id,)) # product_data = cur.fetchone() # print(f" 제품 세부 정보: 이름={product_data[0]}, 가격={product_data[1]}") if __name__ == "__main__": listen_for_notifications()
테스트 방법:
- 하나의 터미널에서 Python 스크립트를 실행합니다.
- 다른 터미널에서 PostgreSQL 데이터베이스에 연결하고 새 제품을 삽입합니다:
Python 스크립트에서 새 제품 알림을 나타내는 출력이 즉시 표시되어야 합니다.INSERT INTO products (name, price) VALUES ('E-Book Reader', 129.99);
사용 사례 및 장점
이상적인 사용 사례:
- 실시간 대시보드: 데이터 변경 시 차트 및 메트릭 업데이트(예: 새 주문, 지원 티켓, 센서 판독값).
- 캐시 무효화: 데이터베이스 레코드가 업데이트될 때 애플리케이션 서버에 캐시된 데이터를 무효화하도록 알림.
- 사용자 알림: 관련 이벤트에 대한 푸시 알림 또는 인앱 경고 전송(예: 채팅 새 메시지, 상태 업데이트).
- 서비스 간 통신(경량): PostgreSQL을 공유하는 마이크로서비스의 경우,
LISTEN/NOTIFY는 데이터베이스 기반의 저용량 이벤트에 대한 간단한 이벤트 버스로 작동할 수 있습니다. - 워크플로우 트리거: 데이터베이스 이벤트 기반으로 다운스트림 프로세스 시작.
장점:
- 단순성 및 제로 설정: 관리할 외부 종속성이나 인프라가 없습니다. PostgreSQL에 내장되어 있습니다.
- 낮은 지연 시간: 기존 데이터베이스 연결을 통해 알림이 직접 전달되며, 종종 매우 낮은 지연 시간을 가집니다.
- 트랜잭션 무결성: 트랜잭션 내에서 전송된 알림은 트랜잭션이 성공적으로 커밋된 경우에만 전달됩니다. 이는 데이터 일관성을 보장합니다.
- 데이터 지역성: 변경 사항과 알림이 긴밀하게 결합되어 데이터베이스를 진실의 단일 소스로 활용합니다.
- 친숙함: SQL 및 트리거에 익숙한 개발자는 빠르게 적응할 수 있습니다.
- 비용 효율성: 기존 데이터베이스 리소스를 활용하여 인프라 비용을 절감합니다.
한계 및 고려 사항
강력하지만 LISTEN/NOTIFY에는 한계가 있습니다:
- 지속성 없음: 클라이언트가 수신 대기 중이지 않으면 알림이 큐에 저장되거나 저장되지 않습니다. 클라이언트가 연결을 끊었다가 다시 연결하면 오프라인 중에 전송된 알림을 받지 못합니다.
- 특정 클라이언트에 대한 전달 보장 없음: 알림은 특정 클라이언트가 아닌 채널의 모든 수신 대기자에게 브로드캐스트됩니다.
- 제한된 페이로드 크기: 8000바이트 페이로드 제한은 일반적으로 ID 또는 작은 JSON 조각만 보내야 함을 의미하며, 필요한 경우 클라이언트가 전체 세부 정보를 가져와야 합니다.
- 확장성: 매우 높은 볼륨의 전역 규모 메시징의 경우 Kafka와 같은 전문 시스템이
LISTEN/NOTIFY보다 뛰어날 것입니다. 데이터 센터 전반에 걸쳐 초당 수백만 건의 메시지를 처리하도록 설계되지 않았습니다. - 클라이언트 연결 관리:
LISTEN하는 각 클라이언트는 열린 데이터베이스 연결을 유지하며, 이는 리소스를 소비할 수 있습니다. 확장성을 위해서는 효율적인 연결 풀링이 필수적입니다. - 복제 및 고가용성:
LISTEN/NOTIFY는 단일 PostgreSQL 인스턴스 내에서 작동합니다. 복제된 설정에서 기본의NOTIFY는 해당 복제본에 연결된 수신 대기 클라이언트에게 자동으로 전파되지 않습니다.
결론
PostgreSQL의 LISTEN/NOTIFY 메커니즘은 PostgreSQL이 이미 기본 데이터 저장소인 애플리케이션에서 실시간 기능을 구현하기 위한 놀랍도록 강력하고 우아한 솔루션을 제공합니다. 이 내장 기능을 활용함으로써 개발자는 많은 일반적인 사용 사례에 대해 외부 메시징 브로커의 복잡성과 오버헤드를 피할 수 있어 아키텍처를 단순화하고 운영 부담을 줄일 수 있습니다. Kafka 또는 Redis Pub/Sub와 같은 대규모 지속적 메시징 시스템을 대체하는 것은 아니지만, LISTEN/NOTIFY는 기존 PostgreSQL 데이터베이스에서 직접 실시간 기능을 지원하는 매우 효과적이고 가벼운 대안으로 두드러집니다. 이는 데이터가 있는 곳으로 실시간 반응성의 힘을 가져와 동적인 사용자 경험을 위한 더 간단한 경로를 제공합니다.

