진행 환경
💡진행 환경
- Java 17
- Spring Boot 3.5.7
- MySQL 8.0.44
- Gradle
문제 상황
💡 문제 상황

💡 문제 증상
- Redisson 분산락을 적용했지만, 100개의 동시 요청을 보냈을 때 재고가 기대값처럼 정확히 차감되지 않았다.
- 테스트 기대값은 다음과 같다.
초기 재고 : 100
요청 수 : 100개
요청당 차감 수량 : 1
기대 결과 : 최종 재고 0
실제 결과 : 최종 재고가 0이 아니거나, 일부 요청이 실패하는 동시성 문제가 발생
원인 분석
💡 원인 1 : 락 해제가 트랜잭션 커밋보다 먼저 일어난다.
❗️StockServiceImpl.java
@Transactional
public StockResult decreaseStock(StockRequests.Decrease request) {
return stockLockManager.executeWithLock(
"stock:decrease:" + request.productId(),
() -> {
Product product = getProductOrThrow(request.productId());
Stock stock = getStockOrThrow(request.productId());
stock.decreaseQuantity(request.quantity());
return StockResult.from(stock, product.getName());
}
);
}
- 현재 구조는 StockServiceImpl.decreaseStock()에 @Transactional이 붙어 있고, 그 메서드 내부에서 Redisson 락을 획득한다.
❗️Redisson 분산락 적용한 재고 감소 서비스 로직 흐름
트랜잭션 시작
→ Redisson 락 획득
→ 재고 조회
→ 재고 차감
→ 락 해제
→ 트랜잭션 커밋
- Spring의 @Transactional은 프록시가 메서드 호출 전후로 트랜잭션을 관리한다. 따라서 메서드 내부의 finally에서 unlock()이 실행된 뒤, 메서드가 완전히 반환되고 나서야 트랜잭션 커밋이 일어난다.
- 락이 풀렸지만 이전 요청의 재고 감소가 아직 DB에 commit되지 않은 상태라면, 다음 요청이 lock을 획득하고 확정되지 않은 데이터를 기준으로 재고를 조회할 수 있다.
- 분산 락의 목적은 같은 상품 재고 감소를 순차 처리하는 것인데, commit 이전에 lock이 풀리면 정합성 보장이 약해진다.
❗️잘못된 흐름 예시
Thread A: 재고 100 조회
Thread A: 재고 99로 변경
Thread A: 락 해제
Thread B: 락 획득
Thread B: 아직 A 커밋 전이라 DB에서 재고 100 조회
Thread A: 커밋
Thread B: 재고 99로 변경
Thread B: 커밋
해결 방안 검토
💡 해결 방안 1 :
해결 방법
💡 해결 방법 : lock을 잡는 책임과 실제 DB 변경 트랜잭션을 분리
- StockServiceImpl은 lock 획득/해제 흐름을 담당하고, StockDecreaseProcessor는 @Transactional로 실제 재고 감소를 담당하게 했다.
- 1
public StockResult decreaseStock(Decrease request) {
return stockLockManager.executeWithLock(
"stock:decrease:" + request.productId(),
() -> stockDecreaseProcessor.decrease(request)
);
}
- 2
@Transactional
public StockResult decrease(Decrease request) {
Stock stock = stockRepository.findByProductId(request.productId())
.orElseThrow(...);
stock.decreaseQuantity(request.quantity());
return StockResult.from(stock, productName);
}
트랜잭션 분리 ?
💡트랜잭션을 분리하는 이유
Redisson 분산 락을 적용할 때 단순히 `@Transactional` 메서드 내부에서 lock/unlock을 처리하면, `finally`에서 lock이 해제된 뒤 Spring 트랜잭션 commit이 수행될 수 있다.
이 경우 다음 요청이 lock을 획득했지만 이전 요청의 DB 변경은 아직 commit되지 않은 상태가 될 수 있어, 재고 정합성 보장이 약해진다.
이를 방지하기 위해 lock을 잡는 바깥 서비스와 실제 DB 변경을 수행하는 `@Transactional` Processor를 분리했다.
결과적으로 실행 순서를 다음과 같이 보장했다.
`lock 획득 → 트랜잭션 시작 → 재고 감소 → commit → lock 해제`
✅ 분리하지 않은 구조

➡️ 문제 흐름
- 락은 풀렸는데 DB 변경은 아직 커밋되지 않은 상태가 생길 수 있다.
- 그러면 다음 요청이 확정되지 않은 데이터를 기준으로 동작할 위험이 있다.
✅ 분리한 구조

➡️ 요청이 여러 개일 때
- 다음 요청은 이전 요청의 DB commit이 끝난 뒤 lock을 획득한다.
- 따라서 확정된 재고 값을 기준으로 감소 로직을 수행할 수 있다.
- 프록시가 뭔지
- 프록시 에서 커밋하는 로직
- 트랜잭션이 뭔지
- 트랜잭션 분리하는 이유
참고 자료
'Projects > hub-eleven' 카테고리의 다른 글
| [트러블슈팅] Redisson 분산 락 적용 중 트랜잭션 커밋 순서 문제를 해결한 과정 (2) (0) | 2026.06.23 |
|---|---|
| [동시성 처리] 재고 감소 락 추상화 및 Redisson 락 구현 (아키텍처 구조 보완 필요) (0) | 2026.06.02 |
| [동시성 처리] RedissonClient Bean 설정 클래스 추가 (2) (리소스 보완 필요) (0) | 2026.05.28 |
| [동시성 처리] RedissonClient Bean 설정 클래스 추가 (1) (0) | 2026.05.27 |
| [동시성 처리] 동시성 문제 (Race Conditon) 해결하기 (0) | 2026.05.11 |