본문 바로가기
Java Projects/Palette 칸반보드 툴 2025. 4. 7.

(25.04.07) 비관적 락(Pessimistic Lock) 동시성 제어 적용

 

 

 

(24.07.15)[14주차] 프로젝트 중 동시성 제어를 위한 Redisson 활용

많은 요청이 한번에 들어올 경우 DB에 CRUD간 데이터의 일관성, 무결성, 정합성의 DB 데이터 특성을 보호하기 위해서 동시성을 제어할 필요가 있다. 쓰레드나 프로세스가 동시에 데이터에 접근하

andrew75313.tistory.com

예전에 진행했던 Side Project인 칸반보드 툴을 제작한 프로젝트에서,
칸반보드의 컬럼당 카드(할일) 을 생성하는데 동시성 문제가 발생했다.
여러 서버(여러 사용자) 가 같은 컬럼을 향해 카드 생성 요청을 할 경우, 카드 순서를 담당하는 필드 값이 1씩 늘어나지 않고 동일한 위치값으로 내지는 더 많은 위치값으로 뛰어넘고 저장되는 이슈였다.

 

DB 단에서 저장이 될때 순차적으로 숫자를 부여할 수는 있었지만, 칸반보드의 특성상 순서를 빠르게 옮기는 기능도 포함했기에, 그 과정에서 순서를 정하는 로직이 얽힐 것이라 판단해 동시성 제어 이슈를 해결하고자 Redission  Lock  분산락을 적용했다.

 

이는, 다수의 서버의 요청에 대해 DB 역시 Scale-out 또는 다중 DB로 확장할 경우를 대응하여, DB 에서 락을 부여하는 방식이 아닌, 

비동기 메모리 인 Redis의  Redisson Lock을 활용하고자 했다.

 

하지만, 다시 프로젝트를 리뷰하면서, 단일 DB를 활용하는 경우, DB단에서 Transaction 단위로 Lock을 부여하는 비관적 락을 활용해도 충분히 구현이 가능하다고 판단했고, 스스로 이를 적용하여, 추가로 적용을 해보았다.

+
비관적 락에 대해서도 다시한번 정리했다.

 


비관적 락 Pessimistic Lock

  • 트랜잭션이 데이터를 읽거나 수정하기 전에 해당 데이터에 락(Lock)을 먼저 걸어다른 트랜잭션이 접근하지 못하게 막는 방식
    • 읽기(Shared Lock) 혹은 쓰기(Exclusive Lock) 접근을 방지
  • SELECT * FROM product WHERE id = 1 FOR UPDATE; 이 작동되는 것과 같음

종류 / Spring Data JPA

  • Shared lock (읽기 잠금, s-lock) LockModeType.PESSIMISTIC_READ
    • 락을 획득한 트랜잭션에서만 대상 레코드를 수정, 삭제
    • 락을 획득하지 못한 트랜잭션은 읽기만 허용하는 방법
  • Exclusive Lock (쓰기 잠금, x-lock) LockModeType.PESSIMISTIC_WRITE
    • 락을 획득하지 못한 트랜잭션에서 대상 레코드를 수정, 삭제 뿐 아니라 읽기도 허용하지 않는 방법
      • 트랜잭션 종료 전까지 해당 행(row)에 락을 걸어, 다른 트랜잭션에서 읽거나 수정할 수 없게
    • 한 번에 하나의 트랜잭션만 작업을 수행함을 보장

주의

  • Deadlock
    • 다수의 요청이 있을 때, (다수의 서버에서의 같은 요청 등) 여러 트랜잭션이 서로 락을 기다리면 교착 상태 → 제한 시간을 설정해줘야
  • 응답 지연
    • 락이 오래 유지되면 대기 시간 증가 → 역시 제한시간을 설정해서 무한 대기를 막아야 클라이언트에게 제한 시간이 지나면 알려줘야함
  • 멀티서버 환경 에서의 주의
    • DB는 단일 인스턴스임에도  애플리케이션이 여러 서버일 경우 락은 DB 차원에서만 작동하여 정상적으로 동시성 제어가 가능
    • → 단, DB에게 전적으로 동시성 제어를 맞기기 때문에, DB에 무리를 주거나 성능 저하가 발생 가능
  • 다중 데이터베이스 환경에서 적용 불가능
    • Scale Out 되는 환경, 샤딩, 복제 등 데이터베으가 늘어나는 경우, 각 DB 단위로 Transaction 에 대해 Lock 을 부여하기 때문에 동시성 제어하는데 까다롭고 트랜잭션간 경합이 무조건 발생
    • → 분산 락을 활용해야

Palette Project의 칸반보드의 카드 생성 시, 순서 데이터 동시성 제어 : 비관적 락

Column Repository

// Pessimistic Lock 비관적 락 적용
@Repository
public interface ColumnsRepository extends JpaRepository<ColumnInfo, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({
            @QueryHint(name = "javax.persistence.lock.timeout", value = "60000")
    })
    @Query("SELECT c FROM ColumnInfo c WHERE c.id = :columnId")
    Optional<ColumnInfo> findByIdWithLock(Long columnId);
}
  • LockModeType : Spring Data JPA 의 JpaRepository 에서 제공되는 기본적인 findById 를 Lock을 부여하는 Custom Method로 바꾼 후, PESSIMISTIC_WRITE 으로 설정
  • → 비관적인 락으로 해당 메서드를 실행할 때마다 적용 가능
  • @QueryHints / @QueryHint : 위의 교착 상태(Deadlock)가 발생할 경우를 대비해 Timeout을 설정
  • @Query : 커스텀 쿼리도 findById 와 똑같이 저장

Card Service

    @Transactional
    public CardResponseDto createCard(Long columnId, CardRequestDto requestDto) {

        ColumnInfo columnInfo = columnsRepository.findByIdWithLock(columnId).orElseThrow(()
                -> new NotFoundException("해당 컬럼을 찾지 못했습니다.")
        );

        int cardSize = columnInfo.getCardList().size();
        int position = 1;

        if (cardSize != 0) {
            position = cardSize + 1;
        }

        Card card = Card.builder()
                .title(requestDto.getTitle())
                .content(requestDto.getContent())
                .deadLineDate(requestDto.getDeadLineDate())
                .worker(requestDto.getWorker())
                .columnInfo(columnInfo)
                .status(Card.Status.ACTIVE)
                .position(position)
                .build();

        cardRepository.save(card);

        return new CardResponseDto(card);
    }
  • 비관적 락 은 해당 비관적 락이 걸린 JPA 메서드가 적용된 Transcation 자체 적용이 되기 때문에 cardRepository.save(card) 에 적용하여 저장되어 commit 될때까지의 Transaction 끝날 때까지 Lock 가지고 있게됨
  • → 따라서 해당 @Transactional 인 createCard 메서드는 굳이 lock에 대한 로직이 필요없이 간단하게 적용할 수 있음

실제 다중 병렬 요청 테스트

  • 5개의 카드 생성 병렬 요청을 했을 경우 = 여러 클라이언트의 요청 = 다중 서버의 요청 (Create Card 요청이 다수 들어온다고 가정)

비관적 락이 없을 경우

  • 동시성 문제가 발생
    • 순서대로 position 값이 증가하지 않음
    • READ ↔ CREATE 간 총 카드의 수를 정확히 셀 수 없기 때문 : Transaction이 중첩해서 발생

  • 거의 동일 하게 카드 생성이 요청이 들어왔음에도, 순서가 Linear하게 보장

Redisson을 채택한 것은 당시, 이 프로젝트를 더욱 확장시켜보자는 취지로 했기 때문에,

초기 기술적 의사결정을 다중서버 다중DB 대응 에 초첨을 맞춘 것 같다.

 

Redisson은 정밀하게 동시성 제어를 조절할 수 있고,(WAIT TIME, LEASE TIME 설정 등) DB 성능에 영향을 주지 않기 때문에 프로젝트가 다루는 데이터 또는 인프라에 따라서 잘 결정을 할 수 있어야할 것이다.

+ 비관적락은 오직 한 DB의 트랜잭션에 대해서만 락 보장이므로 같은 데이터를 다루는 DB가 분리되거나 나눠질 경우에는 비동기 Redisson을 쓰는 것이 훨씬 안정적일 것