현재 개발 중인 티켓 예매 프로젝트의 핵심 비즈니스 로직은 '선착순 동시성 제어'다. 초기 구현 단계에서는 데이터의 정합성을 최우선으로 고려하여 비관적 락(Pessimistic Lock)을 적용해 동시성을 제어했다.
하지만 개발 도중 "과연 트래픽이 수천 명 단위로 늘어나도, 디스크 I/O 기반인 DB가 버틸 수 있을까?" 라는 의문이 들었다. 단순히 "그럴 것이다"라는 뇌피셜이 아닌, 정량적인 데이터로 한계를 확인하고, 캐시 저장소(Redis) 도입의 확실한 근거를 마련하기 위해 극한의 부하 테스트를 진행했다.
1. 테스트 배경 및 설계
DB 비관적 락은 확실한 데이터 일관성을 보장하지만, 다음과 같은 치명적인 성능 저하가 예상되었다.
- Row Lock 경합: 다수가 한 데이터를 노릴 때 발생하는 대기 시간(Blocking).
- Connection Pool 고갈: 잦은 대기열 조회(Polling)로 인한 DB 리소스 점유.
이를 검증하기 위해 k6를 사용하여 두 가지 시나리오를 설계했다.
2. 시나리오 1: 좌석 쟁탈전 (Seat Lock Contention)
첫 번째 시나리오는 "다수의 사용자가 소수의 좌석을 동시에 선점하려 할 때" 발생하는 DB Row Lock의 경합 상황을 시뮬레이션했다.
2-1. 테스트 설계 및 스크립트
- 목적: 비관적 락 환경에서의 성능 비용(Latency) 및 정합성 검증
- 환경: VUs(가상 유저) 100명
- 핵심 로직: contentionPool을 사용하여 100명이 앞쪽 10개 좌석만 노리도록 강제 경합 유도
check(lockRes, {
'Lock Success (200)': (r) => r.status === 200,
'Lock Conflict (409)': (r) => r.status === 409,
'DB Lock Timeout (500)': (r) => r.status === 500,
});

2-2. 테스트 결과 분석
| 지표 | 결과값 | 비고 |
| 성공 (200 OK) | 20% (33건) | 선착순 성공 |
| 실패 (409 Conflict) | 80% (132건) | 이미 선점된 좌석 |
| p95 Latency | 1.29s | 하위 95% 요청 처리 시간 |
테스트 결과, 데이터 정합성은 완벽하게 보장되었다. 100명이 동시에 요청을 보냈음에도 중복 예약은 발생하지 않았으며, 선점 실패 시 정확히 409 Conflict를 반환했다.
하지만 성능 측면에서는 명확한 한계가 드러났다. 고작 100명의 동시 접속임에도 불구하고, 락 대기 시간으로 인해 p95 응답 속도가 1.29초까지 지연되었다. 만약 유저가 1,000명 단위로 늘어난다면, 모든 트랜잭션이 직렬화(Serialization)되어 줄을 서게 될 것이고, 응답 시간은 기하급수적으로 늘어날 것임이 자명했다.
3. 시나리오 2: 대기열 지옥 (Queue Polling Hell)
두 번째 시나리오는 "대기 순번을 확인하려는 단순 조회 트래픽이 폭주할 때" DB Connection Pool이 어떻게 반응하는지 확인했다.
3-1. 테스트 설계 및 스크립트
- 목적: 잦은 폴링(Polling)이 DB 리소스(HikariCP)에 미치는 영향 확인
- 환경: VUs 3,000명, 1초마다 대기열 상태 조회
- 핵심 로직: 한 명의 유저가 루프를 돌며 조회를 반복하여 조회 트래픽 부하를 극대화

3-2. 테스트 결과 분석
| 지표 | 결과값 | 비고 |
| HTTP 에러율 | 0% | 표면적으로는 모두 성공 |
| 평균 응답 시간 | 39.29초 | 사용자 이탈 발생 구간 |
| p95 응답 시간 | 50.79초 | 사실상 서비스 장애 |
k6 결과만 보면 에러율이 0%라 성공한 것처럼 보일 수 있다. 하지만 응답 시간이 50초라는 것은 UX 관점에서 명백한 서비스 장애다. 원인을 파악하기 위해 서버 로그를 확인한 결과, 범인은 HikariCP의 Connection 고갈이었다.

- Pool 고갈: 총 50개의 커넥션이 모두 사용 중(active=50)이었다.
- 병목 발생: 148개의 스레드(waiting=148)가 커넥션을 얻지 못해 대기열에 갇혀 있었다.
- 장애 확산: Spring Boot는 커넥션 획득을 위해 최대 30초(기본값)를 대기한다. 이 때문에 단순 조회 요청들이 WAS의 스레드를 모두 점유해버려, 정작 중요한 다른 서비스까지 마비시키는 결과를 초래했다.
4. 결론 및 향후 계획
이번 부하 테스트를 통해 "대용량 트래픽 처리에 DB 락과 폴링은 적합하지 않다"는 것을 코드로 검증했다. 데이터 정합성은 지켰을지 몰라도, 가용성과 성능은 지키지 못했기 때문이다.
이에 따라 다음과 같이 아키텍처를 개선하기로 결정했다.
| 구분 | AS-IS (현재) | TO-BE (개선안) |
| 좌석 선점 | DB 비관적 락 (Lock Wait 발생) | Redis 분산 락 도입 |
| 대기열 조회 | DB 조회 (Connection 고갈) | Redis In-Memory 조회 (DB 부하 Zero) |
Redis 도입을 통해 p95 Latency를 100ms 이하로 줄이는 것이 목표다. 다음 포스팅에서는 Redis 적용 후 동일한 시나리오에서 성능이 얼마나 개선되었는지, Before & After 데이터를 통해 다룰 예정이다.
"에러가 나지 않았다고 문제가 없는 것이 아니다. 50초의 지연은 곧 장애다."
'Data Engineering > DBMS & Tuning' 카테고리의 다른 글
| Oracle ) Sequence (0) | 2023.04.17 |
|---|---|
| [MariaDB]LIMIT,OFFSET (0) | 2023.04.10 |