[Spring] Spring Events 사용해 이벤트 발행하기

2025. 9. 7. 16:47·Spring

Spring Events를 사용해야 하는 이유 

주문을 완료하면 영수증 메일 발송, 포인트 적립, 내부/외부 시스템 알림 같은 후처리가 뒤따른다고 가정하자. 이를 한 서비스 메서드에 몰아넣으면 문제가 생긴다. 

@Service
@RequiredArgsConstructor
public class OrderService {

    private final MailService mailService;
    private final PointService pointService;
    private final WebhookService webhookService;

    @Transactional
    public void placeOrder(PlaceOrderCommand cmd) {
        // 1) 주문 생성 & 결제 확정
        Order order = createAndPay(cmd);
        System.out.println("주문 완료: " + order.getId());

        // 2) 영수증 메일
        mailService.sendReceipt(order);

        // 3) 포인트 적립
        pointService.earn(order);

        // 4) 내부 시스템 알림
        webhookService.notifyOrderPlaced(order);
    }
}

위와 같이 작성할 수는 있지만, 코드에는 몇 가지 문제점이 있다. 

 

1) 강한 결합 : 핵심 로직과 후처리가 한 메서드에 엉켜 있다.

2) 성능 : 만약 메일 서버가 지연되거나 외부 웹훅 응답 지연이 있으면 주문 완료 응답이 함게 느려진다. 

3) 트랜잭션 : 만약 포인트 적립을 하다가 오류가 나면 어떻게 될까 ? 한 트랜잭션으로 묶어있기 때문에 이전에 처리한 로직들까지 전부 롤백이 된다.

 

이럴 경우에 이벤트를 사용하면 '주문 완료' 라는 사실만 발행하고, 나머지 후처리는 독립된 리스너들이 알아서 처리할 수 있게 만들 수 있다. 

 

이벤트의 실행 단계 (Spring Events 흐름) 

  1. 이벤트 발행 (Publish) : 서비스나 도메인 객체에서 상태 변화가 일어나면 이벤트 객체를 만들고, `ApplicationEventPublisher` 를 통해 발행한다. 
  2. 이벤트 디스패치 (Dispatch) : 스프링 컨테이너가 발행된 이벤트를 받아서, 해다 이벤트 타입을 처리할 수 있는 이벤트 리스터(`@EventListener`) 를 찾아 호출해준다. 
  3. 이벤트 처리 (Handler) : 이벤트 리스너가 이벤트 객체를 전달 받아, 그 안의 데이터를 기반으로 후처리를 실행한다. 

적용 방식 

1) 이벤트 클래스를 생성한다. 

public record OrderPlacedEvent(Long orderId, String buyerEmail, int totalAmount) {}

2) 기존의 OrderService를 수정한다. 

이벤트를 보내는 기능을 사용하기 위해 ApplicationEventPublisher를 주입하였다. 주문이 완료되면, pubilshEvent를 사용해 이벤트를 발행해준다. 

@Service
@RequiredArgsConstructor
public class OrderService {

    private final ApplicationEventPublisher publisher;

    @Transactional
    public void placeOrder(PlaceOrderCommand cmd) {
        Order order = createAndPay(cmd);
        System.out.println("주문 완료: " + order.getId());

        // 주문 완료 이벤트 발행
        publisher.publishEvent(
            new OrderPlacedEvent(order.getId(), order.getBuyerEmail(), order.getTotalAmount())
        );
    }
}

3) 이벤트 핸들러 등록한다. 

`@EventListener` 를 사용하면 이벤트 리스너로 등록이 되고, 매개변수에 이벤트 클래스를 정의하면 해당 이벤트가 발생했을 때 수신해서 처리할 수 있다. 

@Component
@RequiredArgsConstructor
public class OrderEventHandler {

    private final MailService mailService;
    private final PointService pointService;
    private final WebhookService webhookService;

    @EventListener
    public void sendReceipt(OrderPlacedEvent event) {
        mailService.sendReceipt(event.orderId(), event.buyerEmail(), event.totalAmount());
    }

    @EventListener
    public void earnPoints(OrderPlacedEvent event) {
        pointService.earn(event.orderId(), event.totalAmount());
    }

    @EventListener
    public void notifyWebhook(OrderPlacedEvent event) {
        webhookService.notifyOrderPlaced(event.orderId());
    }
}

비동기 처리로 응답 속도 지키기 

예를 들어, 주문 처리를 하고 메일을 보내는데 3초, 포인트 적립 3초, 알림 3초가 순차적으로 실행된다면 최종 응답은 약 9초 뒤에야 사용자에게 전달된다. 이렇게 되면 사용자는 이미 주문 완료를 눌렀는데도 한 참 뒤에서나 완료가 되었다는 응답을 받아볼 수 있다. 그렇기 때문에 핵심 로직은 동기적으로 즉시 완료 시키고 후처리(메일, 포인트, 알림)은 다른 스레드에서 병렬로 실행하도록 해보려고 한다. 

 

1) 전역 설정 

@SpringBootApplication
@EnableAsync
public class ShopApplication {}
@Configuration
public class AsyncConfig {
    @Bean(name = "eventExecutor")
    public Executor eventExecutor() {
        ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
        ex.setCorePoolSize(4);
        ex.setMaxPoolSize(8);
        ex.setQueueCapacity(1000);
        ex.setThreadNamePrefix("event-");
        ex.initialize();
        return ex;
    }
}

2) 리스너 비동기화 

@Component
@RequiredArgsConstructor
public class OrderEventHandler {

    private final MailService mailService;
    private final PointService pointService;
    private final WebhookService webhookService;

    @Async("eventExecutor")
    @EventListener
    public void sendReceipt(OrderPlacedEvent e) {
        mailService.sendReceipt(e.orderId(), e.buyerEmail(), e.totalAmount());
    }

    @Async("eventExecutor")
    @EventListener
    public void earnPoints(OrderPlacedEvent e) {
        pointService.earn(e.orderId(), e.totalAmount());
    }

    @Async("eventExecutor")
    @EventListener
    public void notifyWebhook(OrderPlacedEvent e) {
        webhookService.notifyOrderPlaced(e.orderId());
    }
}

@TransactionalEventListener 

여기에 더해서 트랜잭션 정합성까지 챙겨보도록 하겠다. 아래 두 가지 메서드가 존재한다. 두 메서드는 유사하지만 차이가 있다. 

@Component
@RequiredArgsConstructor
public class OrderEventHandler {

    private final MailService mailService;
    private final PointService pointService;

    @Async("eventExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendReceipt(OrderPlacedEvent e) {
        mailService.sendReceipt(e.orderId(), e.buyerEmail(), e.totalAmount());
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void earnPoints(OrderPlacedEvent e) {
        pointService.earn(e.orderId(), e.totalAmount());
    }
}

 

1) `@Async + @TransactionalEventListener(AFTER_COMMIT)` 

  • 실행 시점 : 주문 트랜잭션이 성공적으로 커밋된 이후에 실행된다. 
  • 실행 스레드 : `@Async` 가 붙으면 스프링이 별도의 스레드 풀(eventExecutor)에서 실행한다. 
  • 트랜잭션 전파 : 기본 (REQUIRED, 이미 진행 중인 트랜잭션이 있으면 합류, 없으면 새 트랜잭션을 열어라) 
  • 언제 사용하나 : DB를 건드리지 않는 외부 I/O 성 후처리에서 사용한다. 예를 들어, 메일 발송이나 푸시/알림톡, 외부 웹훅 호출 시에 사용한다. 커밋 후 알림만 보내기만 하면 충분할 경우 
  • 장점 : 사용자 응답 속도가 빠르다. 

2) `@Transactional(propagation = REQUIRES_NEW) + @TransactionalEventListener(AFTER_COMMIT)` 

  • 실행 시점 : 주문 트랜잭션이 성공적으로 커밋된 이후에 실행된다. 
  • 실행 스레드 : `@Async` 를 사용하지 않았기 때문에, 별도의 스레드가 아니라 현재 스레드 그대로 실행된다. 
  • 트랜잭션 전파 : `REQUIRES_NEW` (항상 새로운 트랜잭션을 열어 독립적인 커밋과 롤백을 보장한다.) 
  • 언제 사용하나 : 리스너에서 DB 작업이 필수이고 결과가 반드시 커밋되어야 할 때 사용한다. 예를 들어, 포인트 적립, 쿠폰 발급 등 데이터 무결성이 중요한 작업 
  • 장점 : 본 트랜잭션과 완전 분리된 커싱르 보장한다. 리스너 실패가 본 트랜잭션에 영향이 없다. 
  • 단점 : 동기 실행이라 처리 시간이 길면 호출 지연이 발생한다. 

위에 코드에서는 이메일을 보내는 것은 다른 스레드에서 비동기 방식으로 진행되고 트랜잭션 전파 수준은 기본이라 원래 트랜잭션에 합류하려 하지만 AFTER_COMIMT 시점엔 이미 끝난 상태라 사실상 트랜잭션 없이 실행된다. 반대로 포인트 제공같은 경우에는 같은 스레드에서 처리되지만 새로운 트랜잭션을 생성하여 독립적인 커밋을 보장한다. 


참고 

https://dgjinsu.tistory.com/41 

 

[Event] Spring Event란? 이벤트 발행과 구독 방법

1. 이벤트를 사용하는 이유 Spring Boot에서 이벤트를 적용하는 방법에 대해 들어가기 전에, 이벤트를 왜 써야하는지, 사용하면 좋은 상황에 대해 먼저 알아보자. 회원가입을 하고 나면 가입 축하

dgjinsu.tistory.com

https://mangkyu.tistory.com/?page=1

'Spring' 카테고리의 다른 글

[Spring] 즉시 로딩과 지연 로딩  (0) 2025.09.21
[Spring] Transactional 사용 시 자기 호출(Self-Invocation) 이슈  (0) 2025.09.11
[Spring] Setter 사용을 지양해야하는 이유  (0) 2025.08.29
[Spring] DTO class를 record로 사용하는 이유  (0) 2025.08.29
[Spring] 연관관계  (0) 2025.08.29
'Spring' 카테고리의 다른 글
  • [Spring] 즉시 로딩과 지연 로딩
  • [Spring] Transactional 사용 시 자기 호출(Self-Invocation) 이슈
  • [Spring] Setter 사용을 지양해야하는 이유
  • [Spring] DTO class를 record로 사용하는 이유
erika0915
erika0915
백엔드 개발자가 되고 싶어요 .
  • erika0915
    erikoding
    erika0915
  • 전체
    오늘
    어제
    • 분류 전체보기 (78)
      • 프로젝트 (13)
        • 끼니콩 (3)
        • 덕메랑 (3)
        • handDoc (7)
        • Haeil (0)
      • Java (9)
        • 클린코더스 (0)
      • Spring (30)
      • Redis (3)
      • CS (7)
        • 운영체제 (3)
        • 컴퓨터구조 (0)
        • 네트워크 (4)
      • DevOps (2)
      • 코딩테스트 (0)
      • Tech (14)
        • TDD (1)
        • 정리 (5)
        • 우테코 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    coderabbit
    promtail
    스프링
    CoolSMS
    AI
    Network
    파인튜닝
    OS
    docker
    운영체제
    jira
    redis
    springboot
    도커
    지라
    github
    코드레빗
    스프링부트
    자바
    깃
    깃허브
    git
    레디스
    java
    STT
    몽고디비
    MongoDB
    Spring
    네트워크
    TDD
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
erika0915
[Spring] Spring Events 사용해 이벤트 발행하기

티스토리툴바