개요
실무에서 동시에 여러 요청이 같은 자원을 수정할 때, 데이터 정합성 문제가 발생합니다.
대표적으로 쇼핑몰의 상품 재고 차감, 은행의 계좌 이체, 예약 시스템의 좌석 예매 등이 있습니다.
단순히 synchronized 같은 자바 키워드만으로는 멀티 인스턴스 환경에서 제어가 불가능합니다. 따라서 DB 차원에서의 동시성 제어가 필요하며, JPA에서는 낙관적 락(Optimistic Lock) 과 비관적 락(Pessimistic Lock) 두 가지 전략을 제공합니다.
이번 글에서는 두 방법을 모두 실습하고, 장단점과 실무 적용 시 고려할 점을 정리해보겠습니다.
주요 기능 요약
✔️ 낙관적 락
- @Version 필드를 이용해 버전 충돌 감지
- 충돌 발생 시 OptimisticLockException 발생 → 재시도 로직 필요
✔️ 비관적 락
- @Lock(LockModeType.PESSIMISTIC_WRITE) → SQL의 SELECT ... FOR UPDATE 수행
- 트랜잭션이 끝날 때까지 해당 행을 독점, 충돌 시 대기
기술 스택
- Java 17
- Spring Boot 3.x (Maven 기반)
- JPA (Hibernate)
- MySQL 8.x (root / 1111)
- JUnit5
예제 흐름
1. 낙관적 락 (Optimistic Lock)
- Stock 엔티티에 @Version 필드를 추가합니다.
- 재고 차감 시 충돌이 발생하면 OptimisticLockException이 발생하고, 트랜잭션이 롤백됩니다.
- 따라서 재시도 로직을 구현해 최종적으로 데이터 정합성을 보장합니다.
@Entity
public class Stock {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int quantity;
@Version
private Long version;
public void decrease() {
if(quantity <= 0) throw new IllegalStateException("재고 부족");
this.quantity--;
}
}
서비스 레이어 (재시도 로직 포함)
public void decreaseWithRetry(Long id) {
for(int i = 0; i < 20; i++) {
try {
decreaseOnce(id);
return;
} catch (OptimisticLockException e) {
Thread.sleep(10);
}
}
throw new IllegalStateException("재시도 초과");
}
- spring-retry 라이브러리 대체 가능
2. 비관적 락 (Pessimistic Lock)
- Repository 계층에서 @Lock(PESSIMISTIC_WRITE) 를 사용합니다.
- SQL 레벨에서 SELECT ... FOR UPDATE 로 해당 행을 잠급니다.
- 충돌이 발생하지 않고, 요청이 직렬화되어 정확성이 보장됩니다.
public interface StockRepository extends JpaRepository<Stock, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Stock s where s.id = :id")
Optional<Stock> findByIdForUpdate(@Param("id") Long id);
}
서비스 레이어
@Transactional
public void decrease(Long id) {
Stock stock = stockRepository.findByIdForUpdate(id)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 상품"));
stock.decrease();
}
📊 낙관적 락 vs 비관적 락 비교
구분 | 낙관적 락 (Optimistic Lock) | 비관적 락 (Pessimistick Lock) |
개념 | 버전(@Version) 값 비교로 충돌 감지 | SELECT ... FOR UPDATE 로 DB 행 잠금 |
충돌 처리 | 예외 발생 후 재시도 필요 | 충돌 자체 없음 (다른 요청은 대기) |
장점 | 락이 없어 성능 유리 (충돌 적을 때) | 데이터 정합성 강력 보장 |
단점 | 충돌이 많으면 재시도 비용 ↑ | 트래픽 많으면 대기/Deadlock 위험 |
적합한 경우 | 조회수 증가, 좋아요 등 충돌 드문 경우 | 재고 차감, 은행 이체 등 충돌 많은 경우 |
회고 및 팁
실습을 통해 체감한 점은 다음과 같습니다:
- 낙관적 락: 충돌이 적은 환경에서는 효율적이지만, 충돌이 많으면 재시도로 인해 오히려 비효율적입니다.
- 비관적 락: 단순하고 정확성을 보장하지만, 트래픽이 몰리면 DB 레벨에서 락 경합이 병목으로 작용할 수 있습니다.
- 따라서 읽기 많은 서비스 → 낙관적 락, 정확성이 우선인 서비스(재고/결제) → 비관적 락 으로 선택하는 것이 합리적입니다.
'프로그래밍 > Java' 카테고리의 다른 글
💻 [Java] Lombok vs record 실무 비교 — DTO/VO에 무엇을 쓸까? (4) | 2025.08.11 |
---|---|
💻 [Java] Record 완전 정복 — 불변 데이터 클래스를 위한 언어 기능 (3) | 2025.08.11 |
💻 [Java] 자바의 실행 프로세스 완전 정복 - 컴파일부터 JVM 메모리까지 (3) | 2025.08.07 |
💻 [Java] 단일 vs 멀티 스레드, 직접 실험해본 성능 차이 (2) | 2025.07.01 |
💻 [Java & Spring] 비동기 처리와 스레드, 언제 쓰고 어떻게 써야 할까? (0) | 2025.07.01 |