본문 바로가기

CS/DB

Synchronized, Transactional Isolation, Pessimistic Lock 정리

  • 프로젝트에서 동시성 이슈를 다루면서 Transactional Serializable이 안되는지 이해가 되지 않았다.
  • isolation level이 가장 높기 때문에 당연히 동시성 이슈가 해결될 것이라 생각했다.
  • 하지만 비관적락에서만 동시성 이슈가 발생하지 않았다.
  • 그래서 구체적인 동작방식을 이해하고자 정리했다.

프로젝트 동시성 이슈 발생

  • 상황
    • 예약 프로젝트이 특성 사 예약 요청 트래픽이 몰리는 상황을 테스트
    • 100개의 스레드로 동시에 예약 요청하여 데이터를 생성
int threadCount = 100;
ExecutorService executorService = Executors.newFixedThreadPool(32);

List<NewReservationRequest> list = new ArrayList<>();
for (int i = 1; i <= threadCount; i++) {
  ReservationPriceDto price = ReservationPriceDto.builder()
          .productSeatScheduleId(1L)
          .price(10)
          .quantity(1)
          .seatType(SeatType.S.name())
          .placeId(31)
          .build();
  List<ReservationPriceDto> list1 = new ArrayList<>();
  list1.add(price);

  NewReservationRequest reservationRequest = NewReservationRequest.builder()
          .memberId(1L)
          .productId(1L)
          .reservationPriceDtos(list1)
          .build();

  list.add(reservationRequest);
}

CountDownLatch latch = new CountDownLatch(threadCount);

// when
for (int i = 1; i <= threadCount; i++) {
  executorService.submit(() -> {
      try {
          NewReservationRequest req = list.remove(0);
          reservationService.createReservation(req);
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          latch.countDown();
      }
  });
}
latch.await();

// then
ProductSeatSchedule ret = productSeatScheduleRepository.findById(1L).get();
Assertions.assertThat(ret.getReservedQuantity()).isEqualTo(100);

Synchronized, Transactional Isolation, Pessimistic Lock 비교

  • 데이터의 무결성을 보장하고 동시성 이슈를 해결하기 위한 방법
  • 하지만 해결 방식이 다르다.

1. Synchronized

  • 특징
    • JVM에서 실행되는 코드의 동시성을 제어
    • JVM에서 제공하는 스레드 동기화 메커니즘 (Montor Lock)
      • 여러 스레드가 동일한 객체나 코드 블록에 동시에 접근하지 못하도록 함
    • 메모리 내의 데이터 일관성을 보장
    • 너무 많은 스레드가 하나의 모니터 락을 사용하려고 하면, 스레드 간 대기 시간이 길어져 성능이 저하될 수 있습니다.
  • 적합 상황
    • JVM 내부의 멀티스레드 환경
    • 객체/코드 블럭 대상 공유 자원(메모리 데이터)의 동시 접근 방지

2. Transactional Serializable

  • 동작 방식
    • 범위 잠금(Range Lock)과 공유 잠금(Shared Lock)을 사용하여 데이터를 직렬화된 순서로 처리
    • 읽기 작업조차 잠금을 설정하거나 범위 잠금을 걸기 때문에, 불필요한 잠금 대기가 발생 가능
    • 트랜잭션 범위에 따라 자동 설정 (범위 잠금)
    • (Pessimistic Lock 보다도) 과도한 잠금으로 동시성 처리량 감소
  • 적합 상황
    • 트랜잭션의 데이터 일관성 요구가 극도로 높은 경우.
  • 테스트결과 DeadLock 발생
    • 원인은 정규화로 나눈 여러 테이블에 대해 SELECT, UPDATE, CREATE 수행으로 여러 범위에 대해 락을 걸기 때문에 데드락 발생 가능성이 커진 것으로 추측

3. Pessimistic Lock

  • 특징
    • 개별 행(row) 또는 필요한 리소스에만 잠금
    • 트랜잭션이 명시적으로 필요한 데이터만 잠그기 때문에, 과도한 잠금 범위를 방지하여 교착 상태 가능성을 줄임
    • 필요한 데이터만 잠그므로 다른 트랜잭션이 불필요하게 대기하지 않음
    • Pessimistic Lock은 개발자가 필요한 데이터만 잠그기 때문에 잠금 동작을 더 예측 가능하게 제어가능
  • 적합 상황
    • 특정 데이터에 동시 접근이 많은 경우
  • 예약 좌석 현황에 대해 비관적 락을 걸어 여러 트랜잭션의 Phantom Read 현상을 방지하여 예약을 못하는 상황을 방지
/*
 * 예매 시 동시성 이슈 방지
 */
@Override
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<ProductSeatSchedule> findById(Long id);
...
  • 테스트 통과