조급하면 모래성이 될뿐

Service에서 DataIntegrityViolationException을 Catch 못함 본문

TroubleShooting/데브코스

Service에서 DataIntegrityViolationException을 Catch 못함

Pawer0223 2022. 7. 14. 02:40

문제 상황


  • 동시에 같은 좌석이 예약이 되는 경우 적절한 예외를 발생시키고 싶었다.
  • 현재 프로젝트에서 동시에 같은 좌석이 예약되는 경우 DataIntegrityViolationException이 발생한다
    • id의 조합으로 String타입 id를 생성한다.
    • 따라서 동일한 id조합일 때 insert 할 때 예외가 발생한다.
  • 해당 예외가 발생했을 때 예외 포인트를 정확히 기록하고자 try-catch로 감싸서 IllegalArgumentException을 발생시켰다.
  • 하지만 예외가 잡히지 않았다...

 

테스트 코드


@Test
@DisplayName("예약 실패 - IllegalException")
void testReservationIllegalException() {
    // given
    User user = saveUser();
    Schedule schedule = saveSchedule();

    int totalSeatCount = 1;
    TheaterRoom theaterRoom = saveSeatMulti(totalSeatCount);
    List<Seat> seats = seatRepository.findByTheaterRoomId(theaterRoom.getId());
    List<Long> seatIdList = seats.stream().map(Seat::getId).toList();

    Integer userMoney = totalSeatCount * PAY_AMOUNT * 3;
    Account account = new Account(user, userMoney);
    accountRepository.save(account);
    RequestReservation request = new RequestReservation(user.getId(), schedule.getId(), seatIdList);
    reservationService.reservation(request);

    // when
    assertThatThrownBy(() -> reservationService.reservation(request))
        // then
        .isInstanceOf(IllegalArgumentException.class);
}

 

AS-IS


@Service
@Transactional(readOnly = true)
public class ReservationService {
	private static final int PAY_AMOUNT = 10000; 

	@Transactional
	public Long reservation(RequestReservation requestReservation) {
		User findUser = userService.findById(requestReservation.userId());
		// 결제
		List<Long> seats = requestReservation.selectedSeatIds();
		int payAmount = PAY_AMOUNT * seats.size();
		payService.pay(findUser, payAmount);

		//티켓 저장
		Schedule findSchedule = scheduleService.findById(requestReservation.scheduleId());
		Ticket savedTicket = ticketService.createTicket(findUser, findSchedule, payAmount);

		// 좌석 예약 처리
		List<Seat> findSeats = seatService.findSeatsByIdIn(seats);
		List<ReservedSeat> selectedSeats = findSeats.stream()
			.map(seat -> new ReservedSeat(savedTicket, seat))
			.collect(Collectors.toList());

		try {
			reservedSeatService.saveAll(selectedSeats);
		} catch (DataIntegrityViolationException e) {
			throw new IllegalArgumentException(ALREADY_RESERVED_SEAT_EXP_MSG.getMessage());
		}

		return savedTicket.getId();
	}
    
    ...
    
}

  • try-catch에 디버깅을 찍고 잡아도 걸리질 않았다.
    • 모든 예외를 잡도록 Exception으로 Catch를 추가해도 안 잡혔다..

 

뭐가 문제?


  • 디버깅에서 Service에서 예외가 잡히지 않았다.
  • 즉, Service에서 예외가 발생하지 않는다.

이유는 DataIntegrityViolationException 공식 문서를 보면 어느 정도 이해가 된다.

데이터를 삽입하거나 업데이트하려는 시도가 무결성 제약 조건을 위반할 때 throw 되는 예외입니다. 이것은 순전히 관계형 개념이 아닙니다. 고유 기본 키는 대부분의 데이터베이스 유형에 필요합니다.
  • Service함수 자체에서는 해당 Entity를 영속화하고, flush가 수행되지 않는다.
  • 따라서 트랜잭션이 끝나기 직전에 해당 예외가 발생하게 된다.
  • 즉, 내가 잡은 try부분인 saveAll을 하는 시점에는 예외가 발생하지 않고 쿼리가 날아갈 때 발생한다.

 

해결방법


  • try 하는 부분에서 쿼리가 날아가도록 하면 된다.
  • JpaRepository에서 제공하는 saveAllAndFlush를 적용해주면 된다.
  • 아니면.. Service를 사용하는 외부.. 즉, 컨트롤러 같은 데서 잡을 수도 있을 거다.
    • 시도는 안 해봄..

 

 

TO-BE


  • service에서 JpaRepository의 saveAll을 호출하던 것을 saveAllAndFlush로 변경해주면 끝이다.
    • 트랜잭션이 끝나기 전에 쿼리를 수행함으로써 중복으로 저장되는 예약좌석을 감지할 수 있다.
@Service
@Transactional(readOnly = true)
public class ReservedSeatService {
	...
    
    @Transactional
    public void saveAllAndFlush(List<ReservedSeat> selectedSeats) {
        reservedSeatRepository.saveAllAndFlush(selectedSeats);
    }
    
    ...
}
반응형