개발 공부/프로젝트

대용량 트래픽과 비동기 처리, 왜 RabbitMQ 대신 Kafka였을까?

baby-t 2026. 3. 10. 11:28

1. 들어가며: 정합성은 얻었지만, 속도를 잃다

지난 1편에서 Redis 분산 락(Redisson)을 도입하여 가상 자산 주문 시 발생하는 동시성 문제를 해결하고 데이터 정합성을 확보했습니다. 하지만 기쁨도 잠시, 락(Lock)을 활용한 동기(Synchronous) 처리 방식의 치명적인 한계를 마주하게 되었습니다.

2. 문제 상황: 트래픽이 몰리자 서버가 멈춰버렸다

실제 운영 환경과 유사한 부하를 주어 시스템의 한계를 테스트해보고 싶었습니다. 매수 주문 시 다른 API 요청 등으로 로직 처리에 시간이 걸린다는 가정하에 임시로 2초의 대기 시간을 추가하고, JMeter를 활용해 테스트를 진행했습니다.

  • 테스트 시나리오: 1,000명의 유저가 동시에 주문 요청
  • 결과: 평균 응답 시간(Average Response Time) 6.7초 (6733ms)

하나의 서버가 모든 요청을 동기식으로 혼자 처리하다 보니, 트래픽이 급증하는 상황에서 락을 획득하기 위해 대기하는 스레드들이 쌓이며 **심각한 병목 현상(Thread Blocking)**이 발생한 것입니다. 유저 입장에서는 주문 버튼을 누르고 6초 넘게 멈춰있는 화면을 봐야 하는 최악의 경험이었습니다.

3. 해결 방안 탐색: 비동기 처리와 Message Queue

사용자에게는 "주문이 접수되었습니다"라는 빠른 응답을 먼저 주고, 실제 복잡한 매수 로직은 백그라운드에서 처리하는 **비동기 기반의 이벤트 주도 아키텍처(Event-Driven Architecture)**가 필요했습니다.

이를 위해 메시지 브로커(Message Broker) 도입을 검토했고, 대표적인 두 기술인 RabbitMQApache Kafka를 비교했습니다.

  • RabbitMQ: 메시지 라우팅 기능이 강력하고 구성이 쉽습니다. 하지만 메시지가 소비되면 큐에서 삭제되는 구조라 장애 복구(Replay)에 한계가 있고, 대규모 트래픽 처리량(Throughput)에서는 상대적으로 아쉬움이 있습니다.
  • Apache Kafka: 디스크에 메시지를 순차적으로 저장(Append-only)하여 처리량이 압도적으로 높고, 장애 발생 시 언제든 이벤트를 다시 읽어올 수 있습니다.

결론: 압도적인 처리량과 순서 보장, Apache Kafka의 승리 가상 자산 거래소는 트래픽이 순간적으로 폭주할 수 있는 환경입니다. 따라서 대용량 트래픽을 안정적으로 삼켜줄 수 있는(Buffer 역할) Kafka를 최종 선택했습니다. 하지만 Kafka를 선택한 가장 결정적인 이유는 따로 있었습니다.

4. 핵심 트러블 슈팅: Kafka Partition Key를 통한 '기능 단순화'

Kafka를 도입하여 Producer가 주문 접수 메시지만 빠르게 던지고, Consumer가 뒤에서 로직을 처리하도록 분리했습니다. 이때 아주 흥미로운 사실을 발견했습니다.

바로 **"Kafka의 파티션 키(Partition Key)를 활용하면 Redis 분산 락을 쓸 필요가 없다!"**는 것이었습니다.

Kafka는 같은 파티션 내에서는 메시지의 순차 처리(Ordering)를 완벽하게 보장합니다. 주문 요청 메시지를 발행할 때 유저의 ID(UserId)를 Partition Key로 설정하면, 동일한 유저의 주문은 항상 같은 파티션으로만 들어가게 됩니다.

  • Before: 여러 스레드가 동시에 한 유저의 잔액에 접근 ➡ 동시성 이슈 ➡ Redis 분산 락 필요
  • After (Kafka 도입): 한 유저의 주문은 무조건 한 줄로 세워져서 순서대로 들어옴 ➡ 동시성 이슈 원천 차단 ➡ Redis 분산 락 제거!

복잡했던 Redis 락 로직을 걷어냄으로써, 코드는 훨씬 단순해졌고 시스템의 복잡도(Complexity)는 획기적으로 낮아졌습니다. 기술을 추가하면서 오히려 로직을 덜어내는 짜릿한 경험이었습니다.

5. 결과 검증: TPS 1,500+ 달성과 0.5초의 마법

리팩토링을 마치고 다시 JMeter를 돌려보았습니다.

  • 1차 테스트 (1,000명 동시 요청): 평균 응답 시간 6.7초 ➡ 0.5초(555ms) 로 무려 12배 단축!
  • 2차 대용량 부하 테스트 (10,000건 동시 요청): 

결과는 완벽했습니다. 서버 중단이나 에러 없이(Error 0%), **초당 1,500건 이상(TPS 1,500+)**의 대량 매수 요청을 끄떡없이 소화해 내는 튼튼한 서버를 구축할 수 있었습니다.

6. 마무리하며

이번 최적화 과정을 통해 "좋은 기술을 덕지덕지 붙이는 것보다, 각 기술의 철학과 특징(Partition Key의 순서 보장)을 정확히 이해하고 기존의 복잡함을 덜어내는 것이 진짜 엔지니어링이다"라는 값진 교훈을 얻었습니다.

하지만 Kafka, Redis, MySQL 등 수많은 인프라 요소들이 추가되면서 로컬 환경에서 이를 하나하나 띄우고 관리하는 것이 새로운 고통으로 다가왔습니다.

다음 마지막 3편에서는 이 얽히고설킨 인프라 환경을 한 방에 우아하게 관리하기 위한 Docker 도입기를 다뤄보겠습니다.