-
Spring @EventListener 사용하기Backend/학습내용 정리 2024. 5. 29. 01:03
글 목차
1. ApplicationEventPublisher vs ApplicationEventPublisherAware
프로젝트를 진행하며 Spring 의 Event 기반으로 개발을 진행했습니다. 사용자가 검색을 진행하면 해당 키워드로 이벤트를 생성하고 EventListener 부분에서는 관련된 로직을 처리하도록 개발했습니다.
이렇게 이벤트 기반으로 로직을 분리하면서 메인 로직인 기사 검색에 집중할 수 있었고, 이벤트 처리 로직에서는 키워드 순위와 관련된 로직을 처리할 수 있었습니다.
이번 글에서는 Event 처리 기반으로 구현하면서 공부했던 내용을 정리했습니다.
ApplicationEventPublisher vs ApplicationEventPublisherAware
이벤트 publisher를 구현하기 위해 여러 블로그를 찾다보니 ApplicationEventPublisher 인터페이스와 ApplicationEventPublisherAware 인터페이스를 많이 보게 되었습니다. 어떤 글에서는 ApplicationEventPublisher 를 이용해 그대로 이벤트를 publish 하는 코드도 있었고, 어떤 글은 ApplicationEventPublisherAware 인터페이스를 구현한 구현체에서 ApplicationEventPublisher를 구현한 코드도 있었습니다.
Spring 에서 Event 를 발행하려면 ApplicationEventPublisher 인터페이스를 구현한 클래스를 사용해야 합니다. 이때 여러 방법이 있겠지만 ApplicationEventPublisher를 의존성 주입을 통해 객체에서 사용할 수 있고 혹은 ApplicationEventPublisherAware 인터페이스를 구현하여 해당 클래스에서 ApplicationEventPublisher 를 설정할 수 있습니다.
ApplicationEventPublisherAware
먼저 ApplicationEventPublisherAware 인터페이스에 대해 살펴보겠습니다.
위 사진은 테스트를 위해 ApplicationEventPublisherAware 인터페이스를 구현한 EventPublisherWithAware 클래스입니다. 위 사진을 통해 ApplicationEventPublisher를 의존성 주입으로 받은것이 아니라 setApplicationEventPublisher 메서드를 통해 설정하는 것을 확인할 수 있습니다.
참고로 이때 주입받는 클래스는 AnnotationConfigServletWebServerApplicationContext 클래스이며
해당 클래스는 스프링 어플리케이션이 초기화될 때 생성됩니다.이때 ApplicationContextAwareProcessor 클래스에서 Aware 인터페이스를 구현한 클래스들을 확인해서 setApplicationEventPublisher 메서드를 실행하기 때문에 따로 ApplicationEventPublisher 구현체를 의존성 주입을 통해 받지 않아도 사용할 수 있습니다.
아래 사진을 통해 직접 구현한 EventPublisherWithAware 클래스 빈 객체의 setApplicationEventPublisher를 통해 ApplicationEventPublisher 를 설정하고 있는것을 확인할 수 있습니다.
ApplicationEventPublisher
아래 사진은 EventPublisher 빈 생성 과정에서 ApplicationEventPublisher를 직접 의존성 주입을 통해 받는 과정입니다.
정리
결국 ApplicationEventPublisher 와 ApplicationEventPublisherAware 인터페이스는 완전히 기능이 다르다는 것을 확인할 수 있습니다.
ApplicationEventPublisher는 뜻 그대로 이벤트를 발행하는 인터페이스를 의미합니다. 그리고 ApplicationEventPublisherAware 인터페이스는 ApplicationEventPublisher 를 구현체에 주입받을 수 있도록 도와주는 인터페이스라고 생각할 수 있습니다.
두 방법 중에 어떤 방법이 좀 더 좋은 방법인지는 개인적인 판단이 어려워 저는 코드 구현이 간단한 ApplicationEventPublisher 의존성 주입으로 받는 코드를 사용했습니다.
비동기로 진행하기
1) @EventListener
가장 먼저 @EventListener 어노테이션만 사용해서 리스너 로직을 구현했습니다. 이후 비동기로 동작하는지 확인하기 위해 테스트를 진행 합니다. 테스트의 흐름은 요청을 받으면 해당 요청을 DB에 저장하고 이벤트를 발행합니다. 이후 EventListener 부분에서 전달받은 이벤트 시작 로그를 작성 후 5초간 대기 후 다시 이벤트 내용에 관한 로그를 작성하는 로직입니다.
// DB에 이벤트를 저장하고 eventPublisher를 이용해 이벤트를 발행하는 로직입니다. @Slf4j @Service @RequiredArgsConstructor public class HelloEventService { private final EventRepository eventRepository; private final EventPublisher eventPublisher; @Transactional public EventEntity saveEvent(String eventName) { EventEntity eventEntity = new EventEntity(eventName); EventEntity savedEvent = eventRepository.save(eventEntity); log.info("before event publish"); eventPublisher.publish(new HelloEventDto(savedEvent)); log.info("after event publish"); return savedEvent; } } // 이벤트를 받으면 시작 로그를 남기고 5초 뒤 이벤트 정보에 대한 로그를 남기는 로직입니다. @Slf4j @Component public class HelloEventListener { @EventListener public void listen(HelloEventDto helloEventDto) throws InterruptedException { log.info("eventListener start"); Thread.sleep(5000); log.info("helloEventDot = {}", helloEventDto); } }
위 코드처럼 작성하게 응답을 처리하는 스레드부터 이벤트를 처리하는 스레드가 모두 동일한 쓰레드에서 작동을 하게 됩니다. 따라서 처음에 원했던 비동기로 로직을 진행하지 못하고 있습니다. 아래 사진을 통해 동일 스레드로 진행하고 있는것을 확인할 수 있습니다.
위 사진을 통해 모두 동일한 스레드에서 실행되고 있는것을 확인할 수 있습니다. 추가로 listener 쪽에서 5초간 대기하고 이벤트 로그를 작성하는것까지 확인 가능합니다.
2) @Async
기존 리스너 코드에 @Async 어노테이션을 추가하고 비동기 메서드 사용을 위해 @EnableAsync 어노테이션을 추가합니다.
@Slf4j @Component public class HelloEventListener { @Async // Async 어노테이션 추가 @EventListener public void listen(HelloEventDto helloEventDto) throws InterruptedException { log.info("eventListener start"); Thread.sleep(5000); log.info("helloEventDot = {}", helloEventDto); } }
아래 사진을 통해 확인할 수 있듯이 HelloEventListener 와 요청을 처리하는 로직의 스레드가 서로 다른 스레드임을 확인할 수 있습니다.
트랜잭션 확인
동기적으로 EventListener 가 처리할 때 트랜잭션 확인
우선 위에서 작성했던 @Async 어노테이션을 삭제하고 트랜잭션이 어떻게 작동하는지 확인하겠습니다
우선 @Async 어노테이션이 없기 때문에 EventListener 로직도 호출 로직과 동일한 스레드를 사용하고 있는 것을 알 수 있습니다. 또한 Service 레이어에서 시작한 트랜잭션이 EventListener 로직에서 동일한 트랜잭션을 유지하고 있고(빨간 박스 로그를 살펴보면 트랜잭션 이름이 동일합니다) EventListener 로직이 끝난 후에 트랜잭션이 완료되는 것을 확인할 수 있습니다.(파란 박스 로그를 살펴보면 확인 가능합니다)
그러면 다음으로 Listener 부분의 코드에 @Transactional 어노테이션을 설정해보겠습니다.
@Slf4j @Component public class HelloEventListener { @EventListener @Transactional public void listen(HelloEventDto helloEventDto) throws InterruptedException { String transactionName = TransactionSynchronizationManager.getCurrentTransactionName(); log.info("HelloEventListener transactionName = {}", transactionName); log.info("eventListener start"); Thread.sleep(5000); log.info("helloEventDot = {}", helloEventDto); } }
기본적으로 비동기 메서드로 작동하지 않기 때문에 listen 메서드는 요청 처리와 동일한 스레드에서 작동을 하게 됩니다. 이때
@Transactional 어노테이션 때문에 기본 설정으로 인해 기존 트랜잭션에 종속된 트랜잭션으로 실행됩니다. 즉, 동일한 트랜잭션으로 실행이 됩니다.
따라서 EventListener 부분의 트랜잭션이 먼저 종료가 된 후 Service 부분의 트랜잭션이 종료가 된다는 로그를 확인할 수 있습니다. 아래 사진을 통해 확인 가능합니다.
당연히 동일한 트랜잭션에 참여하기 때문에 Listener 쪽에서 에러가 발생하면 외부에 존재하던 Service 레이어도 영향을 받고 트랜잭션을 롤백시키게 됩니다. 마찬가지로 Service 레이어에서 에러가 발생하면 Listener 부분의 로직까지 롤백이 되는것을 알 수 있습니다.
이때 서로 다른 트랜잭션으로 실행을 하고자 한다면 @Transactional 의 옵션을 통해 서로 다른 트랜잭션으로 실행할 수 있습니다. @Transactional(propagation=Propagation.REQUIRES_NEW) 옵션을 이용해 서로 다른 트랜잭션으로 실행하도록 설정을 하면 각각 커밋과 롤백을 관리할 수 있습니다.
위 사진에서 볼 수 있듯이 @Transactional(propagation=Propagation.REQUIRES_NEW) 옵션을 이용한 경우 서로 다른 EntityManager가 생성되는 것을 알 수 있고 Listener 부분의 트랜잭션은 커밋되었지만 Service 부분의 트랜잭션은 롤백된 것을 로그를 통해 확인할 수 있습니다.
비동기적으로 EventListener 가 처리할 때 트랜잭션 확인
@Transactional 어노테이션에 propagation 설정이 Propagation.REQUIRES_NEW 로 되어 있던 기본 설정이던 관계없이 서로 다른 스레드에서 실행되는 로직이어서 서로 다른 트랜잭션이 생성됩니다.
따라서 Service 레이어나 LIstener 클래스 각각 서로 영향을 받지 않고 트랜잭션의 커밋이나 롤백이 진행됩니다. 아래 로그는 LIstner 로직에서 에러가 발생하고 Service 레이어에는 에러가 없는 경우 Service 레이어에서는 정상적으로 트랜잭션이 커밋되고 Listner 로직에서는 롤백이 되는 상황을 확인할 수 있습니다.
정리
기본적으로 EventListener를 통해 로직을 처리하려고 했던 이유는 메인이 되는 로직과 서브 로직을 분리하기 위함이었습니다. 따라서 동기적으로 이벤트 처리를 하지 않을 것이기 때문에 일반적으로 @Async 어노테이션 없이 사용할 때 트랜잭션까지 크게 고민하지는 않아도 될 것 같습니다.
그럼에도 EventListener가 어떤 방식으로 동작하는지 알 수 있게 되어서 의미가 있었던 테스트였습니다. 테스트를 진행하며 스프링에서 어떻게 트랜잭션을 다루는지 다시한번 복습할 수 있었습니다.
만약 동기적으로 EventListener를 사용한다고 하더라도 트랜잭션은 분리하여 사용하지 않을까 예상을 하기 때문에 트랜잭션 전파 방식에 유의해서 사용해야 할 것 같습니다.
마지막으로 이번 글에서는 ApplicationEventPublisher 클래스가 정확히 무엇인지, 어떻게 이벤트 발생 코드를 작성할 수 있는지 그리고 EventListener의 동작 방식과 트랜잭션과의 관계를 알 수 있었습니다.
다음 글에서는 이번장에서 미처 다루지 못한 EventListener 의 트랜잭션과의 관계에서 실행 시점을 설정하는 방법과, 전체 스레드 설정과 관련된 내용에 대해 알아보도록 하겠습니다.
직접 테스트 해보고 글을 작성하다보니 잘못된 정보가 있을 수 있습니다. 피드백은 언제나 환영하고 감사하게 생각하겠습니다.
'Backend > 학습내용 정리' 카테고리의 다른 글
JDBC 살펴보기 (1) 2024.09.12 Spring @EventListener 사용하기 (2) (0) 2024.07.19 트랜잭션과 JPA 낙관적 락 (0) 2024.06.17 Server Sent Event 정리 (0) 2024.05.16