[Spring] Spring Event와 Transaction 및 동기여부 이제 그만 찾고 정리 하자!!(TransactionalEventListener, EventListener, Application Event)

YouMakeMeSmile·2024년 11월 10일
0
post-thumbnail

들어가며....

매번 프로젝트마다 초기마다 항상 찾아보는 것들이 몇가지 존재있다....
Querydsl 설정, Multi Datasource 설정 등등 사실 현재 임시글에 작성하다 중단된 글들이 존재한다.
그 중 하나가 Spring EventSpring TransactionSync&Async의 관계들에 대해서 찾아보곤 했다. 이번 글을 마지막으로 이제 그만 찾고 정리를 해보려고 한다.

공식 문서에 더 자세한 내용이 나와있는 것을 잊으면 안된다.

기본 발행/구독

발행은 Spring에서 제공해주는ApplicationEventPublisher.publishEvent를 통해서 가능하다. 발행될 클래스는 각각의 발행과 구독에서 정의된 객체를 자유롭게 생성하며 발행하면 된다.

@Service
@RequiredArgsConstructor
public class TestEventPublisher {
    private final ApplicationEventPublisher applicationEventPublisher;

    public void publishEvent() {
        applicationEventPublisher.publishEvent(new TestEvent("TEST"));
    }
}

구독은 두 가지 어노테이션을 활용하여 처리가 가능하며 각각의 동작 방식이 다르며 각 로직에 맞게 조합하여 사용해야한다.

@Component
@RequiredArgsConstructor
public class TestEventListener {
    private final UserRepository userRepository;

    @EventListener
    public void onTestEvent(TestEvent testEvent) {

    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void onTestEvent2(TestEvent testEvent) {
    }
}

@EventListener

@EventListener의 경우에는 이벤트 발행이후 바로 구독 로직이 동기로 실행되게 된다.

@Service
@RequiredArgsConstructor
@Slf4j
public class TestEventPublisher {
    private final ApplicationEventPublisher applicationEventPublisher;

    public void publishEvent() {
        log.info("발행 시작");
        applicationEventPublisher.publishEvent(new TestEvent("TEST"));
        log.info("발행 종료");
    }
}
@Component
@RequiredArgsConstructor
@Slf4j
public class TestEventListener {
    private final UserRepository userRepository;

    @EventListener
    public void onTestEvent(TestEvent testEvent) throws InterruptedException {
        log.info("구독 시작");
        Thread.sleep(10000);
        log.info("구독 종료");
    }

    @EventListener
    public void onTestEvent3(TestEvent testEvent) throws InterruptedException {
        log.info("구독 시작2");
        Thread.sleep(10000);
        log.info("구독 종료2");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void onTestEvent2(TestEvent testEvent) {
    }
}
2024-11-03T19:06:46.710+09:00  INFO 82052 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 시작
2024-11-03T19:06:46.710+09:00  INFO 82052 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : 구독 시작
2024-11-03T19:06:56.714+09:00  INFO 82052 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : 구독 종료
2024-11-03T19:06:56.716+09:00  INFO 82052 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : 구독 시작2
2024-11-03T19:07:06.721+09:00  INFO 82052 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : 구독 종료2
2024-11-03T19:07:06.726+09:00  INFO 82052 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 종료

로그 순서와 같이 이벤트 발행 이후 같은 쓰레드에서 구독 메소드들이 수행된 이후 다시 발행 로직이 수행되는 것을 확인해볼 수 있다.
이를 통해서 기본적인 @EventListener를 통해서는 같은 Transaction으로 로직이 수행되며 발행과 구독 로직이 동기로 동작한다는 것을 알 수 있다.
발행과 구독 로직을 비동기로 수행되기 하기 위해서는 적절하게 발행 또는 구독 로직을 비동기로 수행시켜야한다.

@Service
@RequiredArgsConstructor
@Slf4j
public class TestEventPublisher {
    private final ApplicationEventPublisher applicationEventPublisher;

    public void publishEvent() {
        log.info("발행 시작");
        applicationEventPublisher.publishEvent(new TestEvent("TEST"));
        log.info("발행 종료");
    }
}
@Component
@RequiredArgsConstructor
@Slf4j
public class TestEventListener {
    private final UserRepository userRepository;

    @Async
    @EventListener
    public void onTestEvent(TestEvent testEvent) throws InterruptedException {
        log.info("구독 시작");
        Thread.sleep(10000);
        log.info("구독 종료");
    }

    @Async
    @EventListener
    public void onTestEvent3(TestEvent testEvent) throws InterruptedException {
        log.info("구독 시작2");
        Thread.sleep(10000);
        log.info("구독 종료2");
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void onTestEvent2(TestEvent testEvent) {
    }
}
2024-11-03T20:04:12.825+09:00  INFO 82986 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 시작
2024-11-03T20:04:12.826+09:00  INFO 82986 --- [persistence-context] [         task-1] i.v.y.event.TestEventListener            : 구독 시작
2024-11-03T20:04:12.826+09:00  INFO 82986 --- [persistence-context] [         task-2] i.v.y.event.TestEventListener            : 구독 시작2
2024-11-03T20:04:12.827+09:00  INFO 82986 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 종료
2024-11-03T20:04:22.831+09:00  INFO 82986 --- [persistence-context] [         task-1] i.v.y.event.TestEventListener            : 구독 종료
2024-11-03T20:04:22.831+09:00  INFO 82986 --- [persistence-context] [         task-2] i.v.y.event.TestEventListener            : 구독 종료2

위의 경우는 발행은 동기식으로 호출하였고 구독은 @Async를 통해서 비동기로 수행된 경우의 로그이다.
이와 같이 발행과 구독에서 해당 비즈니스 로직에 맞게 구성하여 사용가능하며 다음의 그림은 수행되는 쓰레드를 표현하였다.


@EventListener 한계? 그리고 해결 방법(@TransactionalEventListener)

@EventListenerApplicationEventPublisher.publishEvent가 발생하는 즉시 수행되게 된다. 그렇다는 것은 발행 이후 같은 Transaction 로직 결과에 상관없이 @EventListener가 실행된다는 것이다. 백엔드 개발은 Transaction과의 싸움 아니겠는가!!
이렇게 이벤트 구독 로직과 Transaction 로직을 처리하기 위해 Spring에서는 @TransactionalEventListener를 제공하고 있다.

@RestController
@RequiredArgsConstructor
@Transactional
@Slf4j
public class TestController {
    private final TestEventPublisher testEventPublisher;

    @GetMapping("/test111")
    @Transactional
    public void test111() {
        testEventPublisher.publishEvent();
        log.info("로직 종료");
    }

}

@Service
@RequiredArgsConstructor
@Slf4j
public class TestEventPublisher {
    private final ApplicationEventPublisher applicationEventPublisher;

    public void publishEvent() {
        log.info("발행 시작");
        applicationEventPublisher.publishEvent(new TestEvent("TEST"));
        log.info("발행 종료");
    }
}

@Component
@RequiredArgsConstructor
@Slf4j
public class TestEventListener {
    private final UserRepository userRepository;

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void onTestEvent1(TestEvent testEvent) {
        log.info(TransactionPhase.BEFORE_COMMIT.name());
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onTestEvent2(TestEvent testEvent) {
        log.info(TransactionPhase.AFTER_COMMIT.name());
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void onTestEvent3(TestEvent testEvent) {
        log.info(TransactionPhase.AFTER_ROLLBACK.name());
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void onTestEvent4(TestEvent testEvent) {
        log.info(TransactionPhase.AFTER_COMPLETION.name());
    }
}
2024-11-10T18:23:41.926+09:00 TRACE 91099 --- [persistence-context] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [io.velog.youmakemesmile.TestController.test111]
2024-11-10T18:23:41.926+09:00  INFO 91099 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 시작
2024-11-10T18:23:41.927+09:00  INFO 91099 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 종료
2024-11-10T18:23:41.927+09:00  INFO 91099 --- [persistence-context] [nio-8080-exec-1] io.velog.youmakemesmile.TestController   : 로직 종료
2024-11-10T18:23:41.927+09:00 TRACE 91099 --- [persistence-context] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [io.velog.youmakemesmile.TestController.test111]
2024-11-10T18:23:41.927+09:00  INFO 91099 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : BEFORE_COMMIT
2024-11-10T18:23:41.939+09:00  INFO 91099 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : AFTER_COMMIT
2024-11-10T18:23:41.939+09:00  INFO 91099 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : AFTER_COMPLETION

정상적으로 Transaction이 종료된 경우에는 위 로그와 같이 Transaction 완료이후 각각의 BEFORE_COMMIT, AFTER_COMMIT, AFTER_COMPLETION 이 수행되는 것을 확인 할 수 있다.

@RestController
@RequiredArgsConstructor
@Transactional
@Slf4j
public class TestController {

    private final TestEventPublisher testEventPublisher;

    @GetMapping("/test111")
    @Transactional
    public void test111() {
        testEventPublisher.publishEvent();
        throw new RuntimeException();
    }
}

...
2024-11-10T18:33:13.333+09:00 TRACE 91217 --- [persistence-context] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Getting transaction for [io.velog.youmakemesmile.TestController.test111]
2024-11-10T18:33:13.333+09:00  INFO 91217 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 시작
2024-11-10T18:33:13.333+09:00  INFO 91217 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventPublisher           : 발행 종료
2024-11-10T18:33:13.333+09:00 TRACE 91217 --- [persistence-context] [nio-8080-exec-1] o.s.t.i.TransactionInterceptor           : Completing transaction for [io.velog.youmakemesmile.TestController.test111] after exception: java.lang.RuntimeException
2024-11-10T18:33:13.339+09:00  INFO 91217 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : AFTER_ROLLBACK
2024-11-10T18:33:13.339+09:00  INFO 91217 --- [persistence-context] [nio-8080-exec-1] i.v.y.event.TestEventListener            : AFTER_COMPLETION
2024-11-10T18:33:13.341+09:00 ERROR 91217 --- [persistence-context] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException] with root cause

위는 Exception이 발생한 경우로 AFTER_ROLLBACK, AFTER_COMPLETION이 수행되는 것이 확인된다.


@TransactionalEventListenerTransaction이 종료된 이후 실행되므로 @TransactionalEventListener 메소드에 @Transaction으로는 Spring 기동시 런타임 예외가 발생하며 예외 메세지에서 처럼 REQUIRES_NEW, NOT_SUPPORTED 으로 설정하면 해당 메소드에서 @Transaction 처리가 가능하다.

Caused by: java.lang.IllegalStateException: @TransactionalEventListener method must not be annotated with @Transactional unless when declared as REQUIRES_NEW or NOT_SUPPORTED: public void io.velog.youmakemesmile.event.TestEventListener.onTestEvent1(io.velog.youmakemesmile.event.TestEvent)

@TransactionalEventListener@EventListener과 동일하게 기본적으로는 발행한 로직의 쓰레드를 그대로 사용하여 동작하게된다. 그 이야기는 @TransactionalEventListener 로직이 본래의 로직과 상관없이 응답을 전달하고 싶다면 @EventListener에서와 동일하게 비동기를 적용하여 쓰레드를 분리하면 된다.


부록

위는 업무 로직에 의해서 발생되는 Spring Event에 관한 내용이였으며 Spring에서도 각 시점에 Application Event를 발생시켜주고 있으며 개발자는 이러한 Application Event를 사용하여 각 시점에 자신의 로직을 수행 시킬 수 있다. 자세한 내용은 역시나 Spirng Boot Docs, Srping Framework Docs에 작성되어 있으며 다음의 내용을 반드시 알고 있어야 한다.

Some events are actually triggered before the ApplicationContext is created, so you cannot register a listener on those as a @Bean. You can register them with the SpringApplication.addListeners(…​) method or the SpringApplicationBuilder.listeners(…​) method.
If you want those listeners to be registered automatically, regardless of the way the application is created, you can add a META-INF/spring.factories file to your project and reference your listener(s) by using the org.springframework.context.ApplicationListener key, as shown in the following example:
org.springframework.context.ApplicationListener=com.example.project.MyListener

@Slf4j
public class ApplicationPreparedEventListener implements ApplicationListener<ApplicationPreparedEvent> {
    @Override
    public void onApplicationEvent(ApplicationPreparedEvent event) {
        log.info(ApplicationPreparedEvent.class.getName());
    }
}
public class ApplicationStartingEventListener implements ApplicationListener<ApplicationStartingEvent> {

    @Override
    public void onApplicationEvent(ApplicationStartingEvent event) {
        System.out.println(ApplicationStartingEvent.class.getName() + " started");
    }
}
@Slf4j
public class TestEventListener {
    private final UserRepository userRepository;


    @EventListener(ContextRefreshedEvent.class)
    public void onContextRefreshedEvent(ContextRefreshedEvent event) {
        log.info(ContextRefreshedEvent.class.getName());
    }

    @EventListener(ApplicationStartedEvent.class)
    public void onApplicationStartedEvent(ApplicationStartedEvent event) {
        log.info(ApplicationStartedEvent.class.getName());
    }

    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReadyEvent(ApplicationReadyEvent event) {
        log.info(ApplicationReadyEvent.class.getName());
    }

    @EventListener(ContextClosedEvent.class)
    public void onContextClosedEvent(ContextClosedEvent event) {
        log.info(ContextClosedEvent.class.getName());
    }
}
org.springframework.context.ApplicationListener=io.velog.youmakemesmile.event.ApplicationStartingEventListener, io.velog.youmakemesmile.event.ApplicationPreparedEventListener


위와 같이 각 이벤트의 시점마다 실행되는 것을 확인 할 수 있으며 위에 설명처럼 각 이벤트 시점에 따라 @Bean으로 등록되지 못하는 이벤트가 존재하므로 META-INF/spring.factories에 각 이벤트 리스너를 등록해야 한다.


이렇게 기본적인 Spring Event에 대해서 정리해보았다. 이전에 헷갈렸던 내용들도 다시 공부할 수 있었고 사실 알지 못했던 내용들도 상당 부분 존재했었다. 얼마나 더 많은 것을 알고있다고 착각하고 있을지 모르겠다.....

profile
어느새 7년차 중니어 백엔드 개발자 입니다.

0개의 댓글