하지만 "그럼 무조건 트랜잭션을 적용해야할까?" 는 아니다.
예를 들어, 외부 API 를 호출할 경우(예를 들어 소셜 로그인) 를 생각해보자.
만약, 서비스 도중 외부 API 의 장애가 생겨 호출하는데 약 5초 정도의 시간이 걸렸다.
그때 한 트랜잭션에 묶여 있는 다른 작업들은 이 작업이 끝날 때 까지 기다리거나 실패할경우 실행하지 못하는 문제가 발생한다 (기다리는 유저들은 어떻게 할거야 안기다리고 떠나겠지😨)
따라서 트랜잭션의 범위에서 외부 API 호출을 하는 로직은 최대한 분리해야 한다.
나의 예약 로직도 마찬가지이다. 현재 나의 예약 로직을 보면 예약기능(rvConcertToUser) 메서드 전체가 하나의 트랜잭션으로 묶여있다. 예전에는 뭐 상관없지 않을까했는데 문제가 생길 수 있다는 것을 알고도 아무것도 안하면 안되겠지..
@Slf4j
@Service
@RequiredArgsConstructor
public class ReservationService {
private final QueueRepository queueRepository;
private final JpaConcertRepository concertRepository;
private final JpaSeatRepository seatRepository;
private final JpaReservationRepository reservationRepository;
private final ConcertEventRepository concertEventRepository;
private final JpaUserRepository userRepository;
private final RedisTemplate<String,Object> redisTemplate;
@Transactional
@Caching(evict = {
@CacheEvict(value = "availableConcertDates", key = "#token"),
@CacheEvict(value = "availableConcertSeats", key = "#token + '_' + #requestDto.eventId")
})
public ReservationResponseDto rvConcertToUser(Long concertId, String token, ReservationRequestDto requestDto) {
// 대기열 토큰 검증 먼저 수행
User user = validateToken(token);
Concert concert = concertRepository.findById(concertId)
.orElseThrow(() -> new IllegalArgumentException("해당하는 콘서트가 없습니다."));
Seat seat = seatRepository.findSeatForUpdate(
requestDto.getSeatNumber(), requestDto.getEventId())
.orElseThrow(() -> new IllegalArgumentException("해당 좌석은 선택할 수 없습니다."));
// 예약이 불가능한 경우 로직 //
if(!seat.isAvailable()) {
log.error("이미 예약한 유저 존재, 좌석에 접근한 유저 = {} ", user.getName());
log.error("이미 예약이 되어 있는 좌석 = {}", seat.getSeatNumber());
throw new IllegalStateException("이미 예약된 좌석입니다. 다른 좌석을 선택해주세요.");
}
// 예약 가능 : 예약 상태를 true -> false 변경
seat.setUnAvailable();
Reservation reservation = Reservation.builder()
.name(user.getName() + " 의 예약입니다.")
.reservationDate(LocalDateTime.now())
.user(user)
.seat(seat)
.concert(concert)
.status(ReservationStatus.ONGOING)
.build();
reservationRepository.save(reservation);
redisTemplate.opsForValue().set("reservation_token:"+ token, String.valueOf(reservation.getId()));
return new ReservationResponseDto(
requestDto.getConcertName(),
requestDto.getSeatNumber());
}
// 그외 메서드들 //
}
여기서 Redis 연산은 트랜잭션의 일부가 아니다. (왜? )
따라서 DB 트랜잭션과 별개로 처리가 된다.또한 Redis 는 외부 시스템!
만약
redisTemplate.opsForValue().set("reservation_token:"+ token, String.valueOf(reservation.getId()));
에서 오류가 발생한다면 예약기능(rvConcertToUser) 로직은
reservationRepository.save(reservation);
을 롤백해야한다.
사용자 관리
Redis 와 상호작용(토큰관리)
콘서트 관리
등등
예약 관리
대기열 관리
Redis 를 통한 토큰을 관리
일관성 유지의 어려움
여러 서비스 간의 데이터 일관성을 유지하기 어렵다.
단일 트랜잭션 내에서 여러 데이터베이스 연산을 원자적으로 처리하기 어렵다.
복잡한 트랜잭션 관리
전역 트랜잭션 관리가 복잡하고, 성능 저하의 원인이 될 수 있다.
장애 복구의 어려움
한 서비스에서 트랜잭션 실패 시, 다른 서비스의 트랜잭션을 롤백하는 것이 어렵다.