예약 시스템 - 중복 예약 처리 3 synchronized 와@Trasactional

Chan Young Jeong·2024년 3월 12일
0

프로젝트 Dplanner

목록 보기
3/5

예약 시스템 - 중복 예약 처리 2 synchronized 와@Trasactional
이어서 해당 코드는 동기화가 안돼있기 때문에 테스트를 실패한다.

이제 synchronized 를 이용해 동기화를 해보자.

	@Transactional
    public synchronized ReservationDto.Response createReservation(Long clubMemberId, ReservationDto.Create createDto) {
        Long resourceId = createDto.getResourceId();
        LocalDateTime startDateTime = createDto.getStartDateTime();
        LocalDateTime endDateTime = createDto.getEndDateTime();
        // 이미 예약이 있는지 검사
        if (reservationRepository.existsBetween(startDateTime, endDateTime, resourceId)) {
            throw new ServiceException(RESERVATION_UNAVAILABLE);
        }

          ...
            
         // 예약을 생성합니다.
        Reservation reservation = reservationRepository.save(createDto.toEntity(clubMember, resource));
        return ReservationDto.Response.of(reservation);
    }

하지만 이상하게도 테스트 대부분이 통과하는데 몇 개가 실패한다.

이유

synchronized를 사용했음에도 문제를 해결하지 못한 이유는 Spring AOP 때문이다. @Transactional을 사용하면 Spring AOP로 인해 프록시 객체가 만들어지고, 원래 객체인 reservationService의 createReservation()의 실행이 끝나고 트랜잭션이 커밋되기 전에 다른 스레드가 데이터를 읽었기 때문에 중복예약이 발생합니다.

이 때 synchronized는 메서드 시그니처(=메서드 이름 + 파라미터 타입과 개수)가 아니기 때문에, 상속되지 않습니다. 따라서 프록시 객체의 createReservation()는 여러 스레드가 사용할 수 있게 됩니다.

// Proxy class
class ReservationServiceProxy extends ReservationService{

    private ReservationService reservationService;

    @Override
    public void decrease(...) {
         try{
             tx.start();
             reservationService.decrease();
         } catch (Exception e) {
             // ...
         } finally {
             tx.commit();
         }
    }
}

// Origin Class
class ReservationService {
	
    public synchronized void createReservation( ... ) {
        // ...
    }
}

해결방법

프록시가 문제였으니 @Transactional을 쓰지 않으면 됩니다. 따라서 save도 saveAndFlush로 변경하고 그리고 더티 책킹도 되지 않기 때문에 update도 따로 해줘야합니다.

//	@Transactional
    public synchronized ReservationDto.Response createReservation(Long clubMemberId, ReservationDto.Create createDto) {
        Long resourceId = createDto.getResourceId();
        LocalDateTime startDateTime = createDto.getStartDateTime();
        LocalDateTime endDateTime = createDto.getEndDateTime();
        // 이미 예약이 있는지 검사
        if (reservationRepository.existsBetween(startDateTime, endDateTime, resourceId)) {
            throw new ServiceException(RESERVATION_UNAVAILABLE);
        }

          ...
            
         // 예약을 생성합니다.
        Reservation reservation = reservationRepository.saveAndFlush(createDto.toEntity(clubMember, resource));
        return ReservationDto.Response.of(reservation);
    }

synchronized는 근본적인 해결책이 될 수 없다.

synchronized는 한 프로세스 내에서만 동시성 제어를 할 수 있습니다. synchronized를 메서드에 사용한다면, 한 프로세스 내에서 한 번에 하나의 스레드만 해당 메서드에 접근하는 것은 보장할 수 있습니다.

따라서 운영 서버가 여러 대인 경우에는 어플리케이션 단에서 중복 예약을 막기는 어렵습니다. 다른 데이터베이스 락을 이용하거나 다른 방안을 모색해야하지만, 서비스 초기 단일 서버로 운영할 때는 임시방편으로 사용이 가능할 것 같습니다.

대안

데이터베이스 단에서 해당 문제를 해결하기 위한 Lock 혹은 트랜잭션 격리 수준을 설정할 수 있습니다. 하지만 우리 서비스에서는 기존에 디비에 있는 어떤 레코드를 변경하는 것이 아닌, 새로운 예약을 만드는 과정이기 때문에 데이터베이스 락을 이용하는 방법은 적절치 않았습니다.


출처
Transactional과-synchronized를-같이-사용할-때의-문제점

0개의 댓글