본문 바로가기

자바 심화/TIL

DB Lock

개요

DB에 저장되는 데이터의 일관성을 유지하고 충돌을 방지할 수 있는 방법인 DB Lock에 대해 정리할 것이다.

 

DB Lock?

DB Lock은다수의 사용자나 프로세스가 데이터베이스의 동일한 리소스(테이블, 행 등)에 동시에 접근하거나 수정하려고 할 때 발생할 수 있는 데이터 무결성 문제를 방지하기 위해 사용되는 메커니즘.

 

DB Lock 주요 개념

  1. Lock의 필요성:
    • 다중 사용자 환경에서 데이터베이스가 동일한 데이터를 동시에 읽거나 쓸 경우, 데이터 정합성 문제나 경합 조건(Race Condition)이 발생할 수 있다.
    • Lock은 이런 문제를 방지하여 데이터 무결성을 보장한다.
  2. Lock의 수준:
    • 테이블 수준 락(Table-Level Lock): 테이블 전체를 잠그는 방식으로, 간단하지만 동시성이 낮아진다.
    • 행 수준 락(Row-Level Lock): 특정 행만 잠그는 방식으로 동시성이 높아진다.
    • 페이지 수준 락(Page-Level Lock): 데이터가 저장된 페이지(블록 단위)를 잠그는 방식으로, 테이블과 행 수준 락의 중간 수준이다.
  3. Lock의 유형:
    • 공유 락(Shared Lock, S-Lock): 데이터를 읽을 때 설정되며, 여러 트랜잭션이 동시에 공유 락을 설정할 수 있다. 하지만 데이터 수정은 불가능하다.
    • 베타 락(Exclusive Lock, X-Lock): 데이터를 수정할 때 설정되며, 한 트랜잭션만 베타 락을 설정할 수 있다. 다른 트랜잭션은 접근할 수 없다.
    • 의도 락(Intent Lock): 계층적 리소스(예: 테이블과 행)에서의 잠금 계획을 명시하여 상충을 피하도록 돕는다.

DB Lock 사용 시 발생할 수 있는 문제

  1. 데드락(Deadlock):
    • 두 개 이상의 트랜잭션이 서로가 필요한 리소스를 잠그고 있어 무한 대기 상태에 빠지는 상황.
    • 해결 방법:
      • 타임아웃 설정.
      • 교착 상태 탐지 알고리즘 사용.
      • 트랜잭션 순서를 정해 데드락 방지.
  2. 락 경합(Lock Contention):
    • 다수의 트랜잭션이 동일한 리소스를 동시에 잠그려 할 때 성능 저하가 발생.
  3. 과도한 락 사용:
    • 불필요하게 넓은 범위(예: 테이블 전체)로 락을 설정하면 동시성이 낮아지고 성능 저하가 발생.

DB Lock을 최적화하는 방법

  1. 트랜잭션의 크기를 줄이기:
    • 락이 오래 유지되지 않도록 트랜잭션 내 작업을 최소화.
  2. 적절한 인덱스 설정:
    • 행 단위 락에서 불필요한 테이블 스캔 방지.
  3. 락 타임아웃 설정:
    • 일정 시간 이상 대기하지 않도록 타임아웃 설정.
  4. 낙관적 동시성 제어(Optimistic Concurrency Control):
    • 락 대신 버전 번호를 사용해 충돌 감지 후 재시도

 

DB Lock 실습

  • 비관적 락, 낙관적 락, 데드 락
 

낙관적 락 & 비관적 락

트랜잭션 격리 수준 트랜잭션은 ACID(원자성, 일관성, 격리성, 지속성)을 보장해야 한다. 트랜잭션은 원자성, 일관성, 지속성을 보장하지만 문제는 격리성으로 트랜잭션간 완전한 격리를 보장하

eleunadeu.tistory.com

 

설정

application.yml

spring:
  application:
    name: locking
  jpa:
    properties:
      hibernate:
        format_sql: true
        use_sql_comments: true

logging:
  level:
    org:
      hibernate:
        SQL: debug
        orm:
          jdbc:
            bind: TRACE

 

비관적 락(Pessimistic Lock)

Item

@Data
@Entity
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Integer quantity;
}

 

ItemRepository

public interface ItemRepository extends JpaRepository<Item, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select i from Item i where i.id = :id")
    Item findByIdWithLock(Long id);
}

 

ItemService

@Service
@RequiredArgsConstructor
public class ItemService {

    private final ItemRepository itemRepository;

    @Transactional
    public void updateItemQuantity(Long itemId, Integer newQuantity) {
        Item item = itemRepository.findByIdWithLock(itemId);

        item.setQuantity(newQuantity);

        itemRepository.save(item);
    }

    @Transactional
    public Item findItemById(Long itemId) {
        return itemRepository.findById(itemId).orElse(null);
    }
}

 

ItemServiceTest

@SpringBootTest
public class ItemServiceTest {

    private static final Logger logger = LoggerFactory.getLogger(ItemServiceTest.class);

    @Autowired
    private ItemService itemService;

    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void testPessimisticLocking() throws InterruptedException {
        logger.info("초기 아이템 데이터 설정");
        Item item = new Item();
        item.setName("Item 1");
        item.setQuantity(10);
        itemRepository.save(item);

        Thread thread1 = new Thread(() -> {
            logger.info("스레드 1: 아이템 수량 업데이트 시도");
            itemService.updateItemQuantity(item.getId(), 20);
            logger.info("스레드 1: 아이템 수량 업데이트 완료");
        });

        Thread thread2 = new Thread(() -> {
            logger.info("스레드 2: 아이템 수량 업데이트 시도");
            itemService.updateItemQuantity(item.getId(), 30);
            logger.info("스레드 2: 아이템 수량 업데이트 완료");
        });

        thread2.start();
        thread1.start();

        thread1.join();
        thread2.join();

        Item updateItem = itemService.findItemById(item.getId());
        logger.info("최종 아이템 수량 : {}", updateItem.getQuantity());
    }
}

 

테스트 결과

2024-12-24T11:43:20.771+09:00  INFO 21952 --- [locking] [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-12-24T11:43:21.294+09:00  INFO 21952 --- [locking] [    Test worker] o.s.d.j.r.query.QueryEnhancerFactory     : Hibernate is in classpath; If applicable, HQL parser will be used.
2024-12-24T11:43:22.520+09:00  WARN 21952 --- [locking] [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-12-24T11:43:23.394+09:00  INFO 21952 --- [locking] [    Test worker] c.example.locking.item.ItemServiceTest   : Started ItemServiceTest in 8.153 seconds (process running for 9.972)
2024-12-24T11:43:24.049+09:00  INFO 21952 --- [locking] [    Test worker] c.example.locking.item.ItemServiceTest   : 초기 아이템 데이터 설정
2024-12-24T11:43:24.108+09:00 DEBUG 21952 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    /* insert for
        com.example.locking.item.Item */insert 
    into
        item (name, quantity, id) 
    values
        (?, ?, default)
2024-12-24T11:43:24.124+09:00 TRACE 21952 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Item 1]
2024-12-24T11:43:24.125+09:00 TRACE 21952 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (2:INTEGER) <- [10]
2024-12-24T11:43:24.193+09:00  INFO 21952 --- [locking] [       Thread-4] c.example.locking.item.ItemServiceTest   : 스레드 1: 아이템 수량 업데이트 시도
2024-12-24T11:43:24.193+09:00  INFO 21952 --- [locking] [       Thread-5] c.example.locking.item.ItemServiceTest   : 스레드 2: 아이템 수량 업데이트 시도
2024-12-24T11:43:24.290+09:00 DEBUG 21952 --- [locking] [       Thread-4] org.hibernate.SQL                        : 
    /* select
        i 
    from
        Item i 
    where
        i.id = :id */ select
            i1_0.id,
            i1_0.name,
            i1_0.quantity 
        from
            item i1_0 
        where
            i1_0.id=? for update
2024-12-24T11:43:24.290+09:00 DEBUG 21952 --- [locking] [       Thread-5] org.hibernate.SQL                        : 
    /* select
        i 
    from
        Item i 
    where
        i.id = :id */ select
            i1_0.id,
            i1_0.name,
            i1_0.quantity 
        from
            item i1_0 
        where
            i1_0.id=? for update
2024-12-24T11:43:24.293+09:00 TRACE 21952 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [1]
2024-12-24T11:43:24.293+09:00 TRACE 21952 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [1]
2024-12-24T11:43:24.325+09:00 DEBUG 21952 --- [locking] [       Thread-4] org.hibernate.SQL                        : 
    /* update
        for com.example.locking.item.Item */update item 
    set
        name=?,
        quantity=? 
    where
        id=?
2024-12-24T11:43:24.326+09:00 TRACE 21952 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Item 1]
2024-12-24T11:43:24.327+09:00 TRACE 21952 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (2:INTEGER) <- [20]
2024-12-24T11:43:24.328+09:00 TRACE 21952 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (3:BIGINT) <- [1]
2024-12-24T11:43:24.333+09:00  INFO 21952 --- [locking] [       Thread-4] c.example.locking.item.ItemServiceTest   : 스레드 1: 아이템 수량 업데이트 완료
2024-12-24T11:43:24.334+09:00 DEBUG 21952 --- [locking] [       Thread-5] org.hibernate.SQL                        : 
    /* update
        for com.example.locking.item.Item */update item 
    set
        name=?,
        quantity=? 
    where
        id=?
2024-12-24T11:43:24.335+09:00 TRACE 21952 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Item 1]
2024-12-24T11:43:24.335+09:00 TRACE 21952 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (2:INTEGER) <- [30]
2024-12-24T11:43:24.335+09:00 TRACE 21952 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (3:BIGINT) <- [1]
2024-12-24T11:43:24.336+09:00  INFO 21952 --- [locking] [       Thread-5] c.example.locking.item.ItemServiceTest   : 스레드 2: 아이템 수량 업데이트 완료
2024-12-24T11:43:24.353+09:00 DEBUG 21952 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    select
        i1_0.id,
        i1_0.name,
        i1_0.quantity 
    from
        item i1_0 
    where
        i1_0.id=?
2024-12-24T11:43:24.354+09:00 TRACE 21952 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [1]
2024-12-24T11:43:24.356+09:00  INFO 21952 --- [locking] [    Test worker] c.example.locking.item.ItemServiceTest   : 최종 아이템 수량 : 30
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-12-24T11:43:24.394+09:00  INFO 21952 --- [locking] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-12-24T11:43:24.395+09:00 DEBUG 21952 --- [locking] [ionShutdownHook] org.hibernate.SQL                        : 
    drop table if exists item cascade 
2024-12-24T11:43:24.399+09:00  INFO 21952 --- [locking] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-12-24T11:43:24.402+09:00  INFO 21952 --- [locking] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
> Task :test
BUILD SUCCESSFUL in 27s
4 actionable tasks: 4 executed
AM 11:43:24: Execution finished ':test --tests "com.example.locking.item.ItemServiceTest"'.
  • 락이 걸리고 1개의 작업이 완료되고 다음 작업이 실행되는 것을 확인할 수 있다.

 

낙관적 락(Optimistic Lock)

Product

@Data
@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private Double price;

    @Version
    private Integer version;
}

 

ProductRepository

public interface ProductRepository extends JpaRepository<Product, Long> {
}

 

ProductService

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;

    @Transactional
    public void updateProductPrice(Long productId, Double newPrice) {
        try {
            Product product = productRepository
                    .findById(productId)
                    .orElseThrow(() -> new RuntimeException("Product not found"));

            product.setPrice(newPrice);
            productRepository.save(product);
        } catch (ObjectOptimisticLockingFailureException e) {
            System.err.println("낙관적 락 충돌 발생!!!!!");
        }
    }
}

 

ProductServiceTest

@SpringBootTest
public class ProductServiceTest {

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    public void testOptimisticLocking() throws InterruptedException {
        Product product = new Product();
        product.setName("Product 1");
        product.setPrice(100.0);
        productRepository.save(product);

        Thread thread1 = new Thread(() -> {
            productService.updateProductPrice(product.getId(), 200.0);
        });

        Thread thread2 = new Thread(() -> {
            assertThrows(OptimisticLockingFailureException.class, () -> {
                productService.updateProductPrice(product.getId(), 300.0);
            });
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}

 

테스트 결과

2024-12-24T12:14:03.697+09:00  INFO 8388 --- [locking] [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-12-24T12:14:04.252+09:00  INFO 8388 --- [locking] [    Test worker] o.s.d.j.r.query.QueryEnhancerFactory     : Hibernate is in classpath; If applicable, HQL parser will be used.
2024-12-24T12:14:05.536+09:00  WARN 8388 --- [locking] [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-12-24T12:14:06.311+09:00  INFO 8388 --- [locking] [    Test worker] c.e.locking.product.ProductServiceTest   : Started ProductServiceTest in 7.939 seconds (process running for 9.895)
2024-12-24T12:14:06.994+09:00 DEBUG 8388 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    /* insert for
        com.example.locking.product.Product */insert 
    into
        product (name, price, version, id) 
    values
        (?, ?, ?, default)
2024-12-24T12:14:07.003+09:00 TRACE 8388 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Product 1]
2024-12-24T12:14:07.004+09:00 TRACE 8388 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (2:DOUBLE) <- [100.0]
2024-12-24T12:14:07.004+09:00 TRACE 8388 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (3:INTEGER) <- [0]
2024-12-24T12:14:07.095+09:00 DEBUG 8388 --- [locking] [       Thread-4] org.hibernate.SQL                        : 
    select
        p1_0.id,
        p1_0.name,
        p1_0.price,
        p1_0.version 
    from
        product p1_0 
    where
        p1_0.id=?
2024-12-24T12:14:07.095+09:00 DEBUG 8388 --- [locking] [       Thread-5] org.hibernate.SQL                        : 
    select
        p1_0.id,
        p1_0.name,
        p1_0.price,
        p1_0.version 
    from
        product p1_0 
    where
        p1_0.id=?
2024-12-24T12:14:07.097+09:00 TRACE 8388 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [1]
2024-12-24T12:14:07.098+09:00 TRACE 8388 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [1]
2024-12-24T12:14:07.127+09:00 DEBUG 8388 --- [locking] [       Thread-4] org.hibernate.SQL                        : 
    /* update
        for com.example.locking.product.Product */update product 
    set
        name=?,
        price=?,
        version=? 
    where
        id=? 
        and version=?
2024-12-24T12:14:07.127+09:00 DEBUG 8388 --- [locking] [       Thread-5] org.hibernate.SQL                        : 
    /* update
        for com.example.locking.product.Product */update product 
    set
        name=?,
        price=?,
        version=? 
    where
        id=? 
        and version=?
2024-12-24T12:14:07.129+09:00 TRACE 8388 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Product 1]
2024-12-24T12:14:07.129+09:00 TRACE 8388 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Product 1]
2024-12-24T12:14:07.130+09:00 TRACE 8388 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (2:DOUBLE) <- [300.0]
2024-12-24T12:14:07.130+09:00 TRACE 8388 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (2:DOUBLE) <- [200.0]
2024-12-24T12:14:07.130+09:00 TRACE 8388 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (3:INTEGER) <- [1]
2024-12-24T12:14:07.130+09:00 TRACE 8388 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (4:BIGINT) <- [1]
2024-12-24T12:14:07.130+09:00 TRACE 8388 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (3:INTEGER) <- [1]
2024-12-24T12:14:07.130+09:00 TRACE 8388 --- [locking] [       Thread-5] org.hibernate.orm.jdbc.bind              : binding parameter (5:INTEGER) <- [0]
2024-12-24T12:14:07.131+09:00 TRACE 8388 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (4:BIGINT) <- [1]
2024-12-24T12:14:07.131+09:00 TRACE 8388 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (5:INTEGER) <- [0]
Exception in thread "Thread-5" org.opentest4j.AssertionFailedError: Expected org.springframework.dao.OptimisticLockingFailureException to be thrown, but nothing was thrown.
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:73)
	at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35)
	at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3128)
	at com.example.locking.product.ProductServiceTest.lambda$testOptimisticLocking$2(ProductServiceTest.java:31)
	at java.base/java.lang.Thread.run(Thread.java:842)
2024-12-24T12:14:07.157+09:00 DEBUG 8388 --- [locking] [       Thread-4] org.hibernate.SQL                        : 
    select
        p1_0.id,
        p1_0.name,
        p1_0.price,
        p1_0.version 
    from
        product p1_0 
    where
        p1_0.id=?
2024-12-24T12:14:07.158+09:00 TRACE 8388 --- [locking] [       Thread-4] org.hibernate.orm.jdbc.bind              : binding parameter (1:BIGINT) <- [1]
Exception in thread "Thread-4" org.springframework.orm.ObjectOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.locking.product.Product#1]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:325)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:244)
	at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:566)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:795)
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:758)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:698)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:416)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:727)
	at com.example.locking.product.ProductService$$SpringCGLIB$$0.updateProductPrice(<generated>)
	at com.example.locking.product.ProductServiceTest.lambda$testOptimisticLocking$0(ProductServiceTest.java:27)
	at java.base/java.lang.Thread.run(Thread.java:842)
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.example.locking.product.Product#1]
	at org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.identifiedResultsCheck(ModelMutationHelper.java:75)
	at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.lambda$doStaticUpdate$9(UpdateCoordinatorStandard.java:785)
	at org.hibernate.engine.jdbc.mutation.internal.ModelMutationHelper.checkResults(ModelMutationHelper.java:50)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.performNonBatchedMutation(AbstractMutationExecutor.java:141)
	at org.hibernate.engine.jdbc.mutation.internal.MutationExecutorSingleNonBatched.performNonBatchedOperations(MutationExecutorSingleNonBatched.java:55)
	at org.hibernate.engine.jdbc.mutation.internal.AbstractMutationExecutor.execute(AbstractMutationExecutor.java:55)
	at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.doStaticUpdate(UpdateCoordinatorStandard.java:781)
	at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.performUpdate(UpdateCoordinatorStandard.java:328)
	at org.hibernate.persister.entity.mutation.UpdateCoordinatorStandard.update(UpdateCoordinatorStandard.java:245)
	at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:169)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:644)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:511)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:414)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:41)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1429)
	at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:491)
	at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:2354)
	at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:1978)
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:439)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:169)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:267)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
	at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:562)
	... 10 more
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-12-24T12:14:07.198+09:00  INFO 8388 --- [locking] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-12-24T12:14:07.200+09:00 DEBUG 8388 --- [locking] [ionShutdownHook] org.hibernate.SQL                        : 
    drop table if exists item cascade 
2024-12-24T12:14:07.200+09:00 DEBUG 8388 --- [locking] [ionShutdownHook] org.hibernate.SQL                        : 
    drop table if exists product cascade 
2024-12-24T12:14:07.205+09:00  INFO 8388 --- [locking] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-12-24T12:14:07.207+09:00  INFO 8388 --- [locking] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
> Task :test
BUILD SUCCESSFUL in 15s
4 actionable tasks: 3 executed, 1 up-to-date
PM 12:14:07: Execution finished ':test --tests "com.example.locking.product.ProductServiceTest.testOptimisticLocking"'.
  • 낙관적 락을 적용했기 때문에 2번 째 작업을 수행하는 도중 버전 문제로 Exception이 발생하는 것을 확인할 수 있다.

 

데드 락(DeadLock)

ItemService(비관적 락 실습 코드에서 추가)

@Transactional(timeout = 1, isolation = Isolation.SERIALIZABLE)
public Item findItemById(Long itemId) {
    return itemRepository.findById(itemId).orElse(null);
}

public void updateItemsQuantity(Long itemId1, Long itemId2) {
    Item item1 = itemRepository.findByIdWithLock(itemId1);
    item1.setQuantity(item1.getQuantity() + 10);
    itemRepository.save(item1);

    try {
        Thread.sleep(4000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    Item item2 = itemRepository.findByIdWithLock(itemId2);
    item1.setQuantity(item2.getQuantity() + 10);
    itemRepository.save(item2);
}

 

ItemDeadLockTest

@SpringBootTest
public class ItemDeadLockTest {
    private static final Logger logger = LoggerFactory.getLogger(ItemDeadLockTest.class);

    @Autowired
    private ItemService itemService;

    @Autowired
    private ItemRepository itemRepository;

    @Test
    public void testDeadlock() throws InterruptedException {
        // 두 개의 아이템을 생성
        Item item1 = new Item();
        item1.setName("Item 1");
        item1.setQuantity(100);
        itemRepository.save(item1);

        Item item2 = new Item();
        item2.setName("Item 2");
        item2.setQuantity(200);
        itemRepository.save(item2);

        // 스레드 동기화를 위한 CountDownLatch 설정
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<Exception> exceptionInThread = new AtomicReference<>();

        Thread thread1 = new Thread(() -> {
            try {
                logger.info("스레드 1 시작");
                latch.await();  // 다른 스레드가 준비될 때까지 대기
                logger.info("스레드 1: 아이템 1 -> 아이템 2 업데이트 시도");
                itemService.updateItemsQuantity(item1.getId(), item2.getId());
                logger.info("스레드 1: 업데이트 완료");
            } catch (TransactionSystemException e) {
                logger.error("스레드 1: 트랜잭션 오류 발생 - {}", e.getMessage());
                exceptionInThread.set(e);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                logger.error("스레드 1: 알 수 없는 오류 발생", e);
                exceptionInThread.set(e);
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                logger.info("스레드 2 시작");
                logger.info("스레드 2: 아이템 2 -> 아이템 1 업데이트 시도");
                itemService.updateItemsQuantity(item2.getId(), item1.getId());
                logger.info("스레드 2: 업데이트 완료");
            } catch (TransactionSystemException e) {
                logger.error("스레드 2: 트랜잭션 오류 발생 - {}", e.getMessage());
                exceptionInThread.set(e);
            } catch (Exception e) {
                logger.error("스레드 2: 알 수 없는 오류 발생", e);
                exceptionInThread.set(e);
            }
        });

        // 두 스레드를 시작
        thread1.start();
        thread2.start();

        // 스레드가 준비되었음을 알리고 실행
        latch.countDown();

        // 두 스레드가 종료될 때까지 대기
        thread1.join();
        thread2.join();

        // 스레드 중 하나에서 TransactionSystemException이 발생했는지 확인
        assertThrows(Exception.class, () -> {
            if (exceptionInThread.get() != null) {
                throw exceptionInThread.get();
            }
        });
    }
}

 

테스트 결과

2024-12-24T12:30:25.387+09:00  INFO 38784 --- [locking] [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
2024-12-24T12:30:25.420+09:00 DEBUG 38784 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    drop table if exists item cascade 
2024-12-24T12:30:25.423+09:00 DEBUG 38784 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    drop table if exists product cascade 
2024-12-24T12:30:25.430+09:00 DEBUG 38784 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    create table item (
        quantity integer,
        id bigint generated by default as identity (start with 1),
        name varchar(255),
        primary key (id)
    )
2024-12-24T12:30:25.431+09:00 DEBUG 38784 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    create table product (
        price float(53),
        version integer,
        id bigint generated by default as identity (start with 1),
        name varchar(255),
        primary key (id)
    )
2024-12-24T12:30:25.439+09:00  INFO 38784 --- [locking] [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-12-24T12:30:25.976+09:00  INFO 38784 --- [locking] [    Test worker] o.s.d.j.r.query.QueryEnhancerFactory     : Hibernate is in classpath; If applicable, HQL parser will be used.
2024-12-24T12:30:27.274+09:00  WARN 38784 --- [locking] [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-12-24T12:30:28.052+09:00  INFO 38784 --- [locking] [    Test worker] c.example.locking.item.ItemDeadLockTest  : Started ItemDeadLockTest in 7.762 seconds (process running for 9.696)
2024-12-24T12:30:28.737+09:00 DEBUG 38784 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    /* insert for
        com.example.locking.item.Item */insert 
    into
        item (name, quantity, id) 
    values
        (?, ?, default)
2024-12-24T12:30:28.755+09:00 TRACE 38784 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Item 1]
2024-12-24T12:30:28.756+09:00 TRACE 38784 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (2:INTEGER) <- [100]
2024-12-24T12:30:28.825+09:00 DEBUG 38784 --- [locking] [    Test worker] org.hibernate.SQL                        : 
    /* insert for
        com.example.locking.item.Item */insert 
    into
        item (name, quantity, id) 
    values
        (?, ?, default)
2024-12-24T12:30:28.826+09:00 TRACE 38784 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (1:VARCHAR) <- [Item 2]
2024-12-24T12:30:28.826+09:00 TRACE 38784 --- [locking] [    Test worker] org.hibernate.orm.jdbc.bind              : binding parameter (2:INTEGER) <- [200]
2024-12-24T12:30:28.829+09:00  INFO 38784 --- [locking] [       Thread-5] c.example.locking.item.ItemDeadLockTest  : 스레드 2 시작
2024-12-24T12:30:28.829+09:00  INFO 38784 --- [locking] [       Thread-4] c.example.locking.item.ItemDeadLockTest  : 스레드 1 시작
2024-12-24T12:30:28.829+09:00  INFO 38784 --- [locking] [       Thread-5] c.example.locking.item.ItemDeadLockTest  : 스레드 2: 아이템 2 -> 아이템 1 업데이트 시도
2024-12-24T12:30:28.829+09:00  INFO 38784 --- [locking] [       Thread-4] c.example.locking.item.ItemDeadLockTest  : 스레드 1: 아이템 1 -> 아이템 2 업데이트 시도
2024-12-24T12:30:28.892+09:00 ERROR 38784 --- [locking] [       Thread-4] c.example.locking.item.ItemDeadLockTest  : 스레드 1: 알 수 없는 오류 발생

org.springframework.dao.InvalidDataAccessApiUsageException: Query requires transaction be in progress, but no transaction is known to be in progress
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:400) ~[spring-orm-6.2.1.jar:6.2.1]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246) ~[spring-orm-6.2.1.jar:6.2.1]
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:560) ~[spring-orm-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:343) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:160) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:136) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.2.1.jar:6.2.1]
	at jdk.proxy3/jdk.proxy3.$Proxy119.findByIdWithLock(Unknown Source) ~[na:na]
	at com.example.locking.item.ItemService.updateItemsQuantity(ItemService.java:29) ~[main/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:723) ~[spring-aop-6.2.1.jar:6.2.1]
	at com.example.locking.item.ItemService$$SpringCGLIB$$0.updateItemsQuantity(<generated>) ~[main/:na]
	at com.example.locking.item.ItemDeadLockTest.lambda$testDeadlock$0(ItemDeadLockTest.java:47) ~[test/:na]
	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]
Caused by: jakarta.persistence.TransactionRequiredException: Query requires transaction be in progress, but no transaction is known to be in progress
	at org.hibernate.internal.AbstractSharedSessionContract.prepareForQueryExecution(AbstractSharedSessionContract.java:528) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.beforeQuery(AbstractSelectionQuery.java:171) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.beforeQueryHandlingFetchProfiles(AbstractSelectionQuery.java:159) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:140) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:275) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.orm.jpa.SharedEntityManagerCreator$DeferredQueryInvocationHandler.invoke(SharedEntityManagerCreator.java:419) ~[spring-orm-6.2.1.jar:6.2.1]
	at jdk.proxy3/jdk.proxy3.$Proxy153.getSingleResult(Unknown Source) ~[na:na]
	at org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:224) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:93) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:152) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:140) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:170) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:149) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:69) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138) ~[spring-tx-6.2.1.jar:6.2.1]
	... 15 common frames omitted

2024-12-24T12:30:28.892+09:00 ERROR 38784 --- [locking] [       Thread-5] c.example.locking.item.ItemDeadLockTest  : 스레드 2: 알 수 없는 오류 발생

org.springframework.dao.InvalidDataAccessApiUsageException: Query requires transaction be in progress, but no transaction is known to be in progress
	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:400) ~[spring-orm-6.2.1.jar:6.2.1]
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246) ~[spring-orm-6.2.1.jar:6.2.1]
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:560) ~[spring-orm-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:343) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:160) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:136) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223) ~[spring-aop-6.2.1.jar:6.2.1]
	at jdk.proxy3/jdk.proxy3.$Proxy119.findByIdWithLock(Unknown Source) ~[na:na]
	at com.example.locking.item.ItemService.updateItemsQuantity(ItemService.java:29) ~[main/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:723) ~[spring-aop-6.2.1.jar:6.2.1]
	at com.example.locking.item.ItemService$$SpringCGLIB$$0.updateItemsQuantity(<generated>) ~[main/:na]
	at com.example.locking.item.ItemDeadLockTest.lambda$testDeadlock$1(ItemDeadLockTest.java:64) ~[test/:na]
	at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]
Caused by: jakarta.persistence.TransactionRequiredException: Query requires transaction be in progress, but no transaction is known to be in progress
	at org.hibernate.internal.AbstractSharedSessionContract.prepareForQueryExecution(AbstractSharedSessionContract.java:528) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.beforeQuery(AbstractSelectionQuery.java:171) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.beforeQueryHandlingFetchProfiles(AbstractSelectionQuery.java:159) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.list(AbstractSelectionQuery.java:140) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at org.hibernate.query.spi.AbstractSelectionQuery.getSingleResult(AbstractSelectionQuery.java:275) ~[hibernate-core-6.6.4.Final.jar:6.6.4.Final]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
	at org.springframework.orm.jpa.SharedEntityManagerCreator$DeferredQueryInvocationHandler.invoke(SharedEntityManagerCreator.java:419) ~[spring-orm-6.2.1.jar:6.2.1]
	at jdk.proxy3/jdk.proxy3.$Proxy153.getSingleResult(Unknown Source) ~[na:na]
	at org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:224) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:93) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:152) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:140) ~[spring-data-jpa-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:170) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:149) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:69) ~[spring-data-commons-3.4.1.jar:3.4.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-6.2.1.jar:6.2.1]
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.2.1.jar:6.2.1]
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138) ~[spring-tx-6.2.1.jar:6.2.1]
	... 15 common frames omitted

Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-12-24T12:30:28.949+09:00  INFO 38784 --- [locking] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2024-12-24T12:30:28.952+09:00 DEBUG 38784 --- [locking] [ionShutdownHook] org.hibernate.SQL                        : 
    drop table if exists item cascade 
2024-12-24T12:30:28.953+09:00 DEBUG 38784 --- [locking] [ionShutdownHook] org.hibernate.SQL                        : 
    drop table if exists product cascade 
2024-12-24T12:30:28.958+09:00  INFO 38784 --- [locking] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2024-12-24T12:30:28.962+09:00  INFO 38784 --- [locking] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
> Task :test
BUILD SUCCESSFUL in 14s
4 actionable tasks: 3 executed, 1 up-to-date
PM 12:30:29: Execution finished ':test --tests "com.example.locking.item.ItemDeadLockTest.testDeadlock"'.
  • 트랜잭션 2개가 서로 무한 대기하는 데드락이 발생하는 것을 알 수 있다.

정리

  • DB의 데이터 무결성을 보장하기 위한 Lock 방식에 대해 배워보고 실제로 테스트해 보았다.

'자바 심화 > TIL' 카테고리의 다른 글

동시성 제어 시점과 데이터 일관성 유지 관점  (1) 2024.12.30
Redis - Redisson  (2) 2024.12.27
장애 대응  (1) 2024.12.24
시큐어 코딩(Secure Coding)  (0) 2024.12.23
모니터링 - Slack Alert 보내기  (1) 2024.12.20