DB

DB 의 동시성 제어

Dean83 2025. 9. 1. 23:32

DB의 같은 테이블에서 동시에 접근을 하여 작업을 하는경우 (읽기만 하는 경우 제외) 문제가 발생한다. 데이터가 갱신되지 않거나 둘다 실패하거나 하는 일이 발생 할 수 있고, 이를 막기 위해 동시성을 제어하는 처리가 필요하다. 이게 정답이다! 라는 상황은 없고 데이터 특성이나 비즈니스 로직 상황에 맞게 골라서 처리를 해야 한다.  물론 대부분은 DBMS 가 처리를 해주나, 개발자가 수동으로 처리를 해야 할 때가 있다.

 

크게 2가지로 나눠볼 수 있다. 

 

  • 비관적 락
    • 동시에 같은 데이터를 수정하는 일이 잦은 경우 (결제 처리, 쇼핑몰 구매로 인한 재고 감소 등)
    • DB에서 처리 해준다 (후속 요청을 홀드 후 처리한다.) 때문에, DB에 부담이 될 수 있다. 
  • 낙관적 락
    • 같은 데이터를 수정하는 일보다, 읽는 비율이 높은 경우 사용. 
    • 어플리케이션에서 처리 한다.

 

예제 코드를 통해 알아보자. 

 

1. 엔티티

//lombok 을 이용한 자동 getter, setter 설정
@Entity
@getter
@setter
public class testEntity
{
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private int itemCount;

    @Version // 낙관적 락 관리용 버전 컬럼
    private int version;
}
  • @Version 어노테이션이 붙어있는 엔티티의 경우, 기본적으로 낙관적 락으로 동작을 한다. 

 

2. 리파지토리

public interface testRepository JpaRepository<testEntity, Long>
{
	@Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<testEntity> findByIdPessimistic(Long id);
}
  • @Lock 어노테이션 타입이 PESSIMISTIC_WRITE 인 메소드의 경우 비관적 락으로 동작 한다. 
  • 해당 메소드 호출시 커밋, 롤백하기 전까지 락이 걸린다.

 

3. 비관적 락 서비스 예

@Service
public class PessimisticService {
    private final testRepository repository;

    public PessimisticService(testRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void testQuery(Long productId) {
        testEntity testItem = repository.findByIdPessimistic(productId);
        if (testItem.getItemCount() <= 0) {
            throw new RuntimeException("오류 발생");
        }
        testItem.setItemCount(testItem.getItemCount() - 1);
        // commit 시점에 DB row lock 자동 해제
    }
}
  • 리파지토리의 @Lock 어노테이션 타입이 PESSIMISTIC_WRITE 인 메소드를 호출시 자동으로 lock이 걸린다. 
  • 오류가 발생하여 자동으로 롤백하거나, 처리가 완료되어 자동으로 commit 될 때 자동으로 lock을 해제 한다.
  • 만일 A 트랜잭션이 작업중인데, B 트랜잭션이 똑같이 작업 하려 하면 B는 무조건 실패 하는가? 답은 아니다. DBMS 에서 B를 잠시 대기 시켜 놓은 후 처리를 한다. (물론 실패하는 경우도 있다. timeout에 걸리거나 데드락에 걸릴경우)
  • 따라서 비관적 락의 경우 개발자가 보통 재시도 코드를 넣지 않아도 된다. 
  • 하지만 타임아웃이나 데드락 때문에 실패할 경우도 있으니, 재시도 코드를 넣는게 어떤가 싶다.

 

4. 낙관적 락 서비스 예

@Service
public class OptimisticService {

    private final testRepository repository;
    private static final int MAX_RETRIES = 3;

    public OptimisticService(testRepository repository) {
        this.repository = repository;
    }

    public void testQueryWithRetry(Long productId, int quantity) {
        int attempt = 0;
        boolean success = false;

        while (!success && attempt < MAX_RETRIES) {
            attempt++;
            try {
                testQuery(productId, quantity);
                success = true;
            } catch (OptimisticLockingFailureException e) {
                System.out.println("버전 충돌 발생, 재시도 " + attempt);
                // 잠시 대기 후 재시도 가능
                try { Thread.sleep(50); } catch (InterruptedException ignored) {}
            }
        }

        if (!success) {
            throw new RuntimeException("재시도 후에도 처리 실패");
        }
    }

    @Transactional
    public void testQuery(Long Id, int quantity) {
        testEntity testItem = repository.findById(Id)
                .orElseThrow(() -> new RuntimeException("해당 항목 없음"));

        if (testItem.getItemCount() < quantity) {
            throw new RuntimeException("오류 발생");
        }

        testItem.setItemCount(testItem.getItemCount() - quantity);
        // 트랜잭션 커밋 시점에 JPA가 @Version 필드 기준으로 낙관적 락 충돌 체크
    }
}
  • 낙관적 락의 경우, 커밋을 하는 시점에 버전을 확인한다. 만일 버전이 다르다면 쿼리가 실패로 리턴되기 때문에 개발자가 재시도를 해주어야 한다. 

 

  • 비관적 락과 낙관적 락의 간단 차이점
    • 비관적 락의 경우 A 트랜잭션 처리도중 B 트랜잭션이  들어오면 DBMS에서 B를 잠시 대기 -> A 완료시 B를 수행
    • 낙관적 락의 경우 A 트랜잭션 처리도중 B 트랜잭션이 들어오면 락을 걸지 않음 -> B 트랜잭션 처리 시도시 버전이 달라졌다면 (A 트랜잭션 때문에) 실패를 반환 -> 개발자가 재시도 코드를 추가 해야함.

'DB' 카테고리의 다른 글

정규화, 역정규화  (0) 2025.09.29
DB 수동 Index  (3) 2025.08.27