현재 주문 처리 로직에서 OrderService는 EmailService에 직접 의존하며, 주문 처리와 이메일 전송 로직이 결합되어 있다. 이러한 결합은 코드의 유지보수성과 확장성을 저해할 수 있다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final EmailService emailService;
@Transactional
public void order(Long memberId, OrderRequest request) {
//주문 생성 로직
orderRepository.save(order); //주문 저장
emailService.sendOrderConfirmation(member, order); //주문 고객에게 이메일 전송
}
}
Spring Event는 애플리케이션 컴포넌트 간의 느슨한 결합을 가지게 해주며, 컴포넌트들은 직접적인 의존 없이도 서로 통신할 수 있게 된다. ApplicationEventPublisher는 이벤트를 발행하는 역할을 하며, ApplicationListener는 특정 이벤트를 구독하고 처리한다.
Spring Event를 활용하면 주문 처리와 이메일 전송을 분리하여 결합을 느슨하게 할 수 있으며, OrderService는 주문이 완료된 후 이벤트를 발행하고, 별도의 리스너가 해당 이벤트를 수신하여 이메일을 전송하게 된다. 이를 통해 각 컴포넌트는 자신의 역할에만 집중할 수 있으며 변경 사항이 발생하더라도 다른 컴포넌트에 최소한의 영향만 주게 된다.
또한 이메일 전송과 같은 작업은 외부 시스템과의 통신을 포함하므로 지연이 발생할 수 있다. 이러한 작업을 이벤트 리스터에서 비동기적으로 처리하면, 주문 처리를 수행하는 메인 스레드의 부담을 줄일 수 있다.
Spring 4.2부터는 @EventListener만 메서드에 선언하면 간단하게 이벤트 리스너로 등록할 수 있다.
@Getter
@AllArgsConstructor
public class OrderCompletedEvent {
private final Member member;
private final Order order;
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void order(Long memberId, OrderRequest request) {
//주문 생성 로직
orderRepository.save(order); //주문 저장
eventPublisher.publishEvent(new OrderCompletedEvent(member, order)); //이벤트 발행
}
}
@Async와 함께 사용하여 비동기적으로 이벤트를 처리할 수 있다
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderCompletedEventListener {
private final EmailService emailService;
@Async
@EventListener
public void handleOrderCompletedEvent(OrderCompletedEvent event) {
Member member = event.getMember();
Order order = event.getOrder();
try {
emailService.sendOrderConfirmation(member, order);
} catch (EmailSendFailure e) {
log.error("code: {}, message: {}", e.getErrorCode().getCode(), e.getErrorCode().getMessage());
}
}
}
만약 다음과 같이 이메일 전송은 비동기로 이벤트를 처리하지만 주문 로직이 커밋되기 전에 예외가 터진다면 어떻게 될까?
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void order(Long memberId, OrderRequest request) {
//주문 생성 로직
orderRepository.save(order); //주문 저장
eventPublisher.publishEvent(new OrderCompletedEvent(member, order)); //이벤트 발행
throw new RuntimeException("order failed");
}
}
예외가 발생되면서 트랜잭션이 커밋되지 않지만 비동기로 처리된 이메일 전송은 완료되는 것을 볼 수 있다.

이때 @EventListener 대신에 @TransactionalEventListener을 사용하면 해결할 수 있다. @TransactionalEventListener는 phase 속성을 통해 이벤트를 처리할 트랜잭션의 시점을 지정할 수 있다.
기존 이벤트 처리 메서드에 다음과 같이 추가하면 트랜잭션이 성공적으로 커밋됐을 때 이벤트가 처리되게 된다.
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderCompletedEventListener {
private final EmailService emailService;
@Async
@TransactionalEventListener
public void handleOrderCompletedEvent(OrderCompletedEvent event) {
Member member = event.getMember();
Order order = event.getOrder();
try {
emailService.sendOrderConfirmation(member, order);
} catch (EmailSendFailure e) {
log.error("code: {}, message: {}", e.getErrorCode().getCode(), e.getErrorCode().getMessage());
}
}
}
@TransactionalEventListener은 다음과 같이 여러 옵션을 적용할 수 있다.
AFTER_COMMIT: 기본값으로 트랜잭션이 커밋된 후에 이벤트를 처리한다.BEFORE_COMMIT: 트랜잭션이 커밋되기 전에 이벤트를 처리한다.AFTER_ROLLBACK: 트랜잭션이 롤백된 후에 이벤트를 처리한다.AFTER_COMPLETION: 트랜잭션이 완료된 후(커밋 또는 롤백) 이벤트를 처리한다.