Projects/HubEleven

[동시성 처리] 동시성 문제 (Race Condition) 해결 방법 3가지 관점 - (2) Database Lock

annovation 2026. 3. 9. 03:09

Database Lock

  • 데이터베이스가 제공하는 Lock을 활용하여 데이터 정합성을 맞출 수 있다.

💡Pessimistic Lock (비관적 락)

출처 : https://minjooig.tistory.com/147#비관적%20락(Perssimistic%20Locking)이란%3F-1

  • 실제 데이터에 직접적으로 Lock을 거는 방식
  • 다른 트랜잭션은 Lock이 해제되기 전까지 데이터를 읽을 수 없다.

1) 특징 (참고 자료)

  • 데이터 충돌을 피하기 위해 먼저 Lock을 걸고 작업을 진행
  • 일반적으로 Exclusive Lock(X Lock, 배타적 락)을 사용해 다른 트랜잭션의 Read/Write을 방지한다.
  • 은행 계좌, 항공권 좌석, 콘서트 티켓 등 충돌이 빈번하고 데이터 정합성이 중요한 환경에서 유용하다.

2) 장점 (참고 자료)

  • 충돌이 빈번하게 일어날 경우 Optimistic Lock보다 성능이 좋을 수 있다.
  • Lock을 통해 update를 제어하기 때문에 데이터의 정합성이 보장된다.
    (데이터를 수정할 때 미리 잠가(lock) 버리기 때문에, 여러 사람이 동시에 건드려도 데이터가 꼬이지 않는다는 의미)

3) 단점 (참고 자료)

  • 서로 다른 자원을 쥐고 놓지 않는 데드락(Deadlock) 발생 가능

4) 사용 방법

  • Spring Data JPA에서는 @Lock 어노테이션을 사용하여 구현할 수 있다.
public interface StockRepository extends JpaRepository<Stock, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select s from Stock s where s.id = :id")
    Stock findByIdWithPessimistic(Long id);
}

 

➡️ @Lock(LockModeType.PESSIMISTIC_WRITE)

  • 해당 데이터에 대해 쓰기 잠금을 설정하여, 다른 트랜잭션이 해당 데이터를 읽거나 수정하지 못하게 막는다.

💡Optimistic Lock (낙관적 락)

출처 : https://minjooig.tistory.com/147#비관적%20락(Perssimistic%20Locking)이란%3F-1

  • 데이터를 읽고 수정할 때까지 락을 걸지 않고, 수정 시점에서 충돌 여부를 확인
  • 실제로 락을 이용하지 않고 version 을 이용해 데이터 정합성을 맞춤

1) 특징 (참고 자료)

  • 먼저 데이터를 읽은 후에 update를 할 때 내가 읽은 버전이 맞는 확인하며 업데이트한다.
  • 일반적으로 버전번호 또는 타임스탬프를 활용해 변경 여부를 확인
  • 동시성이 높은 환경에서 유용하며, 충돌 가능성이 낮은 경우 적합

2) 장점 (참고 자료)

  • 별도의 Lock을 잡지 않아 Pessimistic Lock보다 성능상으로 이점이 있다.

3) 단점 (참고 자료 1, 참고 자료 2)

  • 충돌이 발생하면 다시 데이터를 읽고 재시도해야 한다.
  • update가 실패했을 때 재시도 로직을 개발자가 직접 작성해야 하는 번거로움이 있다.

3-1) 예외처리를 하는 이유 (참고 자료)

 

1. 사용자 A가 Stock 데이터를 조회 (SELECT)

  • id=1, quantity=100, version=1

2. 사용자 B가 동일한 Stock 데이터를 조회 (SELECT)

  • id=1, quantity=100, version=1

3. 사용자 A가 Stock을 업데이트 (UPDATE)

UPDATE stock SET quantity=90, version=2 WHERE id=1 AND version=1;
  • 성공적으로 업데이트됨 (version=2로 증가)

4. 사용자 B가 Stock을 업데이트 시도 (UPDATE)

UPDATE stock SET quantity=80, version=2 WHERE id=1 AND version=1;
  • version=1을 가진 데이터가 없으므로 예외 발생 (OptimisticLockException)

4) 사용 방법

  • Stock Entity Class 에 version 컬럼 추가
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock {

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

  private Long productId;

  private Long quantity;

  @Version
  private Long version;
  
  //생략...
}

 

➡️ 주의 : javax.persistence 패키지에 있는 @Version 어노테이션을 사용

  • Repository
public interface StockRepository extends JpaRepository<Stock, Long> {

  @Lock(LockModeType.OPTIMISTIC)
  @Query(" select s from Stock s where s.id = :id ")
  Stock findByIdWithOptimisticLock(Long id);
  
}

 

➡️ Spring Data JPA에서는 @Lock 어노테이션을 사용하여 구현할 수 있습니다.

  • facade 생성
@Component
public class OptimisticLockStockFacade {

    private final OptimisticLockStockService optimisticLockStockService;

    public OptimisticLockStockFacade(OptimisticLockStockService optimisticLockStockService){
        this.optimisticLockStockService = optimisticLockStockService;
    }

    public void decrease(Long id, Long quantity) throws InterruptedException{
        while (true) {  //업데이트를 실패했을 때 재시도를 해야함
            try {
                optimisticLockStockService.decrease(id, quantity);

                //정상적으로 업데이트 되면 빠져나감
                break;
            } catch (Exception e) {
                //수량 감소 실패 시, 재시도
                Thread.sleep(50);
            }
        }
    }
}

 

➡️ Optimistic Lock은 실패했을 경우 재시도를 해야하기 때문에 facade(퍼사드)와 같은 재시도 로직을 생성해야한다.

 

💡Named Lock

 

1-1) 특징

  • 이름을 가진 메타데이터를 사용하는  metadata locking 방법이다. (참고 자료)
  • 이름을 가진 lock을 획득한 후 해제할 때까지 다른 세션이 이 lock을 획득할 수 없도록 한다. (참고 자료)
  • 트랜잭션이 종료 될 때 Lock이 자동으로 해제되지 않으므로 별도의 명령어로 해제가 필요하다. (참고 자료)
  • MySQL에서는 GET_LOCK() 함수를 사용해서 사용자가 정의한 이름(name)을 기준으로 lock을 획득할 수 있다. (공식 문서)

1-2) Named Lock 데이터 적용 과정 (참고 자료)

  • Named Lock은 DB에 락을 걸지 않고, 별도의 공간에 락을 건다.
  • session-1이 1이라는 이름으로 락을 건다면 session-1이 1을 해지한 후에 락을 얻을 수 있다.

2) 장점

 

3) 단점 (참고 자료)

  • 락을 획득하지 못하면 대기하거나 재시도 로직 필요
  • 트랜잭션 종료 시 자동으로 Lock 해제 되지 않아 Lock을 걸고 해제하는 별도 로직이 필요

4) 사용 방법 (참고 자료)

  • Named Lock 을 수행하는 repository 를 생성 한다.
public interface LockRepository extends JpaRepository<Stock, Long> {
    @Query(value = "select get_lock(:key, 3000)", nativeQuery = true)
    void getLock(String key);

    @Query(value = "select release_lock(:key)", nativeQuery = true)
    void releaseLock(String key);
}
  • Lock 획득 해제하는 별도의 명령을 위해 Facade 패턴을 생성한다.
@Component
public class NamedLockStockFacade {

    private final LockRepository lockRepository;
    private final StockService stockService;

    public NamedLockStockFacade(LockRepository lockRepository, StockService stockService){
        this.lockRepository = lockRepository;
        this.stockService = stockService;
    }

    //decrease 메서드
    @Transactional
    public void decrease(Long id, Long quantity){
        try {
            //lock 획득
            lockRepository.getLock(id.toString());
            //재고 감소
            stockService.decrease(id, quantity);
        }finally {
            //모든 로직이 종료되었을 때, lock 해제
            lockRepository.releaseLock(id.toString());
        }
    }
}
  • StockService propagation 수정
@Transactional(propagation = Propagation.REQUIRES_NEW)
public synchronized void decrease(Long id, Long quantity){
    // Stock 조회
    Stock stock = stockRepository.findById(id).orElseThrow();

    // 재고를 감소
    stock.decrease(quantity);

    // 갱신된 값을 저장
    stockRepository.saveAndFlush(stock);
}

 

➡️ 부모의 트랜잭션과 별도로 실행되어야 하기 때문에 propagation을 변경해주어야 한다.

❗️Propagation(전파) 란?

현재 메서드가 트랜잭션을 어떻게 사용할지 결정하는 규칙

ex.
@Transactional(propagation = Propagation.REQUIRED) -> 기존 트랜잭션 사용, 없으면 새로 생성
@Transactional(propagation = Propagation.REQUIRES_NEW) -> 항상 새로운 트랜잭션 생성, 기존 트랜잭션은 일시 정지
  • connection pool size 수정

 

  • 같은 데이터 소스를 사용하기 위해 connection pool size를 변경합니다.
    maximum-pool-size: 40

참고 자료

1) 블로그 : SpringBoot 동시성 이슈 해결방법[3] - Lock

https://velog.io/@kiteof_park/SpringBoot-pessimistic-lock

 

SpringBoot 동시성 이슈 해결방법[3] - Lock

synchronized의 한계와 Lock의 종류 - Pessimistic Lock, Optimistic Lock, Named Lock

velog.io

2) 블로그 : [Java/Spring] 재고 시스템으로 알아보는 동시성 이슈와 해결 방법

https://velog.io/@ha02e/Java-Spring-ConcurrencyIssue#2-database-lock

 

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

인프런 재고시스템으로 알아보는 동시성이슈 해결방법 강의를 듣고 작성한 글입니다.

velog.io

3) 블로그 : [Java/Spring] 재고시스템으로 알아보는 동시성 이슈 해결 방법

https://girokeulhaja.tistory.com/107

 

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

❗해당 포스팅은 인프런에서 제공해 주는 강의 내용을 개인적으로 정리하였음을 알려드립니다. 재고시스템으로 알아보는 동시성이슈 해결방법 강의 | 최상용 - 인프런최상용 | , 동시성 이슈

girokeulhaja.tistory.com

4) 유튜브 : 락의 두 종류: 비관적 락 vs 낙관적 락

https://www.youtube.com/watch?v=oJrVl6QKzHw

 

5) 블로그 : 인프런) 재고시스템으로 알아보는 동시성 이슈 해결 (1)

https://dodop-blog.tistory.com/463

 

인프런) 재고시스템으로 알아보는 동시성 이슈 해결 (1)

동시성 이슈 해결을 위한 인프런 강의를 듣고 실습해보았다. https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard 재고시스템으로 알아보는 동

dodop-blog.tistory.com

6) MySQL Docs : Locking Functions

https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html

 

MySQL :: MySQL 8.0 Reference Manual :: 14.14 Locking Functions

This section describes functions used to manipulate user-level locks. Table 14.19 Locking Functions GET_LOCK(str,timeout) Tries to obtain a lock with a name given by the string str, using a timeout of timeout seconds. A negative timeout value means infin

dev.mysql.com