반응형
한정된 수량의 쿠폰을 회원에게 발급하는 시스템에서는 동시 다발적인 요청으로 인해 동시성 문제가 발생할 수 있다. 선착순 쿠폰 발급 시나리오에서 낙관적 락과 비관적 락의 성능을 정량적으로 비교하고, 동시 접속자 수 변화에 따른 각 방식의 특성을 분석했다.
실험 환경
기술 스택
- Backend : Spring Boot, Spring Data JPA, PostgreSQL
- 부하 테스트 : K6
- 실행 환경 : 로컬 개발 환경 (단일 서버)
시스템 설정
별다른 튜닝은 적용하지 않았다.
- HikariCP max-pool-size: 20
- Tomcat max-threads: 100
락 구현 방식
- 비관적 락 (
SELECT FOR UPDATE) - 낙관적 락 + 재시도 3회 + Exponential Backoff
- 낙관적 락 + 재시도 10회 + Exponential Backoff
// 낙관적 락 충돌 발생 시 일정 시간 대기 후 재시도
long sleepTime = (long) (50 * Math.pow(1.5, retryCount));
Thread.sleep(Math.min(sleepTime, 500)); // 최대 500ms
테스트 시나리오
기본 설정
- 쿠폰 총 수량 : 1,000개
- 신청자 수 : 1,000명 (경쟁률 1:1)
- 동시 접속자 수(VUS) : 10, 50
- 낮은 충돌(10), 빈번한 충돌(50)로 가정했다.
- 동시 접속자 수 변화에 따른 각 락 방식의 성능 및 정합성 비교
측정 지표
- 정합성 : 최종 발급된 쿠폰 수가 총 수량(1,000개)과 일치하는지 확인
- 성능
- 평균 응답 시간 : 모든 요청의 평균 처리 시간
- P95 응답 시간 : 상위 5%를 제외한 95%의 요청이 완료된 시간 (일반적 사용자 경험)
- P99 응답 시간 : 상위 1%를 제외한 99%의 요청이 완료된 시간 (최악 케이스 제외)
- 최대 응답 시간 : 가장 오래 걸린 요청의 응답 시간 (최악의 사용자 경험)
- TPS : 초당 처리 가능한 트랜잭션 수 (시스템 처리량)
- 낙관적 락을 위한 추가 지표
- 평균 재시도 횟수 : 성공한 요청의 평균 재시도 횟수
- 총 재시도 횟수 : 모든 성공 요청의 재시도 합계
- 재시도 초과 : 최대 재시도 후에도 실패한 요청 수
실험 결과
VUS 10 (저동시성 상황)
| 지표 | 비관적 락 | 낙관적 락(재시도 3회) | 낙관적 락(재시도 10회) |
| 발급 성공 | 1,000개 (100%) | 898개 (89.8%) | 997개 (99.7%) |
| 평균 응답시간 | 55ms | 90ms | 150ms |
| P95 | 74ms | 399ms | 667ms |
| P99 | 83ms | 411ms | 1,635ms |
| 최대 응답시간 | 109ms | 578ms | 3,610ms |
| TPS | 60.4/s | 37.7/s | 36.5/s |
| 총 재시도 횟수 | - | 652회 | 905회 |
| 재시도 초과 | - | 102건 | 3건 |
| 평균 재시도 | - | 0.73회 | 0.91회 |
- 비관적 락만 모든 수량의 쿠폰을 발급 성공했다.
- 응답 시간은 비관적 락이 가장 안정적이었다.
- 평균 응답시간 : 비관적 락 55ms vs 낙관적 락(3회) 90ms (1.6배)
- P99 : 비관적 락 83ms vs 낙관적 락(10회) 1,635ms (19.7배)
- 비관적 락이 가장 높은 처리량을 보였다 (60.4 TPS)
- 낙관적 락 충돌 : 동시 접속 10명에서도 652~905회의 재시도 발생했다.
동시 접속 10명 수준의 저동시성 환경에서도 낙관적 락은 빈번한 충돌이 발생하여 정합성을 보장하지 못했다. 재시도를 10회까지 늘려도 0.3%의 미발급이 발생했으며, P99 응답 시간이 비관적 락 대비 19.7배 느려졌다. 낙관적 락 사용 시 충돌 발생으로 인한 재시도가 응답 지연의 원인으로 보인다.
VUS 50 (고동시성 상황)
| 지표 | 비관적 락 | 낙관적 락(재시도 3회) | 낙관적 락(재시도 10회) |
| 발급 성공 | 1,000개 (100%) | 564개 (56.4%) | 946개 (94.6%) |
| 평균 응답시간 | 463ms | 230ms | 436ms |
| P95 | 744ms | 554ms | 1,777ms |
| P99 | 1,010ms | 613ms | 3,441ms |
| 최대 응답시간 | 1,033ms | 649ms | 4,448ms |
| TPS | 55.0/s | 60.3/s | 45.0/s |
| 총 재시도 횟수 | - | 350회 | 708회 |
| 재시도 초과 | - | 436건 | 54건 |
| 평균 재시도 | - | 0.62회 | 0.75회 |
- VUS50가 정말 큰 동시접속자 수는 아니지만 VUS10에 비해 크기 떄문에 고동시성으로 명명헀다.
- 여전히 비관적 락만 모든 수량의 쿠폰을 발급했다.
- 낙관적 락 방식은 발급 성공 수량이 조금 하락했다. 충돌이 빈번해져 발급 성공 케이스가 감소했다.
- 낙관적 락은 불안정한 응답 시간 지연이 발생한다.
- P99 : 비관적 락 1,010ms vs 낙관적 락(10회) 3,441ms (약 3.4배)
- 낙관적 락 방식의 경우 최대 응답 지연시간이 4,448ms로 일부 사용자는 4초 이상 대기하게 된다.
동시 접속자가 10명에서 50명으로 증가하자 낙관적 락의 성능이 급격히 저하되었다. 재시도 3회로는 절반 가까이 실패했으며, 재시도 10회로도 5.4%가 실패하고 응답 시간이 극도로 불안정해졌다. 반면 비관적 락은 여전히 100% 정합성을 유지하며 안정적인 성능을 보였다.
결론
- 비관적 락은 완벽한 정합성을 보장했다. 락 대기로 인한 응답 시간 증가가 있지만 정합성이 중요한 곳에 적합하다.
- 낙관적 락은 이론상 락 오버헤드가 없어 빠를 것으로 예상하지만, 충돌이 빈번한 상황에서는 오히려 불안정한 응답 지연을 보였다.
- 이벤트성 쿠폰 발급의 상황에서는 비관적 락을 사용하는게 실용적 선택이 될 같다.
- 낙관적 락은 저동시성 상황에서도 실패가 발생했으며, 재시도로 인한 보완 시도 시 응답시간이 크게 증가했다.
- 비관적 락은 정합성을 지키며 안정적인 성능을 제공했다.
이론으로만 학습했던 낙관적 락과 비관적 락을 직접 구현하고 실험해보니 각 방식의 특성을 공부할 수 있었다. 특히 낙관적 락은 충돌이 적은 환경에서 유리하다는 이론을 직접 확인해 봐서 의미있었다.
실험 과정에서 트랜잭션 범위 설정 실수를 발견하고 수정하거나, K6 메트릭을 분석하는 등 동시성 제어 외에도 많은 것을 배울 수 있었다. 이번 실험을 토대로 다음에는 레디스를 활용한 분산 락 방식도 비교해보고 싶다. 추가로 커넥션 풀 사용량, GC 빈도, CPU 사용률 등 시스템 리소스 지표를 함께 측정해보면 좋을 것 같다.
반응형