Recent Posts
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- 달인막창
- 코루틴 컨텍스트
- 자원부족
- taint
- vfr video
- 헥사고날아키텍처 #육각형아키텍처 #유스케이스
- k8s #kubernetes #쿠버네티스
- preemption #
- OptimisticLock
- k8s
- 코루틴 빌더
- kotlin
- JanusWebRTCServer
- 겨울 부산
- JanusWebRTC
- 티스토리챌린지
- pytest
- JanusGateway
- PessimisticLock
- python
- mp4fpsmod
- terminal
- 깡돼후
- 개성국밥
- Kubernetes
- JanusWebRTCGateway
- 오블완
- Spring Batch
- tolerated
- PersistenceContext
Archives
너와 나의 스토리
Spring JPA 트랜잭션 관리 - 낙관적 락, 비관적 락 본문
반응형
Spring JPA Transaction 관리
JPA에서는 보통 @Transactional 어노테이션으로 트랜잭션을 선언적으로 관리한다.
기본적으로 Srping은 AOP 방식으로 메서드를 감싸서, 진입 시 begin, 정상 종료 시 commit, 예외 발생 시 rollback을 수행한다.
예제: 디바이스 조회 -> 점유 상태 변경 -> 디바이스 저장
@Service
public class DeviceService {
private final DeviceRepository deviceRepository;
@Transactional
public void occupyDevice(Long deviceId, Long userId) {
Device device = deviceRepository.findById(deviceId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 디바이스"));
if (device.isOccupied()) {
throw new IllegalStateException("이미 점유 중");
}
device.occupy(userId); // 상태 변경
deviceRepository.save(device); // flush 시점에 update
}
}
JPA에서 제공하는 동시성 제어 전략
1) Optimistic Lock (낙관적 락)
- 충돌이 드물다고 가정하고 동작
- 엔티티에 버전(@Version)을 두고 업데이트 시점에 버전 비교로 충돌을 감지한다.
- 엔티티의 상태가 변경되어 update가 일어나면 version이 자동으로 증가함.
- 충돌 발생 시 예외를 던지고 재시도하거나 사용자에게 실패를 알린다.
@Entity
public class Device {
@Id
private Long id;
private Boolean occupied;
private Long occupiedBy;
@Version
private Long version; // JPA/Hibernate가 자동으로 관리
public void occupy(Long userId) {
if (Boolean.TRUE.equals(this.occupied)) {
throw new IllegalStateException("이미 점유중");
}
this.occupied = true;
this.occupiedBy = userId;
}
}
@Service
public class DeviceService {
private final DeviceRepository repo;
public void occupyWithRetry(Long deviceId, Long userId) {
final int maxAttempts = 3;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
tryOccupy(deviceId, userId); // 트랜잭션 단위 시도
return;
} catch (ObjectOptimisticLockingFailureException | OptimisticLockException e) {
if (attempt == maxAttempts) throw new ConcurrentModificationException("동시성 충돌 — 재시도 초과");
// 짧은 backoff
try { Thread.sleep(100L * attempt); } catch (InterruptedException ignored) {}
}
}
}
@Transactional
protected void tryOccupy(Long deviceId, Long userId) {
Device d = repo.findById(deviceId).orElseThrow();
if (Boolean.TRUE.equals(d.getOccupied())) throw new IllegalStateException("이미 점유중");
d.occupy(userId);
repo.saveAndFlush(d);
}
}
- 시나리오
- T1과 T2가 동시에 device(버전=1)를 읽음.
- T1이 점유 후 commit → DB 업데이트, 버전 = 2.
- T2가 점유하려고 저장 시도 → 업데이트문이 WHERE id=? AND version=1로 동작 → 영향된 row가 0이면 JPA가 OptimisticLockException 발생.
- T2는 예외를 받아 재시도 로직을 실행하거나 사용자에 실패를 반환.
- 장단점
- 장점: 락을 걸지 않으므로 읽기 성능 유리, 확장성 좋음
- 단점: 충돌이 잦으면 재시도로 효율 저하
2) Pessimistic Lock (비관적 락)
- 충돌을 미리 막는다.
- DB 수준에서 해당 row를 SELECT ... FOR UPDATE처럼 잠궈서 다른 트랜잭션의 접근을 미리 차단(블락킹)
- 충돌 가능성이 높은 경우 사용함.
public interface DeviceRepository extends JpaRepository<Device, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT d FROM Device d WHERE d.id = :id")
@QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = "3000")})
Device findByIdForUpdate(@Param("id") Long id);
}
@Transactional
public void occupyPessimistic(Long deviceId, Long userId) {
try {
Device d = repo.findByIdForUpdate(deviceId); // DB에서 FOR UPDATE (잠금)
if (Boolean.TRUE.equals(d.getOccupied())) throw new IllegalStateException("이미 점유중");
d.occupy(userId);
repo.save(d);
} catch (LockTimeoutException | PessimisticLockException e) {
// 잠금 획득 실패 -> 재시도 혹은 사용자에게 알림
throw new IllegalStateException("다른 요청이 처리 중입니다. 잠시 후 다시 시도하세요.");
}
}
- 시나리오
- T1이 SELECT ... FOR UPDATE로 row 잠금 획득.
- T2가 같은 row를 SELECT ... FOR UPDATE하려 하면 블록되거나(lock timeout 설정 시) 즉시 실패.
- T1이 commit/rollback을 하면 잠금 해제 → T2가 진행.
- 장단점
- 장점: 충돌을 미연에 차단하여 절대적으로 한 번에 한 명만 수행되도록 보장
- 단점: 락 경함시 성능 저하&데드락 위험, 트랜잭션 짧게 유지해야함.
반응형
'개발 > Spring Boot' 카테고리의 다른 글
| [SpringBoot] JPA 양뱡향 매핑은 불필요한 것인가? Fetch 전략과 N+1 문제 (0) | 2025.09.27 |
|---|---|
| Dapr란? 마이크로서비스 운영 최적화하기 - 개념, 기능, 사용 방법 (1) | 2025.03.10 |
| 설정 파일(*.properties, *.yml)에 있는 값들을 자바 클래스로 바인딩해서 사용하기 - @ConfigurationProperties (0) | 2023.07.29 |
| Spring Boot 버전 업그레이드에 따른 변경 사항 / Spring Cloud, Gradle 등 (0) | 2023.03.29 |
| [SpringBoot] JPA Converter 적용 / Converter null 처리 (1) | 2023.03.05 |
Comments