애플리케이션에 실시간 업데이트가 필요할 때 2025년의 기본적인 답은 Firebase Realtime Database, Supabase Realtime, Pusher, Ably, 호스팅된 WebSocket 계층 같은 관리형 서비스를 선택하는 것입니다. 이 서비스들은 연결된 클라이언트에 업데이트를 전달하는 복잡성을 추상화합니다. 잘 작동하지만 그렇지 않을 때도 있습니다.
Atlas Verified는 실시간 구독이 포함된 관리형 데이터베이스 플랫폼으로 시작했습니다. 시스템이 커지면서 그 추상화의 한계에 도달했습니다. 연결 풀링 제약, 실시간 신뢰성 문제, PostGIS 공간 질의 필요성, 맞춤형 트리거 논리를 원하는 요구가 모두 같은 방향을 가리켰습니다. 데이터베이스 계층을 직접 통제하는 것입니다.
자체 호스팅 PostgreSQL로 이전하고 PostgreSQL의 기본 LISTEN/NOTIFY 메커니즘과 Server-Sent Events를 결합하여 실시간 시스템을 구축했습니다. 이 글에서는 그 결정을 내린 이유, 아키텍처의 작동 방식, 배운 교훈을 다룹니다.
관리형 플랫폼을 떠난 이유
관리형 데이터베이스 플랫폼은 인증, 행 수준 보안, 실시간 구독, 자동 생성 API, 대시보드를 모두 바로 제공하는 매력적인 가치 제안을 내놓습니다. 초기 제품에는 매우 유용합니다. 백엔드 코드를 한 줄도 작성하지 않고 작동하는 애플리케이션을 만들 수 있습니다.
애플리케이션이 성숙하고 요구 사항이 플랫폼의 가정과 달라지면서 문제가 나타납니다.
연결 한도. 관리형 플랫폼은 자체 한도를 부과하는 연결 풀러를 통해 데이터베이스 연결을 라우팅하는 경우가 많습니다. API 서버, 백그라운드 작업 시스템, 실시간 리스너 등 각각 연결 풀이 필요한 여러 백엔드 서비스가 있으면 사용 가능한 연결이 빠르게 소진됩니다. 당사는 실제 제품 문제를 해결하기보다 풀러의 한계를 우회하는 엔지니어링을 하게 되었습니다.
실시간 신뢰성. 관리형 플랫폼의 실시간 시스템은 PostgreSQL의 선행 기록 로그에 대한 변경 데이터 캡처로 작동했습니다. 아키텍처는 우아하지만 당사가 통제할 수 없는 실패 유형을 가져왔습니다. WAL 기반 복제에는 본질적인 지연이 있습니다. 실시간 서비스가 때때로 뒤처져 오래된 업데이트를 전달했고 네트워크 중단 후 재연결은 신뢰하기 어려웠습니다. 이 문제를 디버깅하려면 당사가 통제하지 않는 인프라에 접근해야 했습니다.
확장 요구 사항. 당사 애플리케이션은 항만 위치, 운송 경로, 선박 위치 등 지리 데이터가 포함된 무역 문서를 처리합니다. PostgreSQL의 공간 확장인 PostGIS는 이 질의에 필수적입니다. 관리형 플랫폼이 PostGIS를 지원했지만 당사에 필요한 특정 버전과 구성이 항상 제공되지는 않았습니다.
맞춤형 트리거 논리. 단순한 범용 "행 변경" 알림이 아니라 애플리케이션별 이벤트를 실행하는 데이터베이스 트리거가 필요했습니다. 문서 처리 상태가 바뀌면 업로드한 특정 연결 클라이언트에 알려야 합니다. 관리형 플랫폼의 실시간 기능은 테이블 수준이었지만 당사는 개체 수준이 필요했습니다.
이전 결정은 비용이나 원칙에 관한 것이 아니었습니다. 실용적인 결정이었습니다. 기능을 직접 구축하는 데 들었을 시간보다 플랫폼 제약을 우회하는 데 더 많은 시간을 쓰고 있었습니다.
LISTEN/NOTIFY: PostgreSQL의 숨겨진 발행·구독 기능
PostgreSQL은 2010년에 출시된 버전 9.0부터 내장 발행·구독 메커니즘을 포함했습니다. 15년 이상 되었지만 LISTEN/NOTIFY는 놀라울 정도로 활용되지 않습니다. 대부분의 개발자는 존재하는지도 모릅니다.
메커니즘은 간단합니다. 어떤 데이터베이스 연결이든 이름이 지정된 채널에서 LISTEN을 실행하여 구독할 수 있습니다. 어떤 연결이든 NOTIFY를 실행하여 해당 채널의 모든 리스너에게 메시지를 보낼 수 있습니다. 페이로드는 8,000바이트로 제한된 문자열입니다.
LISTEN/NOTIFY가 실시간 애플리케이션에서 강력한 이유:
인프라 오버헤드 없음. 배포, 모니터링, 확장할 별도 메시지 브로커가 없습니다. PostgreSQL이 있으면 발행·구독 기능이 있습니다. 이 특정 용도에는 Redis, RabbitMQ, Kafka가 필요하지 않습니다.
트랜잭션 보장. NOTIFY 메시지는 이를 포함하는 트랜잭션이 커밋될 때만 전달됩니다. 트랜잭션이 행을 삽입하고 알림을 보낸 뒤 롤백되면 알림은 전달되지 않습니다. 외부 메시지 브로커에서는 신중한 통합 실행 없이 얻을 수 없는 특성입니다.
트리거 통합. PostgreSQL 트리거는 실행의 일부로 pg_notify를 호출할 수 있습니다. 즉 쓰기 경로에 애플리케이션 코드 없이도 특정 데이터가 바뀔 때마다 데이터베이스가 리스너에게 자동으로 알리도록 할 수 있습니다.
한계도 실제로 존재하며 이해할 가치가 있습니다.
영속성 없음. NOTIFY가 실행될 때 듣는 주체가 없으면 메시지는 사라집니다. 재생, 메시지 큐, 연결이 끊긴 클라이언트에 대한 전달 보장이 없습니다. 리스너는 재연결과 상태 복구를 처리해야 합니다.
페이로드 크기. 알림 페이로드의 8,000바이트 한도 때문에 NOTIFY로 큰 객체를 보낼 수 없습니다. 당사는 테이블명, 작업 유형, 기본 키라는 최소 페이로드를 보내고 필요하면 클라이언트가 전체 데이터를 가져오게 합니다.
단일 데이터베이스 범위. LISTEN/NOTIFY는 하나의 PostgreSQL 인스턴스 안에서 작동합니다. 복제본이나 클러스터에 걸쳐 작동하지 않습니다. 모든 쓰기가 하나의 기본 인스턴스로 향하는 아키텍처에서는 한계가 아닙니다.
당사의 아키텍처: 트리거, SSE, 채널 다중화
각 요소는 다음과 같이 결합됩니다.
데이터베이스 트리거 는 문서, 메시지, 대화, 알림, 화물 검증 등 실시간 업데이트가 중요한 테이블에 설치됩니다. 행이 삽입, 업데이트, 삭제되면 트리거가 실행되어 테이블명, 작업 유형, 기본 키, 필터 열을 포함한 JSON 페이로드로 pg_notify를 호출합니다.
전용 리스너 연결 은 API 서버에서 알림 채널에 LISTEN을 실행하여 모든 트리거 이벤트를 받습니다. 질의 연결 풀과 분리된 장기 유지 전용 PostgreSQL 연결이며 알림을 듣는 일만 합니다.
이벤트 버스 는 리스너의 알림을 받아 구독에 따라 연결된 클라이언트로 보냅니다. 특정 화물 ID가 일치하는 문서를 구독한 클라이언트는 그 특정 화물의 문서 알림만 받습니다.
Server-Sent Events 는 브라우저에 알림을 전달합니다. 각 연결 클라이언트는 원하는 채널과 필터를 지정하여 API 서버에 하나의 SSE 연결을 엽니다. 이벤트 버스는 들어오는 알림을 클라이언트 구독과 대조하고 일치하는 이벤트를 해당 SSE 연결로 전달합니다.
WebSockets 대신 SSE를 선택한 이유
당사는 이 결정에 계속 만족해 왔습니다. Server-Sent Events는 당사 용도에 중요한 모든 측면에서 WebSockets보다 간단합니다.
설계상 단방향. 당사의 실시간 요구는 서버에서 클라이언트로 향합니다. 서버가 데이터 변경을 클라이언트에 알려줍니다. 클라이언트가 서버에 실시간 메시지를 보낼 필요는 없으며 그 작업에는 표준 HTTP 요청을 사용합니다. SSE의 단방향 모델이 실제 요건과 일치합니다.
HTTP 기본 지원. SSE 연결은 특정 콘텐츠 유형을 사용하는 표준 HTTP 요청입니다. 특별한 구성 없이 모든 HTTP 프록시, 로드 밸런서, CDN을 통과합니다. WebSockets에는 HTTP Upgrade가 필요하지만 일부 프록시는 이를 올바르게 처리하지 않으며 일부 기업 방화벽은 완전히 차단합니다.
자동 재연결. 브라우저의 EventSource API는 기본적으로 재연결을 처리합니다. 연결이 끊기면 브라우저는 지수적으로 대기 시간을 늘리며 자동으로 재연결합니다. WebSockets에서는 재연결 논리를 직접 구현해야 하며 적어도 한 번은 잘못 구현하게 됩니다.
WebSockets가 우세한 유일한 영역인 양방향 통신은 당사에 필요하지 않습니다. 실시간 클라이언트-서버 메시징이 필요한 애플리케이션에는 WebSockets가 올바른 선택입니다. 서버에서 클라이언트로 알림을 전달하는 애플리케이션에는 SSE가 올바른 선택입니다.
ORM 없이 질의 계층 구축
관리형 플랫폼을 떠나면서 자동 생성 질의 API도 떠났습니다. API 서버를 위한 데이터베이스 접근 계층이 필요했습니다.
표준 선택지는 Prisma, TypeORM, Drizzle 같은 ORM 또는 드라이버를 사용하는 원시 SQL입니다. 당사는 세 번째 길을 선택했습니다. 연결 가능한 API 인터페이스를 제공하면서 내부에서 원시 SQL을 생성하고 실행하는 가벼운 질의 빌더입니다.
익숙한 API. 기존 코드베이스는 관리형 플랫폼의 클라이언트 라이브러리에 맞춰 작성되어 있었습니다. 호환 API 인터페이스 덕분에 이전 중 코드 변경을 최소화할 수 있었습니다. 이전에 플랫폼 클라이언트로 실행하던 질의를 동일한 문법으로 질의 빌더에서 실행할 수 있었습니다.
숨겨진 마법 없음. ORM은 볼 수 없고 때로는 통제할 수도 없는 SQL을 생성합니다. 질의가 느리면 생성된 SQL을 읽어야 하지만 그 SQL은 이를 만든 ORM 호출과 거의 닮지 않은 경우가 많습니다. 당사의 질의 빌더는 예측 가능한 SQL을 생성합니다. 각 연결 메서드는 특정 SQL 절에 대응하며 생성된 질의를 언제든 살펴볼 수 있습니다.
내장 조인. 무역 데이터는 관계형입니다. 화물에는 문서가 있고 문서에는 노드가 있으며 노드에는 검증 확인이 있습니다. 매번 복잡한 JOIN 질의를 직접 작성하지 않고 중첩된 선택을 표현하여 중첩된 결과를 받는 것은 개발자 경험을 크게 개선합니다.
그 결과 수백 줄의 코드로 이루어지고 데이터베이스 상호작용의 대부분을 처리하며 언제나 읽고 최적화할 수 있는 SQL을 생성하는 질의 계층이 만들어졌습니다.
연결 풀 예산 관리
PostgreSQL을 자체 호스팅하면 연결 예산을 직접 관리합니다. PostgreSQL의 max_connections 설정이 절대 상한을 정하며 모든 연결은 메모리를 소비합니다.
연결을 서비스 간에 공유하는 유한한 자원인 예산으로 생각하면 이에 대비하지 않는 팀이 겪는 한 유형의 문제를 예방할 수 있다는 사실을 발견했습니다.
각 서비스는 최대 연결 수를 선언합니다. 모든 최대값의 합은 PostgreSQL 한도보다 충분히 낮아야 하며 psql 세션, 이전 스크립트, 모니터링 도구 같은 비정기 연결에 여유를 남겨야 합니다. 당연하게 들리지만 대부분의 팀은 실제 운영에서 연결이 고갈될 때까지 하지 않습니다. 연결 예산을 미리 계획하여 당사는 그 특정 새벽 3시 긴급 호출을 피했습니다.
대규모의 안전한 마이그레이션
관리형 플랫폼에서 자체 호스팅 PostgreSQL로 이전하려면 수백 개의 테이블, 함수, 트리거, 인덱스를 이동해야 했습니다. 그러나 더 큰 과제는 애플리케이션이 발전해도 계속 작동할 마이그레이션 시스템을 구축하는 것이었습니다.
자동 마이그레이션 시스템은 다음처럼 작동합니다. 마이그레이션 파일은 타임스탬프로 이름을 지정하고 전용 디렉터리에 둡니다. 실행기는 디렉터리를 스캔하고 적용된 마이그레이션을 기록하는 추적 테이블과 비교한 뒤 대기 중인 마이그레이션을 순서대로 실행합니다. 각 마이그레이션은 트랜잭션 내에서 실행됩니다. 실패하면 깨끗하게 롤백하고 문제가 해결될 때까지 배포를 차단합니다.
마이그레이션 실행기는 매 배포마다 애플리케이션 시작 전에 자동으로 실행됩니다. 즉 수동 단계 없이 배포 과정의 일부로 스키마 변경을 적용합니다. 개발자가 타임스탬프가 있는 SQL 파일을 만들고 저장소에 푸시하면 다음 배포가 이를 적용합니다.
안전을 유지하는 핵심 설계 결정은 다음과 같습니다. 마이그레이션은 멱등성 보호 조치를 사용하여 일부만 적용된 경우에도 실패하지 않습니다. 모든 마이그레이션은 사람의 입력 없이 실행되는 독립적인 SQL 스크립트여야 합니다. 마이그레이션이 실패하면 트랜잭션 전체가 롤백되므로 부분적으로 적용된 스키마 변경 상태로 애플리케이션이 시작하지 않습니다.
얻은 이점
맞춤형 실시간 기능을 갖춘 자체 호스팅 PostgreSQL로의 이동에는 상당한 엔지니어링 투자가 필요했습니다. 그러나 얻은 이점도 컸습니다.
신뢰성. 기초 데이터 변경 후 몇 밀리초 내에 실시간 알림이 도착합니다. WAL 복제 지연도 없고 핵심 경로의 제삼자 서비스 중단도 없습니다.
디버깅 가능성. 문제가 생기면 트리거 실행, 알림 전송, 이벤트 버스 라우팅, SSE 전달이라는 전체 경로를 추적할 수 있습니다. 모든 단계가 당사 코드, 로그, 인프라에 있습니다.
유연성. 새 테이블에 실시간 기능을 추가하려면 트리거를 설치하는 SQL 마이그레이션 하나면 됩니다. 플랫폼 구성, 제삼자 대시보드, 지원 요청이 필요하지 않습니다.
성능. 조정된 풀을 사용하는 직접 PostgreSQL 연결은 관리형 플랫폼 인프라를 경유하는 연결보다 뛰어납니다. 이전 후 질의 지연이 의미 있게 감소했습니다.
대신 운영 책임을 집니다. 데이터베이스를 관리하고 백업을 처리하며 복제를 모니터링하고 용량을 계획합니다. PostgreSQL 운영에 익숙한 팀이라면 감당할 수 있습니다. 익숙하지 않은 팀이라면 관리형 플랫폼의 추상화가 그 한계를 감수할 가치가 있을 수 있습니다.
Atlas Verified는 맞춤형 실시간 인프라를 갖춘 자체 호스팅 PostgreSQL에서 운영되며 세계 공급망 전반에서 매일 수천 건의 무역 규정 준수 검증을 처리합니다.
