@EventListener, @TransactionalEventListener 설에 따른 이벤트 동작 방식

2025. 8. 13. 21:00·Java & Spring

안녕하세요 이번 포스팅에서는 이벤트 리스너 어노테이션에 대해 다루고자 합니다.

 

도입하여 사용하게된 계기는 다음과 같았습니다.

1. 메인 비지니스 로직과 알림과 같은 부가적인 로직의 분리 - 알림 발송에 대한 처리
2. 분리를 통한 결합도 낮춤 및 확장 가능한 설계
3. @Async 도입을 통해 비동기 처리로 퍼포먼스 향상

 

그럼 스프링에서 제공하는 @EventListener, @TransactionalEventListener를 통해 어떤 경우에 어떻게 동작하는지 정리해 보겠습니다.

 

1. @EventListener

  • 이벤트를 publish 해주는 곳에서 PostCreatedEventFail 타입으로 발행해주면 해당 타입으로 리스너가 구성되어 있는 메소드는수행되게 됩니다
  • EventListener 안에 별도로 class 속성을 정의하여 여러 타입에 대한 이벤트도 받을 수 있습니다.
public void publishCreatedFailEvent(Post post) {
    publisher.publishEvent(new PostCreatedEventFail(post));
}
@EventListener()
@Async
public void handlePostCreatedAfterRollbackFail(PostCreatedEventFail event){
    Post post = event.post();
    log.info("EventListener executed, Use Default EventListener");
    notificationService.send(Notification.from(post));
}

 

2. @TransactionalEventListener

  • 이벤트를 publish 해주는거에 더해 트랜잭션의 흐름에 따라 이벤트 리스너의 동작을 결정할 수 있습니다.
  • TransactionPhase 에 대해 4가지 속성이 존재합니다.
public enum TransactionPhase {
    BEFORE_COMMIT,
    AFTER_COMMIT,
    AFTER_ROLLBACK,
    AFTER_COMPLETION;

    private TransactionPhase() {
    }
}
  1. BEFORE_COMMIT : 트랜잭션 커밋 직전에 실행
  2. AFTER_COMMIT : 트랜잭션 커밋 후 실행
  3. AFTER_ROLLBACK : 트랜잭션 롤백 후 실행
  4. AFTER_COMPLETION : 커밋이든 롤백이든 트랜잭션 종료 후 무조건 실행
  • 이러한 속성들이 존재하여 트랜잭션의 흐름에 따라 이벤트리스너의 동작 시점을 설정하여 사용할 수 있습니다.
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void handlePostCreatedAfterCommit(PostCreatedEventSuccess event) throws InterruptedException {
    var post = event.post();
    log.info("TransactionalEventListener phase = TransactionPhase.AFTER_COMMIT executed");
    notificationService.send(Notification.from(post));
}
  • 트랜잭션이 커밋된 후에 해당 이벤트 리스너가 수행되도록 설정.

3. 동작 테스트

동작 테스트는 대표적으로 AFTER_COMMIT 과 ROLLBACK 상황에 대해서만 해보도록 하겠습니니다.

 

1. 테스트 환경 설정

  • 실제 이메일 전송을 하진 않으므로 테스트코드에서 email 전송 객체를 구현하여 1초동안 sleep 을 주어 동작하는 것과 유사하도록 구성하였습니다.
@TestConfiguration
@Slf4j
public class EmailSendTest implements SendNotificationInterface {

    @Override
    public void send() throws InterruptedException {
        log.info("test email send trigger");
        Thread.sleep(1000);
    }
}
  • NotificationService 또한 SpringBoot 객체가 주입되는게 아닌 TestConfig 객체로 주입되도록 대체하였습니다.
@TestConfiguration
public class TestConfig {

    @Bean
    public NotificationService notificationService(NotificationJpaRepository notificationJpaRepository){
        return NotificationService.builder()
                .notificationJpaRepository(notificationJpaRepository)
                .emailSend(new EmailSendTest())
                .build();
    }
}
  • notificationService 의 send 메소드
@Transactional
public Notification send(Notification notification){
    try {
        emailSend.send();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
    return notificationJpaRepository.save(notification);
}

2. AFTER_COMMIT 테스트

다음 테스트코드에서 테스트한 상황은 아래와 같다 생각 해주시면 되겠습니다.

1. 게시물이 성공적으로 저장이 이루어 진 후에 메일이 발송되어야 합니다.
2. 메일이 발송된 후 해당 발송 이력을 Notification table 에 저장하여 이력을 남겨야 합니다.
  • 실행 흐름 요약 : 트랜잭션이 정상적으로 처리가 된 경우에 이벤트 리스너가 동작하여 메일을 전송하고 해당 메일을 전송하는 테스트 코드
  • 테스트 코드
@Test
@DisplayName("게시글 생성이 정상적으로 된 후 notification 은 정상적으로 1번만 발송된다.")
void createSuccessAndSendNotification() {
    PostCreateRequest postCreateRequest = PostCreateRequest.builder()
            .title("title")
            .content("content")
            .authorId("1L").build();
    postService.createPostSuccess(postCreateRequest);
    await().atMost(Duration.ofSeconds(2))
            .pollInterval(Duration.ofMillis(200)) // 200ms마다 확인
            .untilAsserted(() -> assertThat(notificationService.findAll()).hasSize(1));
}

 

  • PostService 의 게시물 생성 메소드
@Transactional
public Post createPostSuccess(PostCreateRequest postCreateRequest) {
    Post post = postCreateRequest.toEntity();
    Post saved = postJpaRepository.save(post);
    eventPublisher.publishCreatedSuccessEvent(saved);
    return saved;
}

 

  • 이벤트 publisher 메소드
public void publishCreatedSuccessEvent(Post post) {
    publisher.publishEvent(new PostCreatedEventSuccess(post));
}

 

  • Event Listener
    • 이벤트 리스너는 PostCreatedEventSuccess 객체 타입으로 이벤트가 발생한 것 중 커밋이 된 후에 수행되어야 합니다. 
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Async
public void handlePostCreatedAfterCommit(PostCreatedEventSuccess event) throws InterruptedException {
    var post = event.post();
    log.info("TransactionalEventListener phase = TransactionPhase.AFTER_COMMIT executed");
    notificationService.send(Notification.from(post));
}

 

 

- 로그를 확인해보면 실제 이벤트가 실행되고 알림 테이블에 데이터가 정상적으로 1건이 insert 된 걸 확인할 수 있습니다

 

 

3. AFTER_ROLLBACK

롤백에 대한 상황은 다음과 같이 주어진 상황에서 처리가 되어야 한다고 생각해 주시면 됩니다.

1. 게시물 작성 중 예외를 만나 롤백이 처리된다.
2. 게시물이 롤백되고, 관리자에게 실패에 대한 이메일 알림을 전송하여 메일 전송 이력 테이블에 데이터가 생성된다.
3. 이벤트 리스너가 2개 선언되어 있어 알림은 2개가 생성된다. 

 

3번의 경우는 이벤트 리스너가 다중으로 선언되어 있는 경우 어떻게 동작하는지 알아보기 위한 조건이라고 봐주시면 됩니다. 상황에 따라 특정 이벤트가 발생했을 때 서로 다른 동작을 동시에 처리할 경우도 존재하는 상황이라고 가정하였습니다.

 

  • 테스트 코드
@Test
@DisplayName("게시글 생성은 실패하고 실패에 대한 notification 이 해당하는 이벤트 리스너가 수신하여 2개 생성된다.")
void createPostFailAndSendNotification() {
    PostCreateRequest postCreateRequest = PostCreateRequest.builder()
            .title("title")
            .content("content")
            .authorId("1L").build();
    assertThatThrownBy(() -> postService.createPostFail(postCreateRequest)).isInstanceOf(RuntimeException.class);
    await().atMost(Duration.ofSeconds(2))
            .untilAsserted(() -> assertThat(notificationService.findAll()).hasSize(2));
}

 

  • PostService 게시물 생성 실패 메소드
@Transactional
public Post createPostFail(PostCreateRequest postCreateRequest) {
    Post post = postCreateRequest.toEntity();
    Post saved = postJpaRepository.save(post);
    eventPublisher.publishCreatedFailEvent(saved);
    throw new RuntimeException();
}

 

  • 이벤트 publisher 메소드
public void publishCreatedSuccessEvent(Post post) {
    publisher.publishEvent(new PostCreatedEventSuccess(post));
}
  • EventListener
    • 롤백이 이루어진 것과 그리고 PostCreatedEventFail 타입으로 이벤트가 발생하면 수행되는 즉, 다음 2개의 이벤트가 트리거 됩니다.
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
@Async
public void handlePostCreatedAfterRollback(PostCreatedEventFail event) {
    Post post = event.post();
    log.info("EventListener executed , TransactionalEventListener phase = TransactionPhase.AFTER_ROLLBACK executed");
    notificationService.send(Notification.from(post));
}
@EventListener()
@Async
public void handlePostCreatedAfterRollbackFail(PostCreatedEventFail event){
    Post post = event.post();
    log.info("EventListener executed, Use Default EventListener");
    notificationService.send(Notification.from(post));
}

 

RollBack Event 리스너 동작

 

기본 EventListener

 

 

해당 테스트를 통해 트랜잭션의 흐름에 따라 이벤트 수행이 가능하고 또한 하나의 이벤트가 아닌 여러 이벤트를 동시에 트리거할 수 있다는 걸 확인할 수 있었습니다.

4. 생각해볼 포인트

1. 장점

1. 모듈간의 결합도가 낮아집니다.

2. 낮아진 결합도를 통해 확장성 있는 설계가 가능해집니다.

3. 트랜잭션 상태기반 처리를 가능하게 해줍니다.

2. 주의할 점

1. 실행 흐름 추적이 좀 어려울 수 있습니다.

2. 이벤트 리스너 내부 예외는 이벤트를 호출한 쪽으로 전파되지 않아 예외 처리 또는 모니터링을 잘 해주어야 합니다.

3. 테스트를 하는데 있어 복잡도 증가합니다.

5. 정리

@EventListener 와  @TransactionalEventListener를 언제 사용하면 좋을지에 대해 추가로 정리해 보도록 하겠습니다.

구분 @EventListener @TransactionalEventListener
트랜잭션 영향 트랜잭션과 무관, 즉시 실행 트랜잭션 상태(커밋/롤백)에 따라 실행 
실행 시점 제어 불가능 (조건은 condition으로만) TransactionPhase로 시점 지정 가능 
주 사용 사례 단순 이벤트 처리, 비동기 알림 DB 변경 후 처리, 롤백 알림, 후처리 
비동기 처리 @Async 조합 가능  @Async 조합 가능 

 

이번 글에서는 스프링의 이벤트 리스너 동작 방식과 각 시점별 활용법을 살펴보았습니다.
실제 서비스 로직에서 어떤 시점에 어떤 이벤트를 처리해야 할지 명확히 설계하면, 유지보수성과 확장성을 모두 잡을 수 있습니다.

 

 

전체 코드

 

blog-code/spring_event_listener at main · jjjwodls/blog-code

Contribute to jjjwodls/blog-code development by creating an account on GitHub.

github.com

 

'Java & Spring' 카테고리의 다른 글

Awaitility로 비동기 이벤트 테스트 하기: Spring @Async와 함께 쓰는 법  (3) 2025.07.29
Java Enum 다형성으로 Notification 처리 리팩토링하기 - 조건문 없는 전략 설계  (1) 2025.07.26
Spring Cloud Config 도입기: 구성부터 실시간 설정 반영까지  (3) 2025.07.24
Spring Rest Docs 적용 3 - Controller Test 작성을 통한 문서화  (2) 2025.07.22
Spring Rest Docs 적용 2 - Swagger 연동  (1) 2025.07.21
'Java & Spring' 카테고리의 다른 글
  • Awaitility로 비동기 이벤트 테스트 하기: Spring @Async와 함께 쓰는 법
  • Java Enum 다형성으로 Notification 처리 리팩토링하기 - 조건문 없는 전략 설계
  • Spring Cloud Config 도입기: 구성부터 실시간 설정 반영까지
  • Spring Rest Docs 적용 3 - Controller Test 작성을 통한 문서화
jaess
jaess
jaess 님의 블로그 입니다.
  • jaess
    개발하는 개발자
    jaess
  • 전체
    오늘
    어제
    • 분류 전체보기 (9)
      • 회고 (1)
      • API 설계 (1)
      • 생각정리 (0)
      • Java & Spring (7)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

    스프링이벤트
    Spring cloud
    비동기 테스트
    Awaitility
    Git 설정 관리
    비동기 메시징
    REST API
    TransactionalEventListener
    RestDoc
    Spring Actuator
    junit5
    @Async
    springboot
    이벤트 리스너
    OOP (Object Oriented Programming)
    Spring Event
    Spring Cloud Config Client
    분기처리개선
    비동기이벤트
    코드리팩토링
    Spring Cloud Config
    java
    Spring
    무중단설정변경
    CleanCode
    개발자
    백엔드개발
    ifelse지옥탈출
    Spring Cloud Config Server
    Spring boot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
jaess
@EventListener, @TransactionalEventListener 설에 따른 이벤트 동작 방식
상단으로

티스토리툴바