⇒ 이벤트를 사용해야 하는 경우는 특정한 도메인의 상태 변경을 외부로 알려줘야 하는 경우.
예를 들어 주문이 완료되었으면 Order 도메인에서 다른 도메인으로 변경에 따른 처리를 해야 하는 상황이 필요.
@Transactional
public ReservationResponseDto rvConcertToUser(...) {
// 유저 확인()
// 콘서트 확인()
// 좌석 확인()
// 좌석 상태 변경()
// redis 로직 실행 - 토큰을 redis 에 저장 ..
..
}
- 위 로직을 보면 이 모든 로직은 한 트랜잭션에 묶여 있다. 이말은 즉 하나의 메서드만 실패하더라도 모든 로직은 트랜잭션의 특성으로 인해 rollback 되게 된다.
redis 가 만약에 고장이 나면 예약 로직은 실행되지 못한다. 상황에 따라 가능한 시나리오일 수 있다. 하지만 redis 가 고장이 나도 서비스에서 예약을 정상적으로 성공시켜야한다면?
- *redis 에 토큰을 저장하는 로직*이 어떤 이유에 의해 오래 걸릴 경우 전체 트랜잭션에 영향을 끼침
- *redis 에 토큰을 저장하는 로직*이 실패할 경우 예약 처리 전체가 실패하게 됨
- 결국 콘서트를 예약하는(save) 로직은 동기적으로 처리를 해주고, redis 작업은 비동기적으로 처리를 해준다면 메인스레드는 예약저장 응답만 기다리면 되기 때문에, 실제 처리시간이 더 빨라질 것 이다.
- 동기 : 서버에 요청을 보냈을 때, 응답이 돌아오면 다음 동작을 수행 ( 즉, 순차적으로 응답을 처리 / 1 요청 처리 → 2요청 처리 → 3 요청 처리 → ,,,, )
- 코드가 직관적이고 간단
- 순서를 보장받을 수 있기 때문에 명확한 데이터를 확인 가능
- 비동기 : 동기 방식과는 반대로 요청을 보내고 응답과는 상관없이 다음 동작을 수행 ( 2 요청처리 → 1 요청 처리 → 3요청처리 → … )
- 시간이 걸리는 동안 다른 작업을 진행할 수 있다.
- 동기 방식보다 구현이 복잡
- 비동기 Config.java 설정
@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigururSupport { }
@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()
));
...
}
💡 AFTER_COMMIT 은 트랜잭션이 성공적으로 커밋된 이후 실행이 되는 것을 보장한다. 이는 트랜잭션 내에서 발생한 변경사항이 데이터베이스에 반영된 이후 이벤트가 처리됨을 의미한다. 기본적으로 트랜잭션이벤트리스너는 이벤트를 동기적으로 실행한다. 즉 이벤트 리스너가 완료될 때 까지 메인스레드는 블록된다.
💡 @Async(비동기) : 메서드를 별로의 스레드에서 비동기적으로 실행하도록 설정한다. 이를 통해 메인 스레드가 해당 작업을 기다리지 않고 즉시 다음 작업을 계속할 수 있도록 보장한다.
시간이 많이 드는 작업(외부 API 호출 등) 을 비동기적으로 처리하여 메인 스레드의 응답성을 높여주고, 애플리케이션의 전반적인 성능을 높인다.
💡 @TransationalEventlistener 와 @Async 의 관계
- @TransationalEventlistener 단독으로만 사용할 경우
- 트랜잭션이 성공적으로 커밋된 후에만 이벤트가 처리되므로 데이터 일관성이 유지된다.
- 하지만 이벤트 리스너가 완료될 때 까지 메인스레드가 블록된다. 시간이 많이 소요되는 작업이 있을 경우, 메인 트랜잭션의 응답시간이 지연될 수 있다.- @TransationalEventlistener 과 @Async 을 같이 사용할 경우
- 여전히 트랜잭션이 커밋된 이후 이벤트가 실행된다. 하지만 이벤트 리스너가 별도의 스레드에서 실행되므로, 메인 트랜잭션의 응답 시간이 단축된다.
- 시간이 많이 소요되는 작업을 메인스레드와 분리하여 처리함으로써 시스템의 전반적인 성능과 응답성을 향상시킨다.
💡
ApplicationEventPublisher
를 통해 이벤트를 발행하면, 해당 이벤트를 처리하도록 등록된 이벤트 리스너들이 호출된다.
호출을 하게 되면 그 이벤트는 스프링의 애플리케이션 컨텍스트에 게시되게 된다.
@TransactionalEventListener
로 어노테이션된 메서드는 이벤트 리스너로 등록된다. Spring은 애플리케이션 컨텍스트를 초기화할 때 이 메서드를 이벤트 리스너로 인식하고 등록한다. 이벤트가 발핸되면 스프링은 해당 이벤트 타입을 처리하도록 등록된 모든 리스너들을 찾는다.
그렇다면 @TransactionalEventListener 는 어느 순서대로 실행이 되는것일까?
: @TransactionalEventListener
로 어노테이션된 메서드는 Spring 프레임워크에서 이벤트를 처리하기 위해 사용되며, 트랜잭션 상태에 따라 이벤트를 리스한다. 여러 개의 @TransactionalEventListener
가 있을 때, 그 실행 순서는 기본적으로 “비결정적”
즉, 순서를 보장하지 않는다. 실행 순서를 지정하고 싶다면 @Order(n) 어노테이션을 지정하여 순서를 지정해 줄 수 있다.
💡
consume
메서드는@KafkaListener
로 어노테이션되어 있어 Kafka 메시지 리스너로 등록된다.
topics
와groupId
를 지정하여 어떤 토픽의 메시지를 수신할지 설정한다.
스프링 카프카는 애플리케이션 컨텍스트를 초기화할 때,@KafkaListener
가 적용된 메서드를 감지하고, 리스너 컨테이너를 생성한다.
리스너 컨테이너는 백그라운드에서 실행되며, 지정된 토픽에 대해 카프카 브로커와 연결을 유지한다.
카프카 브로커에서 메시지가 해당 토픽으로 발행되면, 리스너 컨테이너는 이를 감지하고 메시지를 가져온다. 가져온 메시지는consume
메서드에 전달되어 호출한다.