트러블슈팅

[Spring] 재고처리 동시성 이슈 해결하기 (2/2)

jny0 2023. 7. 22. 03:16

저번 글에 이어서 데이터베이스 락을 활용하여 동시성 이슈를 해결하는 방법을 정리해보려고 한다.

(프로젝트 DB로 MySQL를 사용하고 있음)

 

해결방법 2. DB에 비관적 락 걸기

 

비관적 락 (Pessimistic Lock)

  • 트랜잭션 충돌이 발생한다고 가정하고 미리 락을 걸어버리는 방법
  • 데이터베이스의 읽기락, 쓰기락 사용
  • 읽기 락 : 다른 트랜잭션에서 읽기만 가능, 쓰기는 불가능
  • 쓰기 락 : 다른 트랜잭션에서 읽기, 쓰기 모두 불가능
  • ex. 서버 1 DB에서 데이터를 가져올 때 비관적 락을 걸면, 다른 서버에서는 서버 1의 작업이 끝나서 락이 풀릴때까지 데이터에 접근하지 못함

 

그림으로 설명하면 아래와 같다.

DB에 쓰게에 대한 비관적 락이 걸린 경우, 트랜잭션 A가 커밋되기 전에는 트랜잭션 B의 select 요청은 쓰기 락에 의해 차단되고, 트랜잭션 A가 커밋된 이후에 select를 실행한다.

 

특징

  • 충돌이 자주 발생하는 환경에서 롤백의 횟수를 줄일 수 있다
  • 데이터의 정합성을 어느정도 보장할 수 있다
  • 데이터 자체에 락을 걸기 때문에 동시성이 떨어져 성능 저하가 있을 수 있다(특히 읽기가 많이 이루어지는 DB의 경우)
  • 데드락이 걸릴 수 있다 (스레드들이 서로 작업이 끝날 때까지 기다리면서 무한 대기 상태에 빠짐)

 

실제 코드에 적용해보자

public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select p from Product p where p.id = :id")
    Product findByWithPessimisticLock(final Long id);
}

JPA에서는 @Lock 어노테이션을 사용해 락을 설정할 수 있다.

@Transactional
public void updateStockAndSalesByOrder(Long productId, Long quantity) {
    Product lockedProduct = findWithWriteLockById(productId);
    lockedProduct.updateStockAndSalesByOrder(quantity);
    productRepository.saveAndFlush(lockedProduct);
}

 

테스트도 잘 통과하고 for update 절이 쿼리에 추가되어서 비관적 쓰기 락이 잘 적용된 것을 확인할 수 있다.

 

📌 이슈

사실 비관적 락을 적용하고도 테스트가 통과되지 않는 문제가 있었다. 재고도 계속 100개로 조회되어 뭐가 문제일까 생각했다.

삽질을 거친 결과 테스트 클래스단에 붙어있던 @Transactional 어노테이션 때문이었다.

다른 메서드 테스트때문에 클래스단에 @Transactional를 설정해두었는데, 동시성 테스트에서는 트랜잭션 범위 내에서 "for update" 락을 설정하는 부분이 롤백되면서 락도 해제되는 것이 문제가 되었던 것 같다.

동시성 테스트 메서드에 @Transactional(propagation = Propagation.NOT_SUPPORTED)를 설정하여 해당 메서드를 실행하는 동안 새로운 트랜잭션을 만들지 않고, 기존 트랜잭션을 일시적으로 중단시켜주었더니 테스트 통과가 잘 되었다.

 

 

 

해결방법 3. DB에 낙관적 락 걸기

 

낙관적 락(Optimistic Lock)

  • 트랜잭션 대부분은 충돌이 발생하지 않는다고 가정하고, 동시성 문제가 발생하면 처리하는 방법이다
  • 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다
  • 일반적으로 version을 보고 충돌을 확인하고, 충돌이 확인된 경우 롤백을 진행

 

트랜잭션 B가 update하려고 할 때 조회할 때와 version이 맞지 않기 때문에 update하지 못하게 된다.

 

특징

  • 충돌이 자주 발생하지 않을 경우에 동시성 처리 성능이 좋다
  • 잦은 충돌이 일어나는 경우 롤백 처리에 대한 비용이 많이들어 성능상 손해를 볼 수 있다.
  • 롤백을 개발자가 수동으로 처리해주어야 한다

 

낙관적 락을 사용하기 위해서는 version 컬럼을 추가해야한다.

//Product.java
public class Product extends BaseEntity {
	// 기존 필드

	@Version
    	private Long version;

	// 기존 코드
}
public interface ProductRepository extends JpaRepository<Product, Long>, ProductRepositoryCustom {
    @Lock(value = LockModeType.OPTIMISTIC)
    @Query("select p from Product p where p.id = :id")
    Product findByIdWithOptimisticLock(final Long id);
}

 

 

 

참고

재고시스템으로 알아보는 동시성이슈 해결방법

[Spring & Java] 🚀 재고시스템으로 알아보는 동시성이슈 해결방법

[재고시스템으로 알아보는 동시성이슈 해결방법] 3. Database Lock