기술 도입의 핵심은 '사용법(How)'이 아니라, 그 기술이 존재해야하는 '이유(Why)'였다. 기술을 선택할 때는 "이 문제 해결에 가장 적합하다는" 명확한 근거가 있어야 한다.
내가 직접 선택하지 않았다는 핑계로 선택의 이유조차 모른채 넘어간다면, 그 기술을 온전히 '내 것'으로 만들 수 없다. 이 글은 과거의 기억을 따라가며, 우리 조직이 왜 이 기술을 선택했는지 스스로 답을 찾아가는 과정이다.
Monolithic에서 MSA로, 필연적이었던 선택
입사 당시 서비스는 Rails 기반의 거대한 모놀리식(Monolithic) 구조였다. 모든 데이터는 하나의 DB에 모여 있었고, 서비스 간 통신이라는 개념 자체가 희미했다.
하지만 트래픽이 급증하면서 한계가 찾아왔다. 단순히 버티는 것을 넘어, 폭발하는 트래픽을 유연하게 받아내고 각 도메인이 서로의 발목을 잡는 의존성(Dependency) 없이 10배(10x) 이상 성장할 수 있는 환경이 필요했다. 이를 위해 우리는 거대한 모놀리식을 여러 개의 마이크로서비스(MSA)로 쪼개는 대대적인 전환을 시작했다.
그때 우리 팀은 문자 그대로 '거대한 변화의 파도 한가운데' 있었다. 숙소, 주문, 결제 같은 핵심 버티컬들이 하나둘씩 분리되어 나가는 와중에도 서비스는 멈추지 않고 돌아가야 했고, 우리의 역할은 그 혼란스러운 파도 속에서 분리되는 시스템과 남겨진 시스템을 끊어지지 않게 이어주는 것이었다.
내 카프카 경험은 이 복잡한 공존의 시기에 진행된 '회원 이관 프로젝트'에서 시작한다.
- 흩어진 데이터를 아우르는 '회원 탈퇴'
회원 도메인이 분리되면서 가장 난관은 '탈퇴 프로세스'였다. 기존에는 회원 탈퇴 시 연관된 모든 데이터를 삭제하거나 이관하는 로직이 하나의 데이터베이스 트랜잭션으로 묶여 있어 처리가 간단했다. 하지만 시스템이 MSA로 분화되면서 이 '원자성(Atomicity)'이 깨졌다.
유저가 탈퇴 버튼을 누르면, 분리된 예약 도메인의 데이터 내역뿐만 아니라, 레거시 시스템에 남아 있는 투어 예약 정보나 각종 레거시 데이터까지 모두 찾아서 정리해야 했다.
물리적으로 쪼개진 데이터베이스들 사이에서, 동기식 API 호출만으로는 데이터의 정합성을 보장하기에 명확한 한계가 있었다.
- 복잡한 의존성: 회원 서비스가 2.0 레거시와 3.0 신규 서비스들의 API를 일일이 호출하며 성공 여부를 확인하기엔 결합도가 너무 높았다.
- 트랜잭션의 부재: 물리적으로 나뉜 DB(Rails DB, MSA DBs) 간의 데이터 처리를 하나의 트랜잭션으로 묶을 수도 없었다.
- 확장성 저하: 탈퇴 후 처리해야 할 도메인(항공, 숙박, 정산, 혜택 등)이 늘어날 때마다, 회원 서비스의 탈퇴 API 코드를 계속 수정해야 한다.
이 문제를 해결하기 위해, 우리는 사내 표준으로 구축된 Kafka 기반의 이벤트 기반 아키텍처(EDA)를 적용했다. 핵심은 "탈퇴했다는 사실(Event)"만 발행(Produce)하면, 필요한 세부 도메인들이 각자 알아서 이벤트를 가져가(Consume) 처리하게 만드는 것이었다.
- 왜 RabbitMQ가 아닌 Kafka였을까?
단순히 메시지를 큐에 넣고 빼는 것이 목적이라면, RabbitMQ의 Pub/Sub 모델로도 느슨한 결합은 충분히 구현할 수 있다. 하지만 왜 우리는 굳이 Kafka를 선택했을까?
여러 기술적 이유가 있겠지만, 나는 우리가 처한 '과도기적 상황'이 결정적이었다고 생각한다.
시스템 이관 중에는 예상치 못한 데이터 누락이 발생하거나, 뒤늦게 2.0 시스템의 특정 테이블도 함께 정리해야 한다는 사실을 발견하곤 했다. 만약 소비되면 사라지는 휘발성 큐(RabbitMQ)였다면, 이미 처리되어 사라진 탈퇴 이벤트를 다시 살려낼 방법이 없다.
하지만 Kafka는 이벤트를 디스크에 '로그(Log)' 형태로 남겨둔다. 덕분에 로직이 변경되거나 새로운 연결이 필요할 때, 언제든 과거 시점(Offset)부터 이벤트를 다시 읽어(Replay) 놓친 데이터를 처리할 수 있었다.
끊임없이 변화하고 이관되는 시스템 속에서, 데이터를 흘려보내지 않고 '기록'해 두는 것. 그것이 이 혼란스러운 과도기를 버티게 해 준 Kafka의 진짜 존재 이유가 아니었을까.
구조로 이해하는 선택의 이유
단순히 "좋아서"가 아니라 "맞아서" 썼다는 확신을 가지려면, 그 기술의 내부 구조를 들여다봐야 한다. 카프카는 스스로를 단순한 메시지 큐(MQ)가 아닌 '분산 이벤트 스트리밍 플랫폼'이라 정의한다. 이 정의 속에 우리가 카프카를 선택할 수밖에 없었던 이유가 숨어있다.
1) 멍청한 브로커(Dumb Broker)와 압도적 성능
우리가 흔히 아는 RabbitMQ 같은 메시지 큐는 '똑똑한 브로커(Smart Broker)'를 지향한다. 브로커가 메시지 전달을 보장하고, 누가 읽었는지 추적하고, 라우팅까지 계산한다. 기능은 강력하지만, 트래픽이 폭주하면 브로커의 CPU와 메모리에 병목이 생길 수밖에 없다.
반면 카프카는 '멍청한 브로커(Dumb Broker)'를 자처한다. 브로커는 복잡한 계산을 하지 않는다. 메시지가 들어오면 메모리에서 큐를 관리하는 대신, 단순히 파일 시스템에 순차적으로 기록(Append Only)할 뿐이다.
이러한 설계 덕분에 브로커는 복잡한 연산 없이 압도적인 처리량(Throughput)을 감당할 수 있다. 10x 성장을 목표로 하는 우리 서비스 환경에서, 브로커가 병목이 되지 않고 무한정 이벤트를 받아낼 수 있는 이 '단순함'이 핵심적인 이유였을 것이다.
2) 토픽과 파티션(Partition): 무한한 수평 확장
단일 큐 하나로는 처리량의 물리적 한계가 명확하다. 카프카는 이를 해결하기 위해 '토픽(Topic)'이라는 논리적 개념을 '파티션(Partition)'이라는 물리적 단위로 쪼갰다.
- 병렬 처리: '회원 탈퇴'라는 하나의 토픽을 여러 파티션으로 나누면, 여러 컨슈머가 동시에 달라붙어 병렬로 처리할 수 있다.
- 분산 저장: 파티션들은 여러 브로커 서버에 분산되어 저장된다. 즉, 트래픽이 늘어나면 장비(Broker)만 추가하여 수평적 확장(Scale-out)이 가능하다.
이는 서비스가 성장함에 따라 인프라를 유연하게 늘릴 수 있음을 의미한다. 폭발적인 트래픽 증가를 대비해야 했던 당시의 우리에게, 파티션은 미래를 위한 보험과도 같았다.
3) 오프셋(Offset): 소비자의 자율성과 복구 능력
가장 매력적인 점은 "어디까지 읽었는가(Offset)"를 관리하는 주체가 브로커가 아닌 컨슈머(Consumer)라는 점이다.
- 속도 조절: 컨슈머는 자신의 처리 능력에 맞춰 메시지를 읽어간다. 시스템 부하가 심할 때는 천천히 읽고, 여유가 있을 때는 빠르게 처리할 수 있다.
- 장애 복구(Replay): 컨슈머 서비스가 로직 오류로 죽거나 데이터 처리를 실패하더라도, 오프셋만 과거로 돌리면(Reset) 언제든 다시 읽을 수 있다.
생산자(Producer)와 소비자(Consumer) 사이의 완벽한 디커플링(Decoupling). 소비하는 쪽의 장애가 생산하는 쪽에 전혀 영향을 주지 않고, 언제든 다시 시작할 수 있는 구조. 이것이 MSA 전환의 핵심 목표와 정확히 맞아 떨어졌을 것이다.
결론: 이유 있는 선택이 기술을 완성한다
카프카를 사용하면서 느낀 가장 큰 효용은 '책임의 분리'였다. 회원 팀은 탈퇴 이벤트를 발행하는 것으로 책임을 다하고, 예약 팀이나 정산 팀은 그 이벤트를 구독하여 자신의 로직을 수행한다. 서로의 존재를 몰라도 시스템은 유기적으로 돌아간다.
과거의 나는 이 구조가 단순히 "주어져서" 사용했다. 하지만 그 이면에는 대용량 처리, 확장성, 그리고 결합도 감소라는 명확한 설계 의도가 있었다. 기술을 선택하는 위치가 아니더라도, "왜 이 기술이어야만 했는가?"를 고민하는 과정은 나를 단순한 '사용자'에서 '엔지니어'로 성장시켰다.
다만, 이러한 장점들에도 불구하고 카프카가 모든 문제를 해결하는 '만능열쇠(Silver Bullet)'는 아니다. 모든 기술적 선택에는 트레이드오프(Trade-off)가 따르며, 카프카 역시 도입과 동시에 무거운 숙제들을 남긴다.
- 오버 엔지니어링과 관리 부채: 단순한 큐가 필요한 곳에 카프카를 도입하는 것은 닭 잡는 데 소 잡는 칼을 쓰는 격일 수 있다. 주키퍼(ZooKeeper)나 KRaft 등의 관리부터 브로커 운영까지, 유지보수 비용은 결코 가볍지 않다.
- 확장성의 함정 (Hot Key): 파티션을 늘리면 무한히 확장될 것 같지만, 특정 파티션에만 데이터가 쏠리는 '핫 키(Hot Key)' 이슈가 발생하면 단순한 스케일 아웃으로는 해결되지 않는다.
- '정확히 한 번(Exactly-Once)'의 환상: 카프카가 정확히 한 번 전송을 지원한다고 해도, 실제 서비스 환경(DB+Kafka)에서는 네트워크 단절이나 컨슈머 장애로 인해 결국 '최소 한 번(At-Least-Once)' 처리를 가정해야 한다. 특히 결제나 예약 변경처럼 중복 처리가 치명적인 도메인에서는 단순한 설정을 넘어 아웃박스 패턴이나 멱등성(Idempotency) 보장 로직이 필수적으로 요구된다.
- 오프셋은 만능인가?: '다시 읽을 수 있다'는 장점은 역설적으로 '중복 처리'나 '순서 보장 실패'라는 새로운 버그를 낳기도 한다.
이러한 '운영의 현실'은 "왜(Why)"를 이해한 뒤에 마주해야 할 "어떻게(How)"의 영역이다. 완벽한 기술은 없다. 다만 우리 상황에 맞는 최선의 선택을 찾을 뿐. 원래라는 것은 없고 그 선택에는 이유가 있어야 한다.
언제 기회가 된다면 카프카 현실편을 작성해봐야겠다.