낙관적 락(Optimistic Lock) 과 비관적 락(Pessimistic Lock)
: 낙관적 락 과 비관적 락은 데이터 동시성 문제를 해결 하기 위한 2 가지 주요 기법입니다.
둘 다 동시에 여러 트랜잭션이 동일한 데이터에 접근 할 때 데이터의 일관성을 보장하기 위해 사용됩니다. 하지만, 사용하는 방식과 적용 시점에서 큰 차이가 있습니다.
1. 낙관적 락(Optimisitc Lock)
1. 1. 개념
- 데이터 충돌이 드물 것이라고 가정
- 트랜잭션이 데이터베이스에 변경사항을 적용하기 전에만 충돌을 확인
- 버전 관리(Versioning)를 통해 동시성 문제를 감지
1.2. 동작 방식
- 데이터를 읽어올 때 트랜잭션이 Lock이 걸지 않습니다.
- 데이터를 수정한 후 업데이트 시점에 버전 번호를 확인하여 충돌 여부를 판단합니다.
- 데이터베이스의 현재 버전과 트랜잭션 시작 시점의 버전이 다르면 충돌 발생.
- 충돌 시 OptimisticLockException 이 발생
- 개발에서는 ObjectOptimisticLockingFailureException 에서 걸림
- 낙관적 락이 적용된 엔티티를 업데이트 할 때 버전 충돌이 발생하는 경우 던져지는 예외입니다. 즉 버전 관리 실패를 나타냅니다.
- 개발에서는 ObjectOptimisticLockingFailureException 에서 걸림
} catch (ObjectOptimisticLockingFailureException e) {
1.3. 장점
- 락을 사용하지 않음
- 데이터 읽기 시 락이 걸리지 않으므로 대기 시간이 발생하지 않아 성능이 좋음
- 읽기 작업이 많고 쓰기 작업이 적은 환경에서 효율적
- 충돌 감지
- 데이터가 충돌할 경우 트랜잭션을 재시도하도록 설계 가능
1.4. 단점
- 충돌 발생 시 처리 필요
- 충돌이 발생하면 애플리케이션에서 이를 처리하는 추가 로직이 필요
- 충돌이 잦은 경우 비효율적
- 경합이 잦은 환경에서는 반복적인 재시도로 성능이 저하될 수 있습니다.
1.5 코드
OptProduct.java
package org.springboot.lock.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Version;
@Setter
@Getter
@Entity
@ToString
public class OptProduct {
// Getter 및 Setter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int stock;
@Version
private Long version; // 낙관적 락을 위해 버전 필드 추가
// 기본 생성자
public OptProduct() {}
// 생성자
public OptProduct(String name, int stock) {
this.name = name;
this.stock = stock;
}
}
OptProductRepository.java
package org.springboot.lock.repository;
import org.springboot.lock.domain.OptProduct;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface OptProductRepository extends JpaRepository<OptProduct, Long> {
}
OptProductService.java
package org.springboot.lock.service;
import org.springboot.lock.domain.OptProduct;
import org.springboot.lock.repository.OptProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.OptimisticLockException;
@Service
public class OptProductService {
private final OptProductRepository optProductRepository;
public OptProductService(OptProductRepository optProductRepository) {
this.optProductRepository = optProductRepository;
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // 항상 새로운 트랜잭션을 시작하며, 기존 트랜잭션과는 독립적으로 실행됩니다.
public void purchaseOptProduct(Long optProductId) {
try {
// 상품 조회
OptProduct optProduct = optProductRepository.findById(optProductId).orElseThrow(()->new IllegalArgumentException("상품을 찾을 수 없습니다."));
System.out.println("상품 조회 성공 : "+optProduct);
// 재고가 있는지 확인
if (optProduct.getStock() > 0) {
// 재고 감소
optProduct.setStock(optProduct.getStock() - 1);
// 엔티티 저장
OptProduct savedProduct = optProductRepository.save(optProduct);
System.out.println("재고 감소 성공 : "+savedProduct.getStock());
} else {
throw new IllegalStateException("재고가 부족합니다.");
}
} catch (OptimisticLockException e) {
throw new RuntimeException("낙관적 락 충돌 발생! 다시 시도해주세요.",e);
}
}
}
2. 비관적 락(Pessimistic Lock)
2.1. 개념
- 데이터 충돌이 자주 발생할 것이라고 가정
- 트랜잭션이 데이터를 읽을 때부터 락을 걸어 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 합니다.
2.2. 동작 방식
- 데이터를 읽는 시점에 락을 걸어 다른 트랜잭션이 동일 데이터를 읽거나 수정하지 못하도록 합니다.
- 락은 트랜잭션이 종료될 때까지 유지
- 다른 트랜잭션은 락이 해제될 때가지 대기하거나 예외가 발생
2.3. 장점
- 충돌 방지
- 데이터 충돌을 사전에 방지하므로 트랜잭션 재시도가 필요 없음
- 데이터 무결성 보장
- 충돌 상황이 자주 발생하는 경우, 데이터의 일관성을 보장
2.4. 단점
- 성능 저하
- 락을 걸기 때문에 다른 트랜잭션은 대기 상태가 되며, 동시성 처리 성능이 떨어짐
- 데드락 위험
- 잘못된 설계로 인해 데드락 이 발생
- 읽기 작업에도 락이 필요
- 읽기 작업이 많은 환경에서 비효율
2.5 코드
PesProduct.java
package org.springboot.lock.domain;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Setter
@Getter
@Entity
@ToString
public class PesProduct {
// Getter 및 Setter
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int stock;
// 기본 생성자
public PesProduct() {}
// 생성자
public PesProduct(String name, int stock) {
this.name = name;
this.stock = stock;
}
}
PesProductRepository.java
package org.springboot.lock.repository;
import org.springboot.lock.domain.PesProduct;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.stereotype.Repository;
import javax.persistence.LockModeType;
import java.util.Optional;
@Repository
public interface PesProductRepository extends JpaRepository<PesProduct, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE) // 비관적 락 적용 특정 엔티티에 대해 비관적 쓰기 락을 걸어 동시성 문제를 방지합니다.
Optional<PesProduct> findById(Long id);
}
PesProductService.java
package org.springboot.lock.service;
import org.springboot.lock.domain.PesProduct;
import org.springboot.lock.repository.PesProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PesProductService {
private final PesProductRepository pesProductRepository;
public PesProductService(PesProductRepository pesProductRepository) {
this.pesProductRepository = pesProductRepository;
}
@Transactional
public void purchaseProduct(Long productId) {
// 비관적 락 적용하여 상품 조회
PesProduct product = pesProductRepository.findById(productId).orElseThrow(()->new IllegalArgumentException("상품을 찾을 수 없습니다."));
// 재고가 있는지 확인
if (product.getStock() > 0) {
// 재고 감소
product.setStock(product.getStock() - 1);
// 엔티티 저장
PesProduct savedProduct = pesProductRepository.save(product);
System.out.println("재고 감소 성공 : "+savedProduct.getStock());
} else {
throw new IllegalStateException("재고가 부족합니다.");
}
}
}
3. 테스트
package org.springboot.lock.product;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springboot.lock.domain.OptProduct;
import org.springboot.lock.domain.PesProduct;
import org.springboot.lock.repository.OptProductRepository;
import org.springboot.lock.repository.PesProductRepository;
import org.springboot.lock.service.OptProductService;
import org.springboot.lock.service.PesProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
public class ProductServiceTest {
@Autowired
private OptProductRepository optProductRepository;
@Autowired
private PesProductRepository pesProductRepository;
@Autowired
private OptProductService optProductService;
@Autowired
private PesProductService pesProductService;
private Long optProductId;
private Long pesProductId;
@BeforeEach
public void setUp() {
System.out.println("setUp Start : ");
/** 낙관적 락 */
OptProduct optProduct = new OptProduct("Test OptProduct", 10); // 재고 10개 생성
optProductRepository.save(optProduct);
optProductId = optProduct.getId();
/** 비관적 락 */
PesProduct pesProduct = new PesProduct("Test PesProduct", 10); // 재고 10개 생성
pesProductRepository.save(pesProduct);
pesProductId = pesProduct.getId();
System.out.println("setUp End : ");
}
@DisplayName("낙관적 Lock 발생")
@Test
public void testOptimistic() throws InterruptedException {
int numberOfThreads = 2; // 동시에 실행할 스레드 개수
ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
executorService.submit(() -> {
try {
// System.out.println("Thread " + Thread.currentThread().getName() + " 시작");
optProductService.purchaseOptProduct(optProductId);
// System.out.println("Thread " + Thread.currentThread().getName() + " 성공");
} catch (ObjectOptimisticLockingFailureException e) {
System.out.println("Thread " + Thread.currentThread().getName() + " 충돌 발생: " + e.getMessage());
} catch (Exception e) {
System.out.println("Thread " + Thread.currentThread().getName() + " 실패: " + e);
} finally {
latch.countDown();
}
});
}
latch.await(); // 모든 스레드가 끝날 때까지 대기
executorService.shutdown();
// 결과 검증
OptProduct product = optProductRepository.findById(optProductId).orElseThrow();
System.out.println("result : "+product);
//assertThat(product.getStock()).isEqualTo(0); // 최종 재고가 0이 되어야 함
assertThat(product.getStock()).isLessThanOrEqualTo(9); // 재고는 9 이하로 감소해야 함
}
@DisplayName("비관적 Lock 발생")
@Test
public void testPesimistic() throws InterruptedException {
int numberOfThreads = 10; // 동시에 실행할 스레드 개수
ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads);
CountDownLatch latch = new CountDownLatch(numberOfThreads);
// test //
for (int i = 0; i < numberOfThreads; i++) {
executorService.submit(() -> {
try {
pesProductService.purchaseProduct(pesProductId);
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await(); // 모든 스레드가 끝날 때까지 대기
executorService.shutdown();
// 결과 검증
PesProduct product = pesProductRepository.findById(pesProductId).orElseThrow();
System.out.println("result : "+product);
assertThat(product.getStock()).isEqualTo(0); // 최종 재고가 0이 되어야 함
}
}
낙관적 Lock Test
setUp Start :
Hibernate: insert into opt_product (id, name, stock, version) values (default, ?, ?, ?)
Hibernate: insert into pes_product (id, name, stock) values (default, ?, ?)
setUp End :
Hibernate: select optproduct0_.id as id1_0_0_, optproduct0_.name as name2_0_0_, optproduct0_.stock as stock3_0_0_, optproduct0_.version as version4_0_0_ from opt_product optproduct0_ where optproduct0_.id=?
Hibernate: select optproduct0_.id as id1_0_0_, optproduct0_.name as name2_0_0_, optproduct0_.stock as stock3_0_0_, optproduct0_.version as version4_0_0_ from opt_product optproduct0_ where optproduct0_.id=?
상품 조회 성공 : OptProduct(id=1, name=Test OptProduct, stock=10, version=0)
상품 조회 성공 : OptProduct(id=1, name=Test OptProduct, stock=10, version=0)
재고 감소 성공 : 9
재고 감소 성공 : 9
Hibernate: update opt_product set name=?, stock=?, version=? where id=? and version=?
Hibernate: update opt_product set name=?, stock=?, version=? where id=? and version=?
2024-11-13 18:55:59.069 INFO 10871 --- [pool-1-thread-2] o.h.e.j.b.internal.AbstractBatchImpl : HHH000010: On release of batch it still contained JDBC statements
Thread pool-1-thread-2 충돌 발생: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update opt_product set name=?, stock=?, version=? where id=? and version=?; nested exception is org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1; statement executed: update opt_product set name=?, stock=?, version=? where id=? and version=?
Hibernate: select optproduct0_.id as id1_0_0_, optproduct0_.name as name2_0_0_, optproduct0_.stock as stock3_0_0_, optproduct0_.version as version4_0_0_ from opt_product optproduct0_ where optproduct0_.id=?
result : OptProduct(id=1, name=Test OptProduct, stock=9, version=1)
비관적 Lock Test
setUp Start :
Hibernate: insert into opt_product (id, name, stock, version) values (default, ?, ?, ?)
Hibernate: insert into pes_product (id, name, stock) values (default, ?, ?)
setUp End :
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
재고 감소 성공 : 9
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 8
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 7
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 6
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 5
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 4
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 3
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 2
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 1
Hibernate: update pes_product set name=?, stock=? where id=?
재고 감소 성공 : 0
Hibernate: update pes_product set name=?, stock=? where id=?
Hibernate: select pesproduct0_.id as id1_1_0_, pesproduct0_.name as name2_1_0_, pesproduct0_.stock as stock3_1_0_ from pes_product pesproduct0_ where pesproduct0_.id=? for update
result : PesProduct(id=1, name=Test PesProduct, stock=0)
'Spring > 1-3. JPA' 카테고리의 다른 글
(9. Spring Data JPA) @Query 와 Native Query (0) | 2023.05.23 |
---|---|
(8. Spring Data JPA) 영속성 전이 Cascade (0) | 2023.05.11 |
(7. Spring Data JPA) Transaction Manager (0) | 2023.05.10 |
(6. Spring Data JPA) 영속성 컨텍스트 Persistence Context (0) | 2023.05.06 |
(5. Spring Data JPA) Entity Listener 활용 (1) | 2022.10.18 |