매치 예약 시스템의 동시성 제어 및 중복요청을 막는것에 대해서

kmb·2023년 9월 9일
0

DB

목록 보기
10/10
post-thumbnail

동시성

하나의 CPU 코어에서 시간분할(Time sharing)을 통하여 여러개의 일을 처리하는것처럼 보여지게 하는 기법을 의미한다.
또는 여러 요청이 동시에 동일한 자원에 접근하고 변경하여 기대값이 다르게 나오는것을 의미한다.

만약 영화 예매를 했는데 내가 예약했던 자리에 다른 사람도 똑같이 예매를 했다면 어떤 생각이 들까? 참으로 황당할 것이다.

아래 코드는 개인 프로젝트내의 클라이밍장 예약을 하는 기능에서 예약(Reservation) 엔티티를 생성(create)하는 작업단위(Transaction)를 실행할 때 동시성 이슈가 발생할 수 있다고 생각한 것이다.

 

클라이밍장 예약 동시성 문제

특정 클라이밍장의 정보가 포함된 match가 있는지 조회하고, reservation을 등록하는 로직이다.

@Transactional
@Override
public ReservationResponse createReservation(Long matchId, String username) {

    Match match = matchRepository.findByIdForCreate(matchId)
            .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_MATCH));

    Member member = memberRepository.findByUsername(username)
            .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_MEMBER));
        
    validateApplyMatch(match, member);

    Reservation reservation = Reservation.builder()
            .match(match)
            .member(member)
            .build();

    Reservation saveReservation = reservationRepository.save(reservation);

    match.increaseApplicantNum();
    match.updateStatus();

    return ReservationResponse.mapToDto(saveReservation);
}

만약 특정 매치에 예약 가능한 자리가 1개 남은 상태에서 동시에 여러 작업단위(예약생성)가 실행된다면?
같은 매칭시간대의 같은 클라이밍장을 예약한 필드정보가 중복되어 최대 예약 가능한 인원을 뛰어넘어 DB의 정합성 문제가 발생한다.

결국 데이터가 저장되고 이후에 조회했을때, 의도하지않은 다른값이 반환되어 데이터의 무결성이 깨지게 된다.

따라서 특정 엔티티에 대한 동시 접근을 막기 위해 MySQL에서 제공하는 Lock 기능들의 사용을 고려했다.

 

Lock의 방식 2가지

비관적 락(Pessimistic Lock)낙관적 락(Optimistic Lock)이 있는데 자세한 설명은
해당 링크에 정리했다.

프로젝트에는 비관적 락(Pessimistic Lock)을 적용했는데
MySQL InnoDB의 경우 ~FOR UPDATE 를 포함한 쿼리로 락 기능을 사용한다. 예시는 아래와 같다.

SELECT * FROM match m where m.match_id = 1 FOR UPDATE;

비관적 락의 경우 조회한 인덱스의 레코드 자체에 락을 걸기 때문에 성능이 저하될 수 있다.
성능상의 이슈가 있다면 낙관적 락을 고려해야한다.

JPA에서 비관적 락을 구현하는 LockModeType의 종류는 다음과 같다.

  • PESSIMISTIC_READ : 다른 트랜잭션에서 읽기만 가능한 것 (공유잠금)
  • PESSIMISTIC_WRITE : 다른 트랜잭션에서 읽기 및 쓰기를 못하는 것 (배타적 잠금)
  • PESSIMISTIC_FORCE_INCREMENT : 다른 트랜잭션에서 읽기 및 쓰기를 못하는것 + Version 정보를 사용

 
실제로 적용한 코드는 다음과 같다.

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select m from Match m where m.id = :id")
Optional<Match> findByIdForCreate(@Param("id") Long matchId);

 

출처

profile
꾸준하게

0개의 댓글