[콘서트 예약 시스템] - 서비스의 규모가 확장된다면 서비스들을 어떻게 분리할래?

Kyungmin·2024년 11월 14일
0

Spring

목록 보기
34/39

🤔 내가 개발한 기능의 트랜잭션 범위에 대해 이해

  • 스프링 프레임워크에서는 @Transactional 사용하면 아주 편하게 트랜잭션을 활용할 수 있다.
    트랜잭션 범위안에 있는 모든 DB 요청들은 하나의 커넥션을 공유하기 때문에 성능도 높일 수 있고, 한 메서드안에서 동작하는 다른 메서드들이 하나만 성공하고 하나만 실패하는 데이터 정합성의문제도 해결할 수 있다.

하지만 "그럼 무조건 트랜잭션을 적용해야할까?" 는 아니다.
예를 들어, 외부 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());
    }

    // 그외 메서드들 // 
}

현재 나의 예약기능(rvConcertToUser) 에서의 트랜잭션 처리 내역은 다음과 같다.

  1. 토큰 검증: Redis를 통해 토큰 유효성 검증.
  2. Concert 조회: concertRepository를 통해 콘서트 조회.
  3. Seat 조회 및 잠금: seatRepository.findSeatForUpdate()를 통해 좌석 조회 및 잠금.
  4. Seat 상태 변경: 좌석의 가용 상태 업데이트.
  5. Reservation 생성 및 저장: reservationRepository.save()를 통해 예약 저장.
  6. Redis에 Reservation Token 저장: RedisTemplate을 통해 예약 토큰 저장.

여기서 Redis 연산은 트랜잭션의 일부가 아니다. (왜? )
따라서 DB 트랜잭션과 별개로 처리가 된다.또한 Redis 는 외부 시스템!

만약

redisTemplate.opsForValue().set("reservation_token:"+ token, String.valueOf(reservation.getId()));

에서 오류가 발생한다면 예약기능(rvConcertToUser) 로직은

reservationRepository.save(reservation);

을 롤백해야한다.

🤔 서비스의 규모가 확장된다면 서비스들을 어떻게 분리할 수 있을까?

  • 서비스 규모가 확장됨에 따라 단일 서비스가 너무 커지거나 복잡해질 수 있다. 이를 방지하고 유연성을 높이기 위해 마이크로서비스 아키텍처(MicroServices Architecture)를 도입할 수 있다. 주요 분리 전략은 뭐가 있을까?
  1. 도메인 기반 분리
  • userService

    사용자 관리
    Redis 와 상호작용(토큰관리)

  • ConcertService

    콘서트 관리
    등등

  • ReservationService

    예약 관리

  • QueueService

    대기열 관리
    Redis 를 통한 토큰을 관리

🤔 분리에 따른 트랜잭션 처리의 한계와 해결방안

트랜잭션의 한계

  • 마이크로서비스 아키텍처에서는 각 서비스가 독립적인 데이터베이스를 가지므로 분산 트랜잭션(Distributed Transaction)을 구현하기 어렵다.
  1. 일관성 유지의 어려움

    여러 서비스 간의 데이터 일관성을 유지하기 어렵다.
    단일 트랜잭션 내에서 여러 데이터베이스 연산을 원자적으로 처리하기 어렵다.

  2. 복잡한 트랜잭션 관리

    전역 트랜잭션 관리가 복잡하고, 성능 저하의 원인이 될 수 있다.

  3. 장애 복구의 어려움

    한 서비스에서 트랜잭션 실패 시, 다른 서비스의 트랜잭션을 롤백하는 것이 어렵다.

해결 방안: Saga 패턴 도입


참고

profile
Backend Developer

0개의 댓글

관련 채용 정보