개요
20,000 TPS 환경에서 평균 500ms의 응답속도를 보장하는
쿠폰 서비스를 설계하며 고민했던 내용을 정리하려 한다.
기능은 이벤트성 선착순 쿠폰 발급과 쿠폰 관리 기능으로 나뉘며,
특히 선착순 쿠폰 발급의 동시성 문제와 대량 트래픽을 안정적으로 처리하는 설계가 주요 도전 과제이다.
쿠폰 서비스 설계
쿠폰
이라는 도메인은 코드의 변동 가능성, 타 서비스와의 연동 및 호출 빈도를 기준으로
아래와 같이 두 기능으로 나눌 수 있었다.
선착순 쿠폰 발급 이벤트 | 쿠폰 사용 / 관리 | |
---|---|---|
운영 성격 | 단기간 집중 이벤트 중심 | 장기적이고 안정적인 기능 운영 |
요구 사항 변경 빈도 | 높음 | 낮음 |
트래픽 특성 | 단시간에 급격한 트래픽 증가 | 비교적 일정하고 안정적인 트래픽 |
시스템 자원 사용량 | 순간적으로 높은 리소스 소비 | 일관된 자원 사용 |
외부 시스템 연동 | 상대적으로 독립적 | 타 서비스와의 통합 및 데이터 연동 중요 |
단기간에 급격한 트래픽이 발생하는 이벤트성 선착순 쿠폰 발급기능,
여러 서비스와의 데이터 연동을 통해 안정적인 트래픽이 발생하는 쿠폰 사용 및 관리 기능이다.
두 기능은 변경 시점, 이유 등의 차이를 보이기에 분리가 필요한 관심사라고 생각했다. 따라서 앞서 설명한 근거에 따라 변경 용이성, 확장성, 성능 최적화를 위해 쿠폰 서비스를 두 가지 서비스(Coupon, Promotion)로 분리했다.
- 이벤트 중심 서비스(promotion): 변동성이 크고 호출 빈도가 낮은 선착순 쿠폰 발급 기능을 처리
- 관리 중심 서비스(coupon): 변동성이 낮고 호출 빈도가 높은 쿠폰 사용 및 관리 기능을 처리
이제 '20,000 TPS 환경에서 평균 500ms의 응답속도를 보장' 이라는 요구사항은 선착순 쿠폰 발급을 처리하는
Promotion 서비스에 조금 더 포커싱될 필요가 있다.
또한, 기존에 REST API 방식으로 통신하던 Coupon 과 Promotion 의 IPC를 이벤트 기반 비동기 통신으로 변경하였다. 비동기 통신을 채택한 첫 번째 이유는 두 서비스 간의 물리적인 의존성을 분리를 통해 독립적인 개발 및 배포가 가능하도록 하기 위함이며, 두 번째 이유는 coupon 서비스의 latency를 promoton에게 전파하지 않기 위함이다.
Promotion 서비스는 선착순 쿠폰발급 시 발생하는 동시성 문제를 Redis 싱글 스레드 특성과 Redis Transaction을 통해 제어한다.
선착순 쿠폰 발급에 성공한 사용자의 데이터는 RDB의 Outbox Table에 저장된 이후, Kafka에 발행된다.
20,000 TPS 처리하기
선착순 쿠폰 발급 기능은 최대 20,000 TPS 가 발생한다고 가정했다.
따라서 DB의 부하를 최소화하는 것이 옳다고 판단했으며, Redis 의 SET 과 Reids Transaction 을 통해 동시성 문제를 제어하도록 설계했다.
실제 20,000 TPS 에서 DB Lock을 통해 동시성을 제어해보려 했으나, 비관적 락의 경우 14.7%의 에러율이 발생했으며, 낙관적 락의 경우 동시성 처리 효율이 매우 떨어졌기에 고려 대상에서 제외했다.
Redis 의 싱글 스레드 특성을 활용한 INCR, DECR 명령어로 동시성을 간단하게 제어하는 방법도 존재했지만, 요구사항 중 '선착순 쿠폰은 중복 발급이 불가능하다.' 라는 항목을 지키기 위해 중복 발급 확인을 위한 추가적인 DB Connection이 필요했다. 해당 작업은 모든 트랜잭션에서 이루어져야 하기에 DB에 부하가 발생할 것이라 예상하여 고려 대상에서 제외했다.
DB Clustering 을 사용하지 않은 이유
DB 클러스터 환경에서는 쓰기 작업 시 동기화 지연(Replication Lag) 이 발생할 수 있다.
특히, 선착순 쿠폰 발급과 같이 대량의 트래픽이 유입되며 실시간으로 정확한 발급 여부 확인이 요구되는 트랜잭션에서는 이러한 지연이 다음과 같은 문제를 초래할 수 있다.
1. 선착순 쿠폰이 이미 발급되었음에도 중복 발급이 가능한 현상
2. 쿠폰 수량이 소진되었음에도 발급 가능한 현상
또한, 선착순 쿠폰 발급은 쓰기 중심의 기능으로, DB 클러스터 환경에서 마스터 노드(소스 서버)에 부하가 집중될 가능성이 존재한다.
이는 DB Connection 부하를 분산하고자 하는 아키텍처 설계 목적과 상충하는 요소로 작용할 수 있다.
이러한 이유로, 해당 시스템은 DB 클러스터링 대신 Redis 기반의 동시성 제어 방식을 채택하였다.
결론
서비스를 구조적으로 분리하고 Redis 기반의 비동기 처리와 동시성 제어 방식을 도입한 결과, 20,000 TPS 환경에서도 시스템 안정성과 응답 속도를 개선할 수 있었다.
비록 여전히 일부 에러율(약 4%)이 존재하지만, 초기 대비 큰 성능 향상을 이루었다. 다만, 비동기 통신의 도입으로 이벤트 발급의 원자성을 보장하는 추가적인 로직이 필요하는 등의 작업이 필요했고, 기술 선택의 트레이드오프와 설계의 중요성을 체감한 경험이었다.
다음에는 이벤트 발급의 원자성을 보장하기위해 Transactional Outbox Pattern을 도입한 사례를 정리해보려 한다.
'트러블슈팅' 카테고리의 다른 글
템플릿 메소드 패턴으로 관리 포인트 줄이기 (0) | 2025.02.17 |
---|---|
커서 기반 페이지네이션으로 조회 성능 개선 (0) | 2025.02.12 |
DB의 외부식별자와 내부식별자 분리 (Auto_Increment vs UUID) (0) | 2025.02.10 |
조건부 속성 문제 해결기 (DDD + Factory Method Pattern) (0) | 2025.01.19 |
Spring Event - Listener가 이벤트를 인식하지 못하는 문제 (Type Erasure) (0) | 2025.01.16 |