회사 프로젝트를 진행하면서 알림 기능을 구현하는 과정 안에서 불편함을 느꼈다. 알림 기능은 부가적인 기능이라고 생각한다. 쉽게 생각하면 특정 로직 이후 그것에 대한 이벤트 처리라고 생각한다.
ex) 회원 가입 후 → 회원 가입 알림 , 게시글 작성 후 → 게시글 작성 알림
그래서 항상 비즈니스 로직 사이에 껴있어야하고, 알림 객체를 DI 받아야 하기에 객체는 알림 객체에 의존적일 수밖에 없었다. 즉, 강결합 문제가 생겼다.
이 문제를 해결하기 위해 해결방법을 찾아보았고 스프링에서 제공해주는 EventListener를 통해 해결할 수 있었다.
예제 코드
@Service
@RequiredArgsConstructor
@Slf4j
public class EventService {
private final ApplicationEventPublisher publisher; // 1
@Transactional()
public void event() throws Exception {
log.info("메서드 시작");
log.info("특정 로직 시작");
publisher.publishEvent(new EventDto("나는 이벤트야"));
log.info("메소드 종료");
}
}
ApplicationEventPublisher 는 Spring의 ApplicationContext가 상속하는 인터페이스 중 하나이다. ApplicationEventPublisher 통해 이벤트를 발생시킬 수 있다.
DTO
@Getter
@NoArgsConstructor
public class EventDto {
private String message;
public EventDto(String message) {
this.message = message;
}
}
이벤트를 운반해 주는 DTO이다. ApplicationEventPublisher.publishEvent(Object event) 이기에 Object 이라면 모두 가능하다.
이벤트 핸들러
@Component
@Slf4j
public class EventHandler {
@EventListener
public void event1(EventDto eventDto) { // 1
log.info("이벤트 시작");
log.info("이벤트 : [{}]", eventDto.getMessage());
log.info("이벤트 종료");
}
@EventListener
public void event2(String eventDto) {
log.info("이벤트 시작");
log.info("이벤트 종료");
}
}
이벤트를 받아서 처리해주는 객체이다. Bean으로 등록하는걸 까먹지 말자.
여기서 신기한 점은 `@EventListener` 가 하나가 아니라 여러개 일 때 스프링에서 파라미터 `EventDto` 에객체를 찾아 해당 이벤트를 실행해준다는 점이다. 잊지 말고 동작원리에 대해 공부하도록 하자.
EventListener 성공 흐름 알아보기
EventService.event() → ApplicationEventPublisher.publishEvent() → eventDto → eventHandler
로직을 실행하게 되면 로그를 통해 예상 로직 순서는 생각해보자
- 메서드 시작
- 특정 로직 시작
- 이벤트 시작
- 이벤트 : 나는 이벤트야!
- 이벤트 종료
- 메서드 종료
[결과]

예상한 대로 결과가 나왔다.
EventListener 실패 흐름 알아보기
비즈니스 로직 도중 예외가 터진다면 이벤트는 어떻게 될까? 예상하겠지만 이벤트는 그대로 동작한다.
@Transactional()
public void event() throws Exception {
log.info("메서드 시작..");
log.info("특정 로직 시작..");
publisher.publishEvent(new EventDto("나는 이벤트야"));
throw new Exception();
// 그다음 로직..
}
[결과]

예를 들어 회원 가입 도중 예외는 터졌으나 회원 가입 알림은 실행돼버리는 구조가 될 것이다. 유저에게 혼란을 가중시킬 수 있다.
해결방법은?
@TransactionalEventListener 어노테이션을 통해 트랜잭션의 어떤 타이밍에 이벤트를 발생시킬지 정할 수 있다.
옵션은 크게 4가지로 분류된다. 본인은 기본값을 사용할 것이다.
- AFTER_COMMIT (기본값) - 트랜잭션이 성공적으로 마무리(commit)됐을 때 이벤트 실행
- AFTER_ROLLBACK – 트랜잭션이 rollback 됬을 때 이벤트 실행
- AFTER_COMPLETION – 트랜잭션이 마무리 됬을 때(commit or rollback) 이벤트 실행
- BEFORE_COMMIT - 트랜잭션의 커밋 전에 이벤트 실행
3. TransactionalEventListener 성공 흐름 알아보기
EventListener와 다르게 트랜잭션 타이밍을 지정하여 이벤트를 동작시킬 수 있다.- Service 단에서 이벤트를 사용할 메서드에 트랜잭션을 달아주도록 하자.
@Transactional
public void event() throws Exception{
// ..
}
이벤트 핸들러에서 @TransactionalEventListener 바꿔주도록 하자.
@TransactionalEventListener
public void event(EventDto eventDto) {
log.info("이벤트 시작");
log.info("이벤트 : [{}]", eventDto.getMessage());
log.info("이벤트 종료");
}
트랜잭션이 끝난 후 이벤트가 발생되기 때문에 예상 로직 순서를 이렇게 될 것이다.
- 메소드 시작
- 특정 로직 시작
- 메소드 종료..
- 이벤트 시작
- 이벤트 : 나는 이벤트야!
- 이벤트 종료
[결과]

예상한 대로 결과를 확인할 수 있었다.
4. TransactionalEventListener 실패 흐름 알아보기
트랜잭션이 실패하여 Rollback이 된다면 이벤트는 아예 실행되지 않을 것이다. 테스트를 해보자.
@Transactional(rollbackFor = Exception.class)
public void event() throws Exception {
log.info("메서드 시작..");
log.info("특정 로직 시작..");
publisher.publishEvent(new EventDto("나는 이벤트야"));
log.info("메소드 종료..");
throw new Exception();
}
강제로 Exception을 만들어 Exception을 만나면 트랜잭션 롤백을 하도록 설정해 주었다.
예상 로직 순서이다.
- 메서드 시작..
- 특정 로직 시작..
- 메서드 종료..
[결과]

이벤트가 아예 동작하지 않는 것을 확인할 수 있다.
정리
스프링에서 제공해 주는 EventListener 를 통하여 객체 간의 결합도를 낮출 수 있었다. 또한 더 나아가 트랜잭션 타이밍과 결과에 따라 이벤트에 동작유무를 정해줄 수 있는TransactionalEventListener 통하여 결과에 맞게끔 이벤트를 동작을 컨트롤 할 수 있다.
참고 자료
TransactionalEventListener 이용 시 insert, update, delete 안 되는 현상
Spring Boot 이벤트핸들링 과 @TransactionalEventListener, @Transactional
[Spring] Spring의 Event를 어떻게 사용하는지에 대해서 알아봅시다. - @TransactionalEventListener에 대해서
ApplicationEventPublisher 기반으로 강결합 및 트랜잭션 문제 해결 - Yun Blog | 기술 블로그
'SpringBoot' 카테고리의 다른 글
| OpenFeign 응답값 받아오는 방법 (0) | 2023.04.21 |
|---|---|
| SpringBoot + S3 연동하여 이미지 올리기 (2) | 2023.04.04 |
| 스프링 예외 처리 Guide (0) | 2023.04.04 |
| SpringBoot + STOMP 웹소켓 고도화 (0) | 2023.04.03 |
| SpringBoot + STOMP 웹소켓 구현 (0) | 2023.04.03 |