1000개 동시 요청 환경에서 락 방식별 Trade-off를 분석하고 서비스 특성에 따른 락 선택 전략을 수립한 프로젝트
- 1000개의 동시 요청 환경을 Apache JMeter로 재현
synchronized방식, 낙관적 락, 비관적 락, Redis 분산 락을 적용하여 동시 요청 상황에서 데이터 정합성 제어 여부를 검증- 성능 테스트를 통해 각 락 방식을 수치로 비교 후, 결과를 분석
| 구분 | 사용 기술 |
|---|---|
| Language | Java |
| Framework | Spring Boot |
| Database | MySQL Redis |
| Performance Test | Apache JMeter Prometheus Grafana |
모든 테스트는 동일한 Post에 대해 동시에 1000개의 좋아요 증가 요청을 보내는 시나리오로 수행
Apache JMeter
Prometheus + Grafana
평균 API 응답 시간이 733ms로 세 가지 락 방식 중 가장 빠른 성능을 보임
DB 커넥션 점유 시간은 23ms, CPU 사용량은 0.2core로 모두 중간 수준을 기록,
응답 속도와 자원 사용 측면에서 가장 균형 잡힌 결과를 보임
CPU 사용량은 0.18core로 세 가지 락 중 가장 낮아, 연산 부담은 상대적으로 적은 것을 알 수 있었음
반면, 비관적 락은 트랜잭션이 락을 획득할 때까지 대기하는 동안에도 DB 커넥션을 점유한 상태를 유지하기 때문에
DB 커넥션 점유 시간이 24ms로 가장 높았으며, 평균 API 응답 시간은 1449ms로 낙관적 락 대비 느린 성능을 보임
락 관리 책임을 Redis로 분리함으로써 DB 커넥션 점유 시간은 21ms로 세 가지 락 중 가장 짧음
그러나 락 획득 및 해제를 위한 Redis와의 네트워크 통신 오버헤드로 인해 CPU 사용량은 0.3core로 가장 높았고,
평균 API 응답 시간은 1554ms로 가장 느리게 측정
| 락 방식 | 적합한 서비스 유형 |
|---|---|
| 낙관적 락 | 충돌이 적고, 평균 API 응답 시간이 중요한 서비스 |
| 비관적 락 | 충돌 빈도가 높고, CPU 사용량을 최소화해야하는 서비스 단, DB 커넥션 점유 증가를 허용할 수 있는 경우 |
| Redis 분산 락 | DB 병목을 분산해야 하는 서비스 단, CPU 사용량 증가를 감수할 수 있는 경우 |
문제상황
동시성 이슈를 방지하기 위해 서비스 레이어의 addLike() 메서드에 synchronized를 적용했지만, 결과는 예상과 달랐다.
적용코드
@Transactional
public synchronized Long addLike(Long postId) {
Post post = postRepository.findById(postId).orElseThrow();
post.addLike();
return post.getLikeCount();
}결과
- 예상 결과 → 좋아요 개수 1000개
- 실제 결과 → 좋아요 개수는 170개
원인분석
synchronized는 트랜잭션 자체에 락을 거는 것이 아닌 메서드 실행 구간에 대해서만 락을 제공- 하나의 트랜잭션이 커밋되기 전에 다른 트랜잭션이 동일한 좋아요 개수를 기준으로 증가 연산이 진행
해결방법
- Post 엔티티에 버전 필드를 추가하여 업데이트 시점에 버전을 비교
@Entity
@Getter
@NoArgsConstructor
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long likeCount;
// 버전 필드 (낙관적 락을 사용할 때, 해당 필드를 활성화)
@Version
private Long version;
...
}- 충돌 시, 재시도 로직 실행
- Post 테이블의 version 필드의 값과 업데이트 쿼리의 version 값을 비교 후, 버전이 다른 경우
ObjectOptimisticLockingFailureException발생 - 해당 예외가 발생하면 Thread는 50ms를 기다린 후, 비즈니스 로직을 재시도
- Post 테이블의 version 필드의 값과 업데이트 쿼리의 version 값을 비교 후, 버전이 다른 경우
// 좋아요 증가 로직 (낙관적 락 방식)
public Long addLike(Long postId) throws InterruptedException {
Long likeCount = 0L;
while(true) {
try {
likeCount = optimisticLockPostService.addLike(postId);
break;
// 재시도 로직 (50ms 기다린 후, 재시도)
} catch (ObjectOptimisticLockingFailureException e) {
log.info("좋아요 카운트 동시성 문제 발생");
Thread.sleep(50);
}
}
return likeCount;
}결과
- 동시 1000개의 좋아요 요청에 대해 DB에 1000개의 좋아요가 반영되는 것을 확인



