ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DB Lock
    개발/기타 2025. 5. 4. 23:57

    최근 간단한 예약 서비스 과제를 진행하면서, JPA의 Lock을 직접 다뤄야 하는 상황이 생겼다.  
    @Transactional만으로는 동시성 문제가 해결되지 않았고 비관적 락(Pessimistic Lock) 을 적용해 해결했다.

    이후 해당 내용에 대해 좀 더 자세히 알아보고자 글을 작성하게 되었다.

     

    대표적으로 사용하는 DB중 하나인 MySQL 기준으로 작성할 예정이며 아래와 같은 순서로 작성하겠다.

     

    1. DB Lock의 기본 개념

    2. Read Lock vs Write Lock

    3. 비관적 락 vs 낙관적 락

    4. 교착 상태

    5. ORM과 Lock (JPA)


    1. DB Lock

    Lock은 왜 사용해야 할까? 예를 들어 캐치테이블 같은 예약 앱을 생각해보자.

    여러 명의 유저가 동시에 예약을 시도했을 때, 누가 예약을 성공해야 할까? 모두 성공하면 중복 예약이 되고, 일부만 성공해도 일관성이 깨질 수 있다.

     

    이처럼 동시성 문제를 방지하기 위해 Lock이 필요하다.

    DB Lock은 트랜잭션 간 경쟁 조건(Race Condition)이나 Dirty Read를 방지하고, 데이터의 무결성과 일관성을 보장하기 위해 사용하는 기법이다.

     

    - 경쟁 조건(Race Condition)?

    둘 이상의 트랜잭션 또는 스레드가 동시에 동일한 자원(데이터)에 접근하여 결과가 실행 순서에 따라 달라지는 현상

     

    - Dirty Read?

    커밋되지 않은 트랜잭션의 데이터를 다른 트랜잭션이 읽는 현상 -> 잘못된 데이터 노출 위험

     

    - 데이터 무결성(Integrity)

    데이터의 형식, 제약 조건, 참조 관계 등이 정해진 규칙을 어기지 않고 유지되는 상태

     

    - 데이터 일관성

    트랜잭션 전후에 데이터가 논리적으로 정합된 상태를 유지하는 것


    2. Read Lock vs Write Lock 

    DB에서의 락은 읽기와 쓰기 작업을 구분하여 관리된다.

    - Read Lock (공유 락): 여러 트랜잭션이 동시에 데이터를 읽을 수 있음. 서로 충돌 없음.

    - Write Lock (배타 락): 데이터를 수정하는 트랜잭션이 단독으로 접근해야 하며, 다른 트랜잭션은 대기한다.

     

    1) 트랜잭션 격리 수준(Transaction Isolation Level)

    트랜잭션의 일관성과 동시성을 보장하기 위해 격리 수준이 필요하다. 각 격리 수준은 다음과 같은 특성과 잠금 전략을 가진다.

     

    - READ UNCOMMITTED: 다른 트랜잭션의 커밋되지 않은 데이터를 읽을 수 있음 (Dirty Read 허용)

    - READ COMMITTED: 커밋된 데이터만 읽음 (Non-repeatable Read 허용)

    - REPEATABLE READ: 동일 데이터를 반복 조회 시 값이 변하지 않음 (Phantom Read 허용)

    - SERIALIZABLE: 가장 엄격한 격리 수준, 트랜잭션을 직렬화하여 처리 (모든 읽기에 락 적용)

     

    2) 읽기 현상(Read Phenomena)

    트랜잭션 동시성에서 발생할 수 있는 대표적인 문제 상황들은 아래와 같다.

     

    - Dirty Read : 커밋되지 않은 데이터를 읽음

    - Non-repeatable Read : 같은 데이터를 두 번 읽었을 때 값이 다름

    - Phantom Read : 같은 조건의 SELECT에서 처음엔 없던 데이터가 나중엔 존재

     

    3) 락의 세부 범위(Lock Granularity)

    락은 적용되는 범위에 따라 성능과 충돌 가능성에 영향을 준다.

     

    - row-level Lock: 가장 정밀한 단위. 충돌은 적지만 비용 높음.

    - page-level Lock: 다수의 레코드를 포함하는 페이지 단위로 락

    - table-level Lock: 전체 테이블을 락으로 묶음. 성능은 좋지만 병렬성 낮음

     

    MySQL InnoDB는 기본적으로 row-level lock을 사용.

     

    3) 2단계 락 규약(2PL - two-phase locking protocol)

    2단계 락 규약은 트랜잭션에서 다음 두 단계를 반드시 따를 때 일관성과 직렬성을 보장한다.

     

    1. 확장 단계(Growing Phase): 락을 획득할 수는 있지만 해제할 수는 없음

    2. 축소 단계(Shrinking Phase): 락을 해제할 수는 있지만 새로운 락을 획득할 수는 없음

     

    이 규칙을 지키면 트랜잭션 간 충돌 시 데이터 불일치나 교착 상태를 피할 수 있다.


    3. 비관적 락 vs 낙관적 락

    1) 비관적 락(Pessimistic Lock)

    - 다른 트랜잭션이 동시에 데이터를 변경할 수 있다고 비관적으로 가정

    - 데이터에 접근할 때 즉시 락을 획득

    - 다른 트랜잭션이 접근 못 하도록 선제적으로 제어

     

    장점: 확실한 충돌 방지
    단점: 락을 지속적으로 유지하기 때문에 락 보유 시간이 길어질수록 성능 저하 발생

     

    2) 낙관적 락 (Optimistic Locking)

    - 대부분 충돌이 없을 것이라 낙관적으로 가정

    - 트랜잭션이 끝나는 시점에서 버전 정보를 비교해 충돌 여부 판단

    장점: 충돌이 없으면 성능 우수, 락이 없기 때문에 다른 트랜잭션과 병렬 실행 가능
    단점: 충돌이 발생할 경우 예외를 처리해야 함


    4. 교착 상태(DeadLock)

    교착 상태란 두 개 이상의 트랜잭션이 서로 상대방의 락을 기다리면서 영원히 대기 상태에 빠지는 현상이다.

     

    교착 상태를 방지하려면 다음과 같은 전략을 고려할 수 있다.

    - 트랜잭션 순서화: 락을 획득하는 순서를 일정하게 만들어 교착 상태를 방지

    - 락 타임아웃 설정: 일정 시간 동안 락을 얻지 못하면 트랜잭션을 롤백하도록 설정

    - 최소화된 락 범위 사용: 필요한 최소 범위만 락을 걸어 다른 트랜잭션의 대기 시간을 줄임

    MySQL InnoDB는 Deadlock 발생 시 하나의 트랜잭션을 강제로 롤백하여 해결한다고 한다.


    5. JPA의 Lock

    0) JPA의 Lock의 종류

    JPA에서 사용하는 LockType의 종류와 설명은 아래와 같다.

    public enum LockModeType {
        READ,               // 낙관적 락 (Optimistic Lock)
        WRITE,              // 낙관적 락 (OPTIMISTIC_FORCE_INCREMENT) 
        OPTIMISTIC,         // 낙관적 락
        OPTIMISTIC_FORCE_INCREMENT, // 낙관적 락 + 버전 증가
        PESSIMISTIC_READ,   // 비관적 읽기 락
        PESSIMISTIC_WRITE,  // 비관적 쓰기 락
        PESSIMISTIC_FORCE_INCREMENT, // 비관적 락 + 버전 증가
        NONE                // 락 적용 안함
    }

     

    1) JPA 비관적 락(pessimistic lock)

    @Lock(LockModeType. PESSIMISTIC_WRITE)을 사용하면 비관적 락을 적용할 수 있다.

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    public Entity findEntityBuId(Long id);

     

    2) JPA 낙관적 락(optimistic lock)

    @Lock(LockModeType. OPTIMISTIC)과 엔티티에 @Version 어노테이션을 사용하여 버전 관리를 통해 충돌 여부를 확인한다.

    @Entity
    public class Entity {
        @Id
        private Long id;
    
        @Version
        private Long version;
    }

     

    3) JPA의 락 예외 처리

    락의 종류에 따라 다음과 같은 예외가 발생할 수 있다.

    예외 클래스 발생 상황
    PessimisticLockException 비관적 락 획득 실패 시 (다른 트랜잭션이 선점 중)
    OptimisticLockException 낙관적 락 사용 시 버전 충돌이 발생했을 때
    CannotAcquireLockException DB에서 Deadlock 또는 락 대기시간 초과 (timeout) 발생 시
    LockTimeoutException (JPA 2.2+) 특정 시간 안에 락을 획득하지 못한 경우

    비관적 락 예외 처리

    비관적 락은 락을 선점하려다 실패하거나, DB 락 타임아웃 설정에 걸리면 아래와 같은 예외가 발생할 수 있다.

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    public Item findWithPessimisticLock(Long id) {
        try {
            return entityManager.find(Item.class, id, LockModeType.PESSIMISTIC_WRITE);
        } catch (PessimisticLockException | LockTimeoutException e) {
            // 다른 트랜잭션이 선점 중이거나 타임아웃이 발생한 경우
            throw new BusinessException("잠시 후 다시 시도해주세요. 현재 다른 사용자가 작업 중입니다.");
        }
    }

     

    낙관적 락 예외 처리

    낙관적 락은 병행 트랜잭션 간 충돌 시점에 발생하며, 주로 merge() 또는 flush() 시점에 예외가 발생한다.

    @Transactional
    public void updateProduct(ProductRequest dto) {
        Product product = productRepository.findById(dto.getId())
            .orElseThrow(() -> new NotFoundException("존재하지 않는 상품입니다."));
    
        product.updateName(dto.getName()); // 변경
    
        try {
            // flush 또는 트랜잭션 종료 시점에서 충돌 감지
        } catch (OptimisticLockException e) {
            throw new ConflictException("다른 사용자가 먼저 수정했습니다. 다시 시도해주세요.");
        }
    }

    + 재시도 전략 (Retryable)

    낙관적 락 충돌 시 @Retryable을 사용하는 전략도 고려할 수 있다.

    @Retryable(
        value = { OptimisticLockException.class },
        maxAttempts = 3,
        backoff = @Backoff(delay = 200)
    )
    @Transactional
    public void updateProductWithRetry() {
        // 업데이트 로직
    }

    Lock에 대해 작성해봤고 ORM에서 Lock을 사용하는 방법과 예외 처리 방법에 대해 작성해봤다.

    실무에서 성능과 동시성 요구사항에 따라 어떤 락 전략을 선택해야 할 지 고민하는데 도움이 되리라 생각한다.

     

     

    출처

    https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-transaction-model.html

    https://www.postgresql.org/docs/current/explicit-locking.html

    반응형

    댓글

Designed by Tistory.