[콘서트 예약 시스템] - 트랜잭션과 관심사 분리(이벤트 기반 아키텍처)

Kyungmin·2024년 11월 17일
0

Spring

목록 보기
35/39

트랜잭션 내에서 핵심 비즈니스 로직을 처리하고, 트랜잭션이 성공적으로 커밋된 후에 별도의 비동기 작업을 수행하여 시스템의 관심사를 분리하는 이벤트 기반 아키텍쳐를 만들어보자.

✏️ 이벤트 기반 아키텍처

  • 이벤트(Event) : 시스템 내에서 발생하는 중요한 상태 변화나 활동을 나타내는 메시지
  • 이벤트 발행자(Publisher) : 이벤트를 생성하고 발행하는 컴포넌트
  • 이벤트 리스너(Listener) : 특정 이벤트를 구독하고, 이벤트 발생 시 이를 처리하는 컴포넌트
  • 트랜잭션 : 데이터베이스의 일관성과 무결성을 보장하기 위한 논리적 작업 단위

✏️ 이벤트 기반 아키텍처 장점

  • 관심사의 분리 : 핵심 비즈니스 로직과 부가적인 작업을 분리하여 코드의 가독성과 유지보수성을 향상
  • 비동기 처리 : 시간이 오래 걸리는 작업을 비동기적으로 처리하여 메인 트랜잭션의 성능에 영향을 주지 않는다.
  • 확장성 : 새로운 이벤트 리스너를 추가하여 기능을 쉽게 확장할 수 있다.

✏️ @EventListener & ✏️ @TransactionEventListener

  • @EventListener

    • 용도 : 일반적인 이벤트 리스너로, 트랜잭션과 무관하게 이벤트를 처리
    • 실행 시점 : 이벤트가 발행되면 즉시 실행
  • @TransactionEventListener

    • 용도 : 트랜잭션과 연계된 이벤트 리스너로, 트랜잭션의 상태에 따라 이벤트를 처리할 수 있다.
    • 실행 시점 : AFTER_COMMIT ,AFTER_ROLLBACK, AFTER_COMPLETION 등 다양한 시점에서 이벤트를 처리할 수 있다.
    • 특징 : 트랜잭션이 성공적으로 커밋된 후에만 처리되도록 보장한다.
    • AFTER_COMMIT
      • 트랜잭션 결과를 반영(commit) 하고 난 시점에 이벤트가 실행. 기본값
    • AFTER_ROLLBACK
      - 트랜잭션에 rollback 이 일어난 시점에 이벤트가 실행
    • AFTER_COMPLETION
      - 트랜잭션이 끝났을 때(Commit or Rollback) 이벤트가 실행
    • BEFORE_COMMIT
      - 트랜잭션이 커밋되기 전에 이벤트가 실행

✏️ 스프링 이벤트란 ?

이벤트란 ?

  • 시스템 내에서 발생하는 중요한 상태 변화나 활동을 나타내는 메시지이다. 예약을 완료했을 때, 좌석 상태가 변경되었을 때 등이 이벤트가 될 수 있겠다.

스프링의 이벤트 시스템

  • 스프링 프레임워크는 애플리케이션 내에서 이벤트를 발생하고 , 이를 구독하는 매커니즘을 제공한다. 이를통해 느슨하게 결합된 컴포넌트 간의 통신을 용이하게 할 수 있다.

[이벤트 사용 장점, 단점 및 사용 예시]

  • 장점
    • 의존성을 분리하여 두 클래스를 느슨하게 결합할 수 있다. ( 의존성을 분리한다. )
      • 클래스가 독립적이므로 재사용성을 높인다.
      • 추후 별도의 서비스로 분리하기 용이
      • 메시지 구독 모듈을 추가 또는 삭제할 경우, 다른 모듈에 영향을 주지 않은 채로 수정이 가능
      • 단위 테스트 용이
  • 단점
    • 전반적인 작업량이 많아질 수 있다. (이벤트 클래스, 커스텀 어노테이션 등)
      • 코르 흐름 따라가기 어려울 가능성
      • 메시지 구독 순서를 고려해야 하는 경우 복잡
      • 전체적인 이벤트의 구독 및 발행 과정을 테스트하기 어려움
      • 특정 프레임워크 API 에 의존하게 됨

⇒ 이벤트를 사용해야 하는 경우는 특정한 도메인의 상태 변경을 외부로 알려줘야 하는 경우.

예를 들어 주문이 완료되었으면 Order 도메인에서 다른 도메인으로 변경에 따른 처리를 해야 하는 상황이 필요.

💡 기존 예약 로직

    @Transactional
    public ReservationResponseDto rvConcertToUser(...) {

		// 유저 확인() 
		// 콘서트 확인() 
		// 좌석 확인()

		// 좌석 상태 변경()
		// 예약.save()

		// redis 로직 실행 - 토큰을 redis 에 저장 .. 
		... 
	}
  • 위 로직을 보면 이 모든 로직은 한 트랜잭션에 묶여 있다. 이말은 즉 하나의 메서드만 실패하더라도 모든 로직은 트랜잭션의 특성으로 인해 rollback 되게 된다.

  • redis 가 만약에 고장이 나면 예약 로직은 실행되지 못한다. 상황에 따라 가능한 시나리오일 수 있다. 하지만 redis 가 고장이 나도 서비스에서 예약을 정상적으로 성공시켜야한다면?

  • redis 에 토큰을 저장하는 로직이 어떤 이유에 의해 오래 걸릴 경우 전체 트랜잭션에 영향을 끼침

  • redis 에 토큰을 저장하는 로직이 실패할 경우 예약 처리 전체가 실패하게 됨

💡 트랜잭션을 분리하고 이벤트 리스너를 적용

    @Transactional
    public ReservationResponseDto rvConcertToUser(...) {

		// 유저 확인() 
		// 콘서트 확인() 
		// 좌석 확인()
				
		// 예약이 불가능한 경우 로직()
				
		// 좌석 예약 가능 여부 변경()
		// 예약.save()
				
		// redis 이벤트 발생() 
		... 
	}
// redis 이벤트 발생 // 

@Component
@RequiredArgsConstructor
@Slf4j
public class ReservationEventListener {
    private final RedisTemplate<String, String> redisTemplate;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleReservationCompleted(ReservationCompletedEvent event) {
        log.info("예약 완료 후 예약 이벤트 처리를 시작. ReservationId: {}", event.reservationId());

        try {
            redisTemplate.opsForValue().set("reservation_token:" + event.token(),
                    String.valueOf(event.reservationId()));
        } catch (Exception e) {
            log.error("예약 이벤트 처리 중 오류 발생", e.getMessage());
        }
    }
}

즉 다음과 같은 로직을

@Transactional
    public ReservationResponseDto rvConcertToUser(Long concertId, String token, ReservationRequestDto requestDto) {

				...
				// 예약.save()
        reservationRepository.save(reservation);
				
        redisTemplate.opsForValue().set("reservation_token:"+ token, String.valueOf(reservation.getId()));

        ...
    }

이와같이 변경할 수 있다.

@Transactional
    public ReservationResponseDto rvConcertToUser(Long concertId, String token, ReservationRequestDto requestDto) {
        ...
        // 예약.save()
        reservationRepository.save(reservation);

        // 이벤트 발행 (만약 실패해도 에약은 정상적으로 save() )
        eventPublisher.publishEvent(new ReservationCompletedEvent(
                reservation.getId(),
                token,
                user.getId()
            ));
			...
    }

참고

profile
Backend Developer

0개의 댓글

관련 채용 정보