-
Notifications
You must be signed in to change notification settings - Fork 0
04. Technical Issue
- 쿠폰 1,000장을 선착순으로 발급하는 이벤트를 진행하며, 1,000명 이상의 사용자 요청이 몰려도 1초 이내의 지연 시간으로 안정적인 발급이 가능한 시스템 구축을 목표로 했습니다.
- 최소한의 구성으로 시작하여, 웹 서버와 DB 서버 각 1대로 초기 시스템을 설계한 후, 점진적으로 트래픽을 증가시키며 짧은 시간에 높은 요청이 집중되는 상황에서도 안정적인 서비스 운영이 가능하도록 시스템 구조를 확장하는 경험을 쌓았습니다.
아래와 같은 주요 특징 및 개선점을 통해 시스템 성능을 단계적으로 향상시켰습니다.
주요 특징 / 개선점 | VUser 수 | 대기열 위치 조회 | 쿠폰 발급 신청 | |||
---|---|---|---|---|---|---|
평균 응답 속도 (ms) | 개선률 | 평균 응답 속도 (ms) | 개선률 | |||
1 | 단일서버 | 100 | 78.81 | - | 313.64 | - |
2 | 단일 서버 + 스레드풀, 커넥션풀 튜닝 | 100 | 73.77 | 6% ⬆️ | 268.06 | 15% ⬆️ |
3 | 서버 스케일 아웃 (2대) | 100 | 63.6 | 14% ⬆️ | 185.44 | 26% ⬆️ |
4 | 다중 서버 + Nginx 설정 수정 | 100 | 30.68 | 52% ⬆️ 최총 61% 개선 ⬆️ |
82.35 | 56% ⬆️ 최총 72% 개선 ⬆️ |
5 | 대규모 트래픽 (서버 최적화 상태) | 1,000 | 22.38 | - | 436.2 | - |
6 | - | 1,500 | 41.86 | - | 757.02 | - |
-
쿠폰 1000개, 사용자 2만명 데이터를 넣고 테스트를 진행했습니다.
-
테스트 시나리오
- 사용자는 쿠폰 발급을 신청합니다.
- 신청 후 1초 간 대기 후 대기열 위치 조회 API를 호출하여 결과를 확인합니다.
- 발급 결과가 성공 또는 실패로 확정될 때까지 2초 간격으로 반복 호출합니다.
-
순차성 보장을 위해 성능 테스트용 Redis를 사용했습니다. 쿠폰 발급 신청 시 User ID를 리스트에 저장하고, 대기열 위치 조회 시 리스트에서 데이터를 순차적으로 꺼내 확인할 수 있도록 스크립트를 작성했습니다.
-
테스트 성공 기준
성능 테스트의 성공 기준은 다음 세 가지로 정의했습니다.- HTTP 요청 실패율 : 1% 미만이어야 함
- 쿠폰 발급 API : 평균 응답 시간이 1초 이내
- 대기열 위치 조회 API : 평균 응답 시간이 100ms 이내
-
발급 신청은 짧은 시간 안에 많은 양의 요청이 몰려 대기열 위치 조회는 상대적으로 여유롭게 처리될 것입니다.

단일 서버 환경에서는 추가적인 외부 리소스 없이 높은 성능을 유지하기 위해 쿠폰 발급 대기열을 직접 구현했습니다.
테스트를 VUser 1명부터 시작하여 점진적으로 늘려가며 진행한 결과, 100ms 이내로 빠른 응답 시간을 보이다가 100명 이상부터 응답 시간이 300ms 이후로 급격히 느려지는 현상이 확인되었습니다.
Pinpoint를 활용해 분석한 결과, DB 커넥션을 얻는 과정에서 지연이 발생하는 것으로 나타났습니다. 이 문제를 해결하기 위해 커넥션 풀의 크기를 적절히 증가시키는 조치가 필요해 보였습니다.
커넥션 풀의 크기를 점진적으로 증가시키며 테스트를 반복했습니다. 동시에 커넥션 풀 대비 기본값인 200으로 많이 설정되어 있는 스레드 풀도 정적콘테츠만 필요한 경우를 고려해 커넥션 풀 보다 10 정도 많도록 함께 늘려가며 조정했습니다.
최종적으로 커넥션 풀 20, 스레드 풀은 30으로 설정했습니다.
응답 소요시간이 약 15% 정도 개선되었습니다.
- 쿠폰 발급 시마다 매번 DB에 발급 현황을 저장하는 방식은 정확성을 보장하지만, 처리 속도를 저하시킬 우려가 있었습니다.
- 서비스를 설계하면서 쿠폰 발급의 응답 속도가 중요한 과제였고, 쿠폰 발급 개수는 기존의 개수보다 약간의 오차로 더 발급되는 건 허용 가능한 범위라 판단했습니다.
- 매번 DB에 저장하기보다는 10회마다 한 번씩 동기화하도록 로직을 변경해 성능을 최적화했습니다. 이를 통해 쿠폰 발급의 처리 속도를 높이고, 동시에 시스템 부담을 줄일 수 있었습니다.
- Redis에 의존도가 높은 시스템 특성상, Redis가 갑작스럽게 종료될 경우 발급 개수가 초기화되고 전반적인 서비스 운영에 어려움이 발생할 위험이 있었습니다. 이를 해결하기 위해 Redis Sentinel을 활용한 Failover 구조를 구축하여 시스템의 안정성과 가용성을 강화했습니다.
서버 분리 후 성능 테스트 중 쿠폰 발급 동기화 실패와 함께 lock wait timeout 오류가 발생했습니다.
에러 로그를 기반으로 락 상태를 확인하기 위해 아래 쿼리를 실행해 DB 상태를 분석했습니다.
select * from information_schema.INNODB_LOCKS; -- 현재 Lock 정보
select * from information_schema.INNODB_LOCK_WAITS; -- Lock 대기 정보
select * from information_schema.INNODB_TRX; -- 트랜잭션 상태
-
트랜잭션 상태
- trx_id: 444807이
coupons
테이블 첫 번째 레코드에 공유 락을 획득한 상태로 쿼리는 NULL로 표시되어, 실행 중인 쿼리를 파악하기 어려웠습니다. - trx_id: 444808과 trx_id: 444809는 각각 trx_id: 444807이 소유한 락을 기다리는 lock wait 상태로 확인되었습니다.
- 즉, 444807 ← 444808 ← 444809 순으로 락을 획득하기 위해 대기 중이었습니다.
- trx_id: 444807이
INNODB_TRX
INNODB_LOCK_WAITS
LOCKS
-
락 발생 원인
- 쿠폰 발급 처리 로직에서 트랜잭션을 시작하고
findCoupon
메서드가 공유 락을 획득한 후, 트랜잭션이 커밋 되지 않아 락이 유지되고 있었습니다. - 락을 점유하고 있는 상태에서 발급 동기화 로직인
SyncCouponIssuedCount
에서@Transactional(propagation = Propagation.REQUIRES_NEW)
로 새로운 트랜잭션을 시작하면서, 기존 트랜잭션의 락을 기다리는 상태가 되어 Deadlock이 발생한 것이 원인이었습니다.
- 쿠폰 발급 처리 로직에서 트랜잭션을 시작하고
쿠폰 발급 처리 시 트랜잭션을 짧게 유지하기 위해 발급 로직과 동기화 로직을 분리하고, 트랜잭션 전파 수준을 Propagation.REQUIRES_NEW
에서 Propagation.REQUIRED
로 변경했습니다.
추가적으로, 파사드 패턴을 적용하여 코드의 복잡성을 줄였습니다.

- 쿠폰 대기열을 Redis sorted set을 사용했습니다. 선착순 이벤트의 특성을 고려하여 순차성을 고려할 필요가 있어 score 값을 timestamp 값으로 넣어 먼저 발급 신청을 한 순서대로 발급을 진행할 수 있었습니다.
- 또한 특정 값의 위치 조회에 최적화된
ZRANK
명령어를 제공하여 O(log n)이라는 시간 복잡도로 구현할 수 있어 짧은 처리 시간으로 안정적인 대기 순번 조회가 가능하도록 설계했습니다.

개인적인 비용 문제로 인해 서버를 2대 이상 늘릴 수 없었습니다. 자원이 한정적인 상황에서 트래픽이 몰릴 경우, while 문을 활용해 대기열 요청을 최대한 처리하는 방식은 CPU 사용량이 과도하게 증가하거나 redis에 부하 문제가 발생할 가능성이 있었습니다.
이를 해결하기 위해 다음과 같이 처리량 조절 방식을 적용했습니다.
- Redis
ZRANGE
명령어를 사용해 한 번에 10개씩 대기열에서 데이터를 가져와 발급 처리하도록 구현했습니다. - 처리량 조절 API를 개발해, 운영 상황에 따라 대기열의 처리량을 유연하게 조정 가능하도록 했습니다. 추가적으로, 처리량 설정 값을 일정 시간 동안 캐싱하여 성능을 최적화하고, 처리량 변경 시 해당 값을 업데이트하는 방식으로 효율성을 강화했습니다.