“Postgres만으로 충분하다”거나 “당신 규모엔 Kafka가 필요 없다”는 주장에 반박하며, 두 도구가 설계 목적부터 다르다는 점과 이벤트 스트리밍에 필요한 핵심 역량(로그 의미론, 고가용성, 컨슈머 그룹, 저지연, 커넥터 생태계, 개발자 경험)을 짚어봅니다. 규모가 작더라도 올바른 도구를 고르고, 필요 시 CDC와 아웃박스 패턴으로 Postgres와 Kafka를 함께 사용하는 접근을 권합니다.
HackerNews 첫 페이지에 올라가고 싶나요? 그렇다면 “Postgres만으로 충분하다”, 혹은 “당신 규모에서는 Kafka가 필요 없다”는 글을 쓰는 게 거의 확실한 방법입니다. 얼마나 자주 반복되었든, 이 주제는 늘 성과가 좋죠. 좋아할 만한 요소가 다 들어 있으니까요. 모두가 가장 좋아하는 RDBMS인 Postgres—체크! 간결하고 쉬운 걸 선호한다—좋아, 나도 끼워줘! 약간 도발적인 관점—좋아, 받아들이지!
하지만 제 생각에 이런 글들은 요점을 비껴갑니다. Postgres와 Kafka는 애초에 매우 다른 목적을 위해 설계된 도구이고, 당연히 어떤 도구를 쓸지는 해결하려는 문제에 크게 좌우됩니다. “Kafka는 필요 없고, 그냥 Postgres를 써라”는 조언은 선의와 달리 해를 끼쳐, 최적과 거리가 먼 시스템을 만들게 하고, 왜 그런지 이 글에서 자세히 이야기해보려 합니다. 먼저 분명히 해둘 것이 있습니다. 이 글은 반(反) Postgres 글이 아닙니다. 저는 Postgres(그 본연의 용도에 대해선) 다른 사람 못지않게 좋아합니다. 예전 직장에서도 썼고, 이 블로그에서 관련 글 여러 편을 썼습니다. 이 글은 “일에 맞는 올바른 도구를 쓰자”는 입장입니다.
그렇다면 “Kafka는 필요 없고, 그냥 Postgres를 써라”는 글들의 논지는 무엇일까요? 보통 Kafka는 운영이 어렵거나, 운영 비용이 비싸거나, 혹은 둘 다라고 주장합니다. “빅데이터”가 아니라면 그 비용을 정당화하기 어렵다는 것이죠. 게다가 이미 기술 스택에 Postgres가 있다면, 굳이 다른 기술을 추가하지 말고 그것만 쓰자는 겁니다.
대개 이런 글들은 이어서 SELECT ... FOR UPDATE SKIP LOCKED를 이용해 잡 큐를 만드는 법을 보여줍니다. 여기서부터 제게는 조금 덜 합리적으로 보입니다. 왜냐하면 큐잉은 애초에 Kafka의 전형적인 사용 사례가 아니기 때문입니다. 큐는 메시지 단위의 소비자 병렬성과 개별 메시지의 확인(ack)이 필요하지만, Kafka는 역사적으로 이를 지원하지 않았습니다. 사실 Kafka 커뮤니티는 KIP-932를 통해 큐 지원을 추진 중이고, 저는 올해 초 그 KIP를 살펴봤습니다. 하지만 아직 본격적으로 쓸 만한 단계는 아닙니다. 그 전까지는, 애초에 Kafka가 설계되지 않은 용도에 Kafka를 쓰지 말라는 주장으로 귀결됩니다. 음, 그래요, 오케이?
그렇다고 해서 Postgres 위에 견고한 큐를 만드는 일이 쉬운 것도 아닙니다. 큐 소비자의 장기 실행 트랜잭션은 MVCC 블로트와 WAL 누적을 유발할 수 있고, VACUUM이 변경 속도를 따라가지 못하면 이 용도에서 빠르게 문제가 됩니다. 이 길을 가겠다면, 대표성 있는 성능 테스트를 충분한 기간 동안 꼭 돌리세요. 2분짜리 테스트로는 이런 문제들을 절대 발견하지 못합니다.
이제 “소규모” 논지를 조금 더 자세히 보죠. “데이터량이 적으니 Postgres를 쓰면 된다”라는데, 도대체 무엇을 위해 쓰려는 건가요? 해결하려는 문제가 무엇인지가 중요합니다. 결국 Postgres와 Kafka는 특정 사용 사례를 풀기 위해 설계된 도구입니다. 하나는 데이터베이스, 다른 하나는 이벤트 스트리밍 플랫폼이죠. 목표가 무엇인지 모른 채 이야기하면, 논의는 “난 이 도구가 저 도구보다 좋아” 수준으로 떨어지고 의미가 없어집니다.
Kafka는 마이크로서비스 간 통신과 데이터 교환, IoT 센서 데이터 수집, 클릭스트림이나 메트릭, 로그 처리와 집계, 운영 데이터베이스와 데이터 레이크/웨어하우스 간 저지연 데이터 파이프라인, 실시간 스트림 처리(예: 사기 탐지나 추천 시스템) 등 매우 다양한 용도를 가능케 합니다.
그렇다면 이런 사용 사례가 있는데 규모가 작다면, Kafka 대신 Postgres를 쓸 수 있을까요? 그리고 그게 합당할까요? 이를 답하려면, Kafka가 이런 애플리케이션에 적합하게 만드는 역량과 기능을 살펴봐야 합니다. 확장성이 Kafka의 핵심 특성 중 하나인 것은 맞지만, 이벤트 스트리밍 애플리케이션에 매력적인 이유는 그 외에도 많습니다.
로그 의미론: Kafka의 본질은 영속적이고 순서가 보장되는 이벤트 로그입니다. 레코드는 처리 후 삭제되지 않으며, 시간 기반 보존 정책이나 키 기반 컴팩션의 대상이 되거나 영구 보존될 수 있습니다. 컨슈머는 원하는 오프셋부터, 혹은 처음부터 토픽을 재생(replay)할 수 있습니다. 필요하다면 정확히 한 번 의미론도 구현할 수 있습니다. 이는 단순한 큐 의미론을 훨씬 넘어서는 것으로, Postgres 위에서 이를 복제(replicate)하려면 상당한 노력이 듭니다.
내결함성 및 고가용성(HA): Kafka 워크로드는 여러 컴퓨팅 노드에 걸친 클러스터로 스케일 아웃됩니다. 이유는 두 가지입니다. 시스템이 처리할 수 있는 처리량을 높이기 위해(소규모에선 중요하지 않을 수 있음) 그리고 신뢰성을 높이기 위해(소규모에도 매우 중요)입니다. 데이터를 여러 노드에 복제하므로 인스턴스 장애를 쉽게 감내할 수 있습니다. 클러스터의 각 노드는 어떤 토픽 파티션의 리더가 될 수 있고(즉, 쓰기를 받음), 이전 리더가 사라지면 다른 노드가 자동으로 승계합니다.
반면 Postgres는 모든 쓰기가 단일 노드로 향하고, 리플리카는 읽기만 지원합니다. Kafka에서 브로커의 페일오버는 그 브로커가 리더로 맡고 있던 파티션에만(지연 증가 형태로) 영향을 주지만, Postgres 클러스터에서 프라이머리 노드가 장애가 나면 모든 쓰기 요청이 영향을 받습니다. Kafka의 브로커 페일오버는 자동으로 일어나지만, Postgres에서 리플리카를 프라이머리로 승격하려면 수동 개입이 필요하거나 Patroni 같은 외부 코디네이터가 필요합니다. 혹은 CockroachDB 같은 Postgres 호환 분산 DB를 고려할 수도 있지만, 그러면 논의는 더 이상 “그냥 Postgres만 쓰자”와는 거리가 멀어집니다.
컨슈머 그룹: Kafka 프로토콜의 강점 중 하나는 컨슈머를 그룹으로 조직화하는 지원입니다. 여러 클라이언트가 특정 토픽에서 메시지를 읽는 부하를 분산하면서, 각 메시지는 그룹의 정확히 한 멤버에게만 처리되도록 합니다. 메시지량이 적어도 이는 매우 유용합니다. 예를 들어 어떤 마이크로서비스가 다른 서비스로부터 메시지를 받는다고 합시다. 내결함성을 위해 이 서비스를 여러 인스턴스로 스케일 아웃했다면, 모든 인스턴스를 하나의 Kafka 컨슈머 그룹으로 묶기만 하면 들어오는 메시지가 인스턴스들 사이에 분산됩니다.
Postgres라면 어떨까요? “소규모” 시나리오라며 서비스 인스턴스 중 하나만 모든 메시지를 읽게 할 수 있다고 가정해봅시다. 그런데 어떤 인스턴스를 선택하죠? 그 노드가 죽으면요? 일종의 리더 선출이 필요합니다. 그렇다면 애플리케이션 클러스터의 각 멤버가 모두 토픽을 소비하게 만들까요? 그러려면 Postgres 기반 토픽에서 메시지를 어떻게 분배할지, 클라이언트 장애는 어떻게 처리할지 등을 고민해야 합니다. 결국 Kafka의 컨슈머 리밸런스 프로토콜을 재구현하는 셈입니다. 이는 결코 사소하지 않고, 처음 목표였던 “단순함”에도 정면으로 배치됩니다.
저지연: 이제 지연시간(메시지를 토픽에 보낸 순간부터 컨슈머가 처리할 때까지 걸리는 시간)을 이야기해봅시다. 데이터량이 적다고 해서 저지연이 필요 없다는 뜻은 아닙니다. 예컨대 사기 탐지를 생각해보세요. 초당 몇 건 안 되는 트랜잭션만 처리하더라도, 의심스러운 패턴을 최대한 빨리 감지하고 대응해야 합니다. 혹은 운영 데이터 스토어에서 검색 인덱스로 가는 데이터 파이프라인도 마찬가지입니다. 좋은 사용자 경험을 위해서는 검색 결과가 가능한 최신 데이터를 반영해야 하죠. Kafka라면 이런 용도에서 밀리초 단위의 지연을 달성할 수 있습니다. Postgres로 같은 수준을 내려면, 가능하더라도 정말 어렵습니다. 폴링 기반 큐 클라이언트 무리가 너무 자주 데이터베이스를 두들기게 만들고 싶지는 않을 겁니다. 한편 LISTEN/NOTIFY는 심각한 락 경합 문제로 알려져 있습니다.
커넥터: “그냥 Postgres 쓰자” 류의 글에서 보통 빠지는 중요한 측면이 연결성입니다. 데이터 파이프라인과 ETL 사용 사례를 구현하려면, 데이터 소스에서 데이터를 꺼내 Kafka로 넣어야 하고, 거기서 다시 각종 싱크로 흘려보내야 합니다. 하나의 동일한 데이터셋이 종종 동시에 여러 싱크(예: 검색 인덱스와 데이터 레이크)로 흘러가죠. Kafka Connect를 통해 Kafka는 소스/싱크 커넥터의 방대한 생태계를 갖고 있으며, 이를 믹스앤매치 방식으로 조합할 수 있습니다. MySQL에서 Iceberg로? 쉽습니다. Salesforce에서 Snowflake로? 물론이죠. 세상에 존재하는 거의 모든 데이터 시스템에 대한 준비된 커넥터가 있습니다.
Postgres를 쓰면 어떨까요? Kafka처럼 Postgres에는 커넥터 생태계가 없습니다. 이는 Postgres가 데이터 통합 플랫폼으로 설계된 적이 없었다는 점에서 당연하지만, 그 말은 곧 당신이 통합하려는 모든 시스템에 대해 맞춤형 소스/싱크 커넥터를 구현해야 함을 뜻합니다.
클라이언트, 스키마, 개발자 경험: “그냥 Postgres로 이벤트 스트리밍을 하자”는 해법의 일반적인 프로그래밍 모델도 짚고 가겠습니다. 메시지 생산과 소비의 주 인터페이스로 SQL을 쓰고 싶어질 겁니다. 얼핏 쉬워 보이지만, 매우 로우 레벨입니다. 결국 일종의 클라이언트를 만들 필요가 있을 겁니다. 앞서 논의한 컨슈머 그룹 지원이 필요할 수도 있죠. 메트릭과 옵저버빌리티(“내 컨슈머 랙은 얼마지?”) 지원도 필요합니다. 이벤트를 영속 포맷으로 어떻게 직렬화할까요? 직렬화/역직렬화(serdes) 인프라가 필요하고, 가능하다면 스키마 관리와 진화도 지원해야겠죠. DLQ 지원은요? Kafka와 그 생태계는 이런 모든 것을 도와주는 검증된 클라이언트와 도구를, 다양한 프로그래밍 언어로 제공합니다. 물론 이 모든 것을 직접 다시 만들 수는 있지만, 오래 걸릴 뿐 아니라 결국 Kafka와 그 생태계의 상당 부분을 재창조하는 일입니다.
그렇다면 결론은 무엇일까요? Postgres를 잡 큐로 써야 할까요? 그게 요건에 맞는다면, 물론 쓰세요. 다만 직접 만들지는 말고 pgmq 같은 기존 확장을 쓰세요. 그리고 앞서 논의한 MVCC 블로트와 VACUUM 이슈의 잠재적 영향을 반드시 이해하세요.
이제 이벤트 스트리밍 플랫폼으로 Kafka 대신 Postgres를 쓰는 문제로 오면, 데이터량이 얼마가 되든 제게는 전혀 합리적으로 보이지 않습니다. “그냥 Postgres 쓰자” 글에서 보통 논의되는 것보다 이벤트 스트리밍에는 훨씬 많은 것들이 필요합니다. 어떤 과제는 한동안 미뤄둘 수 있겠지만, 결국 Postgres 위에 당신만의 Kafka를 다시 만드는 사업에 발을 들이게 될 겁니다. 그런데 수년간 수백 명의 기여자가 이미 만들어놓은 것을 왜 다시 만들고 유지하나요? “단순함을 지키자”로 시작한 일이 사실 불필요한 복잡성을 대량으로 끌어들이게 됩니다. 순수한 엔지니어링 관점에선 재미있을지 몰라도, 대부분의 조직에겐 초점을 맞출 올바른 문제가 아닙니다.
“소규모” 논지의 또 다른 문제는, 오늘은 적은 데이터량이 다음 주에는 훨씬 커질 수 있다는 점입니다. 물론 이는 트레이드오프지만, 흔히 하는 조언은 현재와 그다음 차수(order of magnitude)의 부하를 견딜 시스템을 만들라는 것입니다. 즉, 비즈니스가 성장해도 현재 부하와 데이터량의 10배를 감당할 수 있어야 합니다. 이는 확장성을 핵심으로 설계된 Kafka에선 비교적 쉬운 일입니다. 반면 Postgres 기반 큐 구현은 훨씬 어려울 수 있습니다. 앞서 말했듯 단일 쓰기 노드 구조이므로, 수직 확장에 의존하게 되고 이는 비용이 매우 빠르게 커집니다. 결국 Kafka로 마이그레이션하기로 결정할 수도 있는데, 데이터 이전, 애플리케이션을 자가 제작한 클라이언트에서 Kafka로 옮기는 일 등, 그 비용은 상당합니다.
결국 요지는 “일에 맞는 도구를 고르자”입니다. 관계형 데이터셋을 관리하고 질의하려면 Postgres를 쓰고, 실시간 이벤트 스트리밍이 필요하면 Kafka를 쓰세요. 즉, 많은 경우 둘을 함께 쓰는 것이 전체적인 해법으로 맞습니다. 서비스 내부 상태 관리는 Postgres, 다른 서비스들과의 데이터/이벤트 교환은 Kafka. 둘 중 하나로 다른 하나를 흉내 내려 하지 말고, 각자의 강점을 위해 쓰세요. 이때 Postgres와 Kafka를 어떻게 동기화할까요? 변경 데이터 캡처(CDC), 특히 아웃박스 패턴이 도움이 됩니다. “Kafka 대신 Postgres”가 설 자리가 있다면, 사실 여기입니다. 많은 경우 애플리케이션에서 Kafka에 직접 쓰지 말고, 먼저 데이터베이스에 쓰고 나서 CDC로 Kafka에 이벤트를 내보내는 편이 낫습니다. Debezium 같은 도구를 쓰면 됩니다. 이렇게 하면 두 리소스는 (결국) 일관되게 유지되고, 애플리케이션 개발자 관점에선 매우 단순해집니다.
이 접근은 또한 운영 데이터 스토어를 다운스트림 이벤트 컨슈머의 잠재적 영향으로부터 분리(및 보호)하는 이점이 있습니다. 다른 팀이 소유한 데이터 레이크 적재 프로세스가, 하필이면 당신 서비스 DB의 테이블에서 전체 토픽을 다시 읽는 바람에, 운영 REST API의 꼬리 지연이 늘어나는 위험은 피하고 싶겠죠. 동기성 예산의 개념을 따르자면, 서로 다른 관심사들을 다루는 시스템은 분리하는 것이 합리적입니다.
운영 오버헤드는 어떨까요? 충분히 고려할 가치가 있지만, 종종 그 우려는 과장되어 있다고 봅니다. 소규모 데이터셋을 위해 Kafka를 운영하는 일은 정말 어렵지 않습니다. ZooKeeper에서 KRaft 모드로 전환된 이후, 내결함성이 필요 없는 시나리오에선 단일 Kafka 인스턴스 운영이 매우 간단해졌습니다. 매니지드 서비스는 Kafka 운영을 말 그대로 아무 일도 일어나지 않는(말장난이지만) 경험으로 만들어주며, 특히 소규모로 시작할 때는 최우선 선택이어야 합니다. 데이터량이 적다는 사실 자체가 비용을 관리 가능하게 만들어줍니다. 게다가 앞서 논의한, 자가 구현에서 발생하는 온갖 문제를 해결하는 시간과 노력도 총소유비용(TCO) 계산에 반드시 포함되어야 유의미합니다.
그러니 네, HackerNews 첫 페이지에 오르고 싶다면 “Postgres만으로 충분하다”는 주장이 도움이 될 겁니다. 하지만 현실 세계의 문제를 효과적이고 견고하게 풀고 싶다면, 도구의 강점과 한계를 이해하고, 일에 맞는 도구를 고르세요.