매번 프로젝트마다 초기마다 항상 찾아보는 것들이 몇가지 존재있다....
Querydsl 설정, Multi Datasource 설정 등등 사실 현재 임시글에 작성하다 중단된 글들이 존재한다.
그 중 하나가 Spring Event
와 Spring Transaction
및 Sync&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
의 경우에는 이벤트 발행이후 바로 구독 로직이 동기로 실행되게 된다.
@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
는 ApplicationEventPublisher.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
이 수행되는 것이 확인된다.
@TransactionalEventListener
는 Transaction
이 종료된 이후 실행되므로 @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
에 대해서 정리해보았다. 이전에 헷갈렸던 내용들도 다시 공부할 수 있었고 사실 알지 못했던 내용들도 상당 부분 존재했었다. 얼마나 더 많은 것을 알고있다고 착각하고 있을지 모르겠다.....