@EventListener vs @TransactionalEventListener

2024. 8. 31. 12:05JAVA/Spring

@EventListener

@EventListener는 스프링 이벤트 리스너를 등록하는데 사용되는 애노테이션입니다. 이 애노테이션을 적용한 메서드는 특정한 이벤트가 발생했을 때 수신하여 애노테이션을 적용한 메서드를 실행시킵니다. @EventListener 애노테이션을 적용한 리스너 메서드는 다음과 같은 특징을 가지고 있습니다. 앞으로의 설명에서 @EventListener 애노테이션을 적용한 메서드를 리스너 메서드라고 표현합니다.

  • 기본적인 동기적 처리 : 리스너 메서드는 기본적으로 이벤트를 동기적으로 처리합니다. 이벤트를 동기적으로 처리하게 되면 이벤트를 발생시킨 곳에서 이벤트를 호출하고나서 이벤트를 처리한 다음에 그 이후의 코드를 실행하게 됩니다.
  • 비동기 처리 가능 : @Async 애노테이션을 리스너 메서드에 같이 적용하면 해당 리스너 메서드를 비동기적으로 처리할 수 있습니다.
    • @Async 애노테이션을 활성화시키기 위해서는 @EnableAsync 애노테이션을 적용해야 합니다.
  • 조건 설정 : @EventListner의 condition 옵션을 설정하여 특정 조건에 따라 이벤트를 처리할지 여부를 지정할 수 있습니다.
    • condition 옵션에는 String 타입의 문자열을 전달하는데 SpEL(Spring Expression Language)를 전달합니다.
  • 다양한 이벤트 타입 처리 : 하나의 리스너 메서드에서 여러 타입의 이벤트를 처리할 수 있습니다.

@TransactionalEventListener

@TransactionalEventLister 애노테이션은 트랜잭션 이벤트 리스너를 등록하는데 사용됩니다. 이는 스프링의 트랜잭션 관리 기능과 통합되어, 트랜잭션이 성공적으로 완료된 후에만 이벤트를 처리하도록 설계되었습니다. @TransactionalEventListener 애노테이션을 적용한 메서드는 다음과 같은 특징을 가지고 있습니다.

  • 트랜잭션과의 통합 : @TransactionalEventListener 애노테이션을 사용하면 트랜잭션의 특정한 상태(ex, 커밋, 롤백)에 따라 이벤트를 처리할 수 있습니다.
  • 지연된 이벤트 처리 : 기본적으로 트랜잭션이 성공적으로 커밋된 후에 이벤트가 처리됩니다.
  • 트랜잭션 상태를 기준으로한 이벤트 발생 : phase 옵션을 사용하여 트랜잭션의 상태를 기준으로 이벤트를 실행시킬 수 있습니다.
    • AFTER_COMMIT (default) - 트랜잭션이 성공적으로 commit 되었을 때 이벤트 실행
    • AFTER_ROLLBACK – 트랜잭션이 rollback 되었을 때 이벤트 실행
    • AFTER_COMPLETION – 트랜잭션이 마무리 되었을 때(commit or rollback) 이벤트 실행
    • BEFORE_COMMIT - 트랜잭션의 커밋 전에 이벤트 실행
    • 단일 트랜잭션 내 이벤트 처리 : 트랜잭션이 완료된 후에만 이벤트를 처리하기 때문에, 트랜잭션이 실패하면 이벤트도 처리되지 않습니다.

@EventListener, @TransactionalEventListener 비교

  • 트랜잭션 의존성
    • @EventListener : 트랜잭션에 의존하지 않고, 이벤트가 발생하면 즉시 처리됩니다.
    • @TransctionalEventListener : 트랜잭션 결과에 따라서 이벤트가 처리됩니다. 트래잭션이 성공적으로 커밋된 후에 이벤트를 처리합니다.
  • 사용 목적
    • @EventListener : 일반적인 이벤트 처리에 사용되며, 트랜잭션 상태에 상관없이 이벤트를 처리할 수 있습니다.
    • @TransactionalEventListener : 트랜잭션이 성공적으로 완료된 후에만 이벤트를 처리해야 하는 경우에만 사용됩니다. 예를 들어, 데이터베이스 변경이 확정된 후에 후속 작업을 수행할 때 유용합니다.
  • 이벤트 처리 시점
    • @EventListener : 이벤트 발생 즉시(또는 비동기적으로) 처리됩니다.
    • @TransactionalEventListener : 트랜잭션 상태(ex, 커밋 이후, 롤백 이후) 이후에 처리됩니다.

@EventListener를 사용하여 회원가입 성공 메시지 예제 구현하기

회원가입을 수행하는 MemberService의 메서드를 다음과 같이 구현합니다.

@Transactional
public void signup(String name) {
	Member member = new Member(name);
	memberRepository.save(member);
	eventPublisher.publishEvent(new MemberSignupEvent(member.getName()));
	log.info("end signup service");
}
  • eventPulibsher의 타입은 ApplicationEventPublisher 타입으로써 publishEvent 메서드를 실행함으로써 이벤트를 발생시킵니다.

위와 같이 이벤트를 발생시켰을때 이벤트를 캐치할 리스너를 다음과 같이 구현합니다.

@Component
@RequiredArgsConstructor
@Slf4j
public class MemberSignupListener {

	private final SignupMessageService signupMessageService;

	@EventListener
	public void handleMemberSignupEvent(MemberSignupEvent event) {
		log.info("MemberSignupListener.handleMemberSignupEvent, event = {}", event);
		signupMessageService.sendSignupMessage(event.getName());
	}
}

  • SignupMessageService : 해당 서비스 객체의 역할은 이름을 인수로 받아서 회원에게 회원가입 성공 메시지를 전달하는 역할입니다.
  • @Async 애노테이션을 사용함으로써 handleMemberSignupEvent 메서드는 비동기적으로 수행됩니다.
  • ApplicationEventPublisher 객체가 publishEvent 메서드 실행시 인수에 MemberSignupEvent 객체를 전달하면 해당 리스너의 handleMemberSignupEvent 메서드가 실행됩니다.

위 이벤트 리스너를 테스트하기 위해서 테스트 코드를 다음과 같이 작성합니다.

@DisplayName("사용자는 회원가입하고 회원가입 메시지를 받는다")
@Test
void signup(){
    // given

    // when
    memberService.signup("kim");
    // then

}

실행 결과는 다음과 같습니다. 실행 결과를 분석해보면 우선 리스너 클래스의 handleMemberSignupEvent 메서드로 이동하여 로깅하는 것을 볼수 있고 SignupMessageService 객체에 의해서 회원가입 축하 메시지를 로깅하는 것을 볼수 있습니다. 마지막으로 MemberService의 signup 메서드 실행시 마지막에 실행이 종료되었음을 알리는 로깅하는 것을 볼수 있습니다.

16:58:10.798 [Test worker] INFO  nemo.event_listener.MemberSignupListener - MemberSignupListener.handleMemberSignupEvent, event = Member Event, name is kim
16:58:10.799 [Test worker] INFO  nemo.event_listener.ConsoleSignupMessageService - kim, congratulations on your membership.
16:58:10.801 [Test worker] INFO  nemo.event_listener.MemberService - end signup service

위 실행 결과와 같이 이벤트 리스너 메서드는 동기적으로 수행되었기 때문에 signup 메서드는 이벤트가 전부 처리되기 전까지 대기하는 것을 볼수 있습니다. 또한 @Async 애노테이션을 적용하지 않기 때문에 별도의 쓰레드 없이 동기적으로 수행됩니다.

이번에는 리스너 메서드에 @Async 애노테이션을 적용한 다음에 실행해보겠습니다.

@Async
@EventListener
public void handleMemberSignupEvent(MemberSignupEvent event) {
	log.info("MemberSignupListener.handleMemberSignupEvent, event = {}", event);
	signupMessageService.sendSignupMessage(event.getName());
}
17:11:46.476 [Test worker] INFO  nemo.event_listener.MemberService - end signup service
17:11:46.476 [task-1] INFO  nemo.event_listener.MemberSignupListener - MemberSignupListener.handleMemberSignupEvent, event = Member Event, name is kim
17:11:46.479 [task-1] INFO  nemo.event_listener.ConsoleSignupMessageService - kim, congratulations on your membership.
  • 실행 결과를 보면 로깅 내용은 동일하지만 로깅된 순서가 다른 것을 볼수 있습니다. @Async 애노테이션을 적용하였기 때문에 이벤트 리스너 메서드가 비동기적으로 수행되어 signup 메서드 실행 도중 이벤트를 발행했을때 대기하지 않고 바로 실행되어 “end signup service” 로깅이 먼저 찍힌 것을 볼수 있습니다.
  • 또한 이벤트 리스너 메서드가 비동기적으로 실행되었기 때문에 별도의 쓰레드(task-1)가 할당되어 실행되었습니다.

위 예제를 통하여 @EventListener 애노테이션을 이용하여 이벤트 리스너 메서드를 실행시킬수 있고 이 메서드 또한 @Async 애노테이션을 통하여 비동기적으로 실행시킬 수 있다는 것을 알게 되었습니다.

@TransactionalEventListener를 사용하여 회원가입 성공 메시지를 회원가입 이후에 저장하기

회원가입을 수행하는 MemberService.signup 메서드의 내용은 동일하며 달라진 부분은 리스너 메서드 부분입니다.

@Component
@RequiredArgsConstructor
@Slf4j
public class MemberSignupListener {

	private final SignupMessageService signupMessageService;

	@Async
	@TransactionalEventListener
	public void handleMemberSignupEvent(MemberSignupEvent event) {
		log.info("MemberSignupListener.handleMemberSignupEvent, event = {}", event);
		signupMessageService.sendSignupMessage(event.getName());
	}
}

  • 이전 예제와 달라진 부분은 @EventListner를 @TransactionalEventListener 애노테이션으로 변경한 점입니다.

SignupMessageService 인터페이스의 구현체인 ConsoleSignupMessageService의 내용은 다음과 같습니다. 간단하게 로깅할 메시지를 작성한 다음에 로깅을 수행하고 메시지는 저장소에 저장하는 방식입니다.

@Service
@RequiredArgsConstructor
@Slf4j
public class ConsoleSignupMessageService implements SignupMessageService {

	private final MemberSignupMessageRepository memberSignupMessageRepository;

	@Override
	@Transactional
	public void sendSignupMessage(String name) {
		String message = String.format("%s, congratulations on your membership.", name);
		log.info("message is {}", message);
		memberSignupMessageRepository.save(message);
	}
}

위 예제에 대한 테스트 코드는 다음과 같습니다.

@SpringBootTest
class MemberServiceTest {

	@Autowired
	private MemberService memberService;

	@Autowired
	private MemberRepository memberRepository;

	@Autowired
	private MemberSignupMessageRepository memberSignupMessageRepository;

	@DisplayName("사용자는 회원가입후 가입 축하 메시지를 받고 메시지는 저장된다")
	@Test
	void signup(){
	    // given

	    // when
	    memberService.signup("kim");
	    // then
		Assertions.assertThat(memberRepository.findByName("kim")).isNotNull();
		Assertions.assertThat(memberSignupMessageRepository.findAll()).hasSize(1);

	}
}

실행 결과는 다음과 같습니다. 실행 결과는 @EventListner의 결과와 동일합니다.

17:33:51.472 [Test worker] INFO  nemo.listener.transactional_event_listener.MemberService - end signup service
17:33:51.491 [task-1] INFO  nemo.listener.transactional_event_listener.MemberSignupListener - MemberSignupListener.handleMemberSignupEvent, event = Member Event, name is kim
17:33:51.498 [task-1] INFO  nemo.listener.transactional_event_listener.ConsoleSignupMessageService - message is kim, congratulations on your membership.

정리

  • @EventListener 애노테이션을 적용한 메서드는 이벤트 리스너 메서드로 작동하게 됩니다.
  • @EventListener를 적용하여 실행되는 이벤트 리스너 메서드는 기본적으로 동기적으로 실행되지만 @Async 애노테이션을 적용하면 비동기적으로 수행됩니다.
  • @TransactionalEveentListener 애노테이션을 적용한 이벤트 리스너 메서드는 트랜잭션의 특정한 상태에 따라 실행시킬 수 있습니다. 기본적으로 트랜잭션의 커밋 상태 이후에 실행됩니다.
  • @EventListener는 트랜잭션과 상관없이 이벤트를 실행시키고 싶을때 수행하고 이벤트를 즉시 실행시키고 싶을 때 사용할 수 있습니다. 반대로 @TransactionalEventListener는 트랜잭션 특정한 상태를 기준으로 해당 상태 이후에 이벤트를 실행시키고 싶을때 사용합니다.

'JAVA > Spring' 카테고리의 다른 글

SpringBoot 3.1 TestContainer  (0) 2024.05.13
SpringBoot 2.7 TestContainer  (0) 2024.05.13
MockMvc 사용시 LocalDateTime 배열 직렬화 문제 해결  (0) 2024.02.22
Spring Framework Filter 등록  (0) 2024.01.28
빈 생명주기 콜백  (0) 2023.05.10