2024.09.25 - [Spring] - [Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 2
[Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 2
2024.09.22 - [Spring] - [Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 1 [Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 1동시성 처리우리가 웹 서비스를 개발하다보면
hdbstn3055.tistory.com
이전 포스팅에서 동시성 문제를 PESSIMISTIC_WRITE
를 사용해서 해결하는 실습까지 진행했다.
그렇다면 Spring JPA에서 사용할 수 있는 Lock 기법에는 어떤 것이 있으며, 트랜잭션의 격리 레벨에 대해 알아보자.
Spring JPA Lock 기법
낙관적 잠금 @Version 값 사용
- Spring JPA에서
@Version
어노테이션은 낙관적 잠금(Optimistic Locking)을 구현하는 데 사용된다. - 낙관적 잠금은 동시성 제어를 위한 전략 중 하나로, 데이터를 변경하려 할 때 다른 트렌젝션에 의해 데이터가 변경되었는지를 확인하는 방식이다.
- 이를 위해
@Version
어노테이션을 사용하여 버전 컬럼을 관리한다.
- 이를 위해
- Entity에
@Version
어노테이션이 붙은 필드의 값은 해당 Entity가 업데이트될 때마다 자동으로 증가한다. - 이제 두 개의 트랜잭션이 동일한 엔티티를 동시에 수정하려고 시도한다고 가정해보자.
- 먼저 시작된 트랜잭션이 엔티티를 수정하고 커밋하면,
@Version
필드의 값이 증가한다. - 이후로 시작된 다른 트랜잭션은 이전에 읽었던
@Version
필드의 값이 현재 데이터베이스에 있는 값이 다르므로
OptimisticLockingFailureException
이 발생한다.
- 따라서, 이런 방식으로 동시에 같은 데이터에 접근하는 트랜잭션들 사이의 충돌을 방지할 수 있다.
- 하지만, 이렇게 예외가 발생할 가능성이 있기 때문에, 적절한 예외 처리 로직이 필요하다.
- 예를 들어, 재고수가 정상적으로 감소하지 못했을 때, 이를 다시 시도하거나 사용자에게 에러 메시지를 보여주는 등의 방법
- 먼저 시작된 트랜잭션이 엔티티를 수정하고 커밋하면,
Ticket
우선, 버전 관리를 위해 필드를 하나 추가해주자.
@Getter
@Setter
@Entity
public class Ticket {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int stock;
@Version // <- 추가
private long version;
public Ticket(int stock) {
this.stock = stock;
}
public Ticket() {
}
public void purchase(int amount){
if(stock == 0){
throw new RuntimeException("티켓이 매진되었습니다.");
}
if(stock < amount){
throw new RuntimeException("티켓 재고가 부족합니다.");
}
stock -= amount;
}
}
TicketRepository
public interface TicketRepository extends JpaRepository<Ticket, Long> {
@Query("select t from Ticket t where t.id = :ticketId")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Ticket> findByIdWithLock(Long ticketId);
@Query("select t from Ticket t where t.id = :ticketId")
@Lock(LockModeType.OPTIMISTIC)
Optional<Ticket> findByIdWithLockOps(Long ticketId);
}
OPTIMISTIC
을 사용하는 @Lock
도 추가해준다.
그 후, 앞선 포스팅에서 사용했던 K6
를 통해 요청을 보내면 아래와 같이 예외가 발생하는 것을 볼 수 있다.

낙관적 락에서의 예외 종류
javax.persistence.OptimisticLockException (JPA)
org.hibernate.staleObjectStateException (Hibernate)
org.springframework.orm.ObjectOptimisticLockingFailureException (Spring)
Spring 기반의 JPA에서 낙관적 락을 사용하게 되면 충돌시 Hibernate
에서 StaleStateException
을 발생시킨다.
그리고 Spring에서 이 예외를 OptimisticLockingFailureException
으로 감싸서 응답한다.
이처럼, 낙관적 락은 동시성 문제 발생시 오류를 일으키며 Race Condition
을 방지한다고 생각할 수 있다.
하지만, 적절한 예외처리를 하지 않는 이상 작업이 진행되진 않는다.
실제로 id = 2
의 경우 낙관적 잠금을 사용한 경우인데 예외가 발생한 경우의 API는 동작하지 않아,
Ticket이 전부 처리되지 않았다.

비관적 잠금 @(PESSIMISTIC_WRITE) 값 사용
비관적 잠금을 사용한 예시는 이전 포스팅에서 살펴보았던 것처럼,
DB에게도 원자성 책임을 묻는 방식으로, 비관적 잠금을 사용한다.
- JPA(Java Persistence API)에서 제공하는 어노테이션으로, 특정 엔티티에 관한 비관적 잠금(Pessimistic Locking)을 설정하는 데 사용한다.
- 이는 데이터베이스까지 전파된다.
MySQL
기준SELECT ... FOR UPDATE
라는 쿼리를 통해WriteLock
이 걸린다.Row Level
의Lock
에는Shared Lock(sLock-공유 락)
과Exclusive Lock(xLock - 베타 락)
이 존재한다.- 공유 락 - 읽기, 베타 락 - 쓰기 라고 생각해도 된다.
Exclusive Lock(xLock)
은Spring Data JPA
에서LockModeType.PESSIMISTIC_WRITE
에 해당한다.
- 비관적 잠금이란, 데이터를 읽은 후 변경할 가능성이 있는 경우, 데이터를 읽는 즉시 해당 데이터에 관한 잠금을 설정함으로써 다른 트랜잭션에서 해당 데이터를 변경하지 못하도록 하는 방법이다.
- 해당 엔티티를 읽는 즉시 쓰기 잠금이 설정되어, 해당 엔티티를 읽은 트랜잭션이 완료될 때까지 다른 트랜잭션에서는 해당 엔티티를 일거나 쓸 수 없게 됨
- 이미 다른 트랜잭션에 의해 락이 설정된 경우 요청된 트랜잭션은 누락이 아닌 대기 상태에 들어간다고 한다.
public interface TicketRepository extends JpaRepository<Ticket, Long> {
@Query("select t from Ticket t where t.id = :ticketId")
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Ticket> findByIdWithLock(Long ticketId);
}


Spring JPA Lock 옵션 정리
LockModeType
|
설명
|
NONE
|
▪ 잠금을 사용하지 않음
▪ 기본값
|
OPTIMISTIC
|
▪ 낙관적 잠금 사용
▪ 데이터 변경 시 다른 트랜잭션에 의한 변경 확인
▪ @Version 애노테이션을 사용하여 버전 컬럼 관리
|
OPTIMISTIC_FORCE_INCREMENT
|
▪ 낙관적 잠금 사용
▪ 잠금이 걸린 엔티티의 버전을 강제로 증가시킴
▪ 다른 트랜잭션에서 해당 엔티티를 읽을 때 충돌을 일으킴
|
PESSIMISTIC_READ
|
▪ 비관적 잠금 사용
▪ 엔티티를 읽은 트랜잭션이 완료될 때까지 다른 트랜잭션에서 해당 엔티티 변경 방지
▪ 다른 트랜잭션에서는 해당 엔티티를 읽을 수 있음
|
PESSIMISTIC_WRITE
|
▪ 비관적 잠금 사용
▪ 엔티티를 읽은 트랜잭션이 완료될 때까지 다른 트랜잭션에서 해당 엔티티를 읽거나 쓸 수 없게 함
|
PESSIMISTIC_FORCE_INCREMENT
|
▪ 비관적 잠금 사용
▪ 잠금이 걸린 엔티티의 버전을 강제로 증가시킴
▪ 다른 트랜잭션에서 해당 엔티티를 읽을 때 충돌을 일으킴
|
트랜잭션 격리 레벨 관리
@Transactional
의 isolation
레벨을 지정하여 트랜잭션 접근 및 활동을 제한하는 방법도 사용할 수 있다.
사용 방법
@Slf4j
@Service
@RequiredArgsConstructor
public class TicketV6Service {
private final TicketRepository ticketRepository;
@Transactional(isolation = Isolation.SERIALIZABLE) // 이 부분 추가
public Ticket purchaseIsolation(Long ticketId, int amount){
Ticket ticket = ticketRepository.findById(ticketId).orElseThrow(RuntimeException::new);
log.info("{} -> [Ticket{} 현재 보유량={} & 구매 요청량={}", Thread.currentThread().getName(), ticket.getId(), ticket.getStock(), amount);
ticket.purchase(amount);
return ticket;
}
}
격리 단계 (Isolation Level)
|
설명
|
READ_UNCOMMITTED
|
✨ 가장 낮은 격리 수준. 다른 트랜잭션에서 아직 커밋되지 않은 변경 내용을 읽을 수 있음. Dirty Read, Non-Repeatable Read, Phantom Read 모두 발생 가능.
|
💡 READ_UNCOMMITED는 기본 격리 수준이며, 추가 정보 없이도 트랜잭션 처리
|
|
READ_COMMITTED
|
✨ 다른 트랜잭션에서 커밋된 변경 내용만 읽을 수 있음. Dirty Read는 방지하지만, Non-Repeatable Read와 Phantom Read는 발생 가능.
|
💡 SELECT 쿼리에 FOR UPDATE 키워드를 추가하여, 읽은 데이터가 다른 트랜잭션에 의해 변경되지 않도록 잠금
|
|
REPEATABLE_READ
|
✨ 같은 트랜잭션 내에서 여러 번 데이터를 읽을 때 항상 같은 결과를 보장. Dirty Read와 Non-Repeatable Read는 방지하지만, Phantom Read는 발생 가능.
|
💡 SELECT 쿼리에 FOR UPDATE 키워드를 추가하여, 읽은 데이터가 다른 트랜잭션에 의해 변경되지 않도록 잠금
💡 트랜잭션 시작 시점의 데이터 상태를 기록하여, 트랜잭션 진행 중에 다른 트랜잭션이 데이터를 변경해도 영향을 받지 않도록 함 (ReadView)
|
|
SERIALIZABLE
|
✨ 가장 높은 격리 수준. 트랜잭션들을 순차적으로 실행하여 동시성 문제를 완전히 방지. 모든 동시성 문제를 방지하며, 가장 안전하지만 성능 저하가 발생할 수 있음.
|
💡 SELECT 쿼리에 FOR UPDATE 키워드를 추가하여, 읽은 데이터가 다른 트랜잭션에 의해 변경되지 않도록 잠금
💡 트랜잭션 시작 시점의 데이터 상태를 기록하여, 트랜잭션 진행 중에 다른 트랜잭션이 데이터를 변경해도 영향을 받지 않도록 함 (ReadView)
💡 모든 쿼리에 SERIALIZABLE 옵션을 추가하여, 모든 쿼리가 순차적으로 실행
|
마무리하며
우리는 동시성 문제를 처리하기 위해 많은 것을 살펴보았고 실습을 통해 여러 Lock 기법과 격리 레벨도 알아보았다.
포스팅을 하며 Redis
를 사용한 분산 락에 관해서도 알게 되었고 Jmeter
의 사용법도 알게 되었다.
기회가 된다면 다음에 관련 내용에 관해 포스팅하겠다.
'Spring > 동시성 & Lock' 카테고리의 다른 글
[캐시 & 동시성 & Lock] 선착순 티켓 시스템 고도화 - 1 (0) | 2024.11.19 |
---|---|
[Spring Data] Redis Lock을 적용해보자 - 2 (0) | 2024.10.17 |
[Spring Data] Redis Lock을 적용해보자 - 1 (0) | 2024.10.16 |
[Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 2 (0) | 2024.09.25 |
[Spring] synchronized 키워드를 활용한 동시성 문제 해결 및 한계 - 1 (0) | 2024.09.22 |