트랜잭션 내에서 핵심 비즈니스 로직을 처리하고, 트랜잭션이 성공적으로 커밋된 후에 별도의 비동기 작업을 수행하여 시스템의 관심사를 분리하는 이벤트 기반 아키텍쳐를 만들어보자.
@EventListener
@TransactionEventListener
- 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()
));
...
}