![[Spring] Spring에서 이벤트 발행과 구독](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBgKPH%2FbtsN7U5OWdL%2FZGikRnpbvVJoOKkcV5gu30%2Fimg.png)
❓이벤트(Event)와 Spring Boot 이벤트의 이해
먼저 프로그래밍에서 이벤트란, 애플리케이션 내에서 발생시킬 수 있는 어떤한 사건이나 상태 변경 등을 의미한다. 애플리케이션에는 어떠한 이벤트를 바생시키는 주체와 정해진이벤트의 발생을 탐지해 동작을 처리하는 주체가 존재할 수 있다.
Spring Boot는 이벤트 처리를 위해 ApplicationEvent 클래스와 ApplicationEventPublisher 인터페이스를 제공한다.
Spring에서 발생하는 모든 이벤트는 ApplicationEvent 클래스를 상속하여 정의되며, 이벤트를 발생시킬 때 전달되고, ApplicationEventPublisher는 이벤트를 발생시키고 이벤트를 구독하는 리스너에게 전달하는 역할을 합니다. Spring Context에서 주입하여 사용할 수 있으며, publishEvent() 메서드를 통해 이벤트를 발행할 수 있다.
언제 이벤트를 사용할까 ?
개발을 하는 과정에서 어느 시점에서 Spring Boot 이벤트를 적용할 수 있을지 알아보자.
먼저 특정 동작을 수행하는 과정에서 그 동작과는 개별적으로 추가 수행을 해야하는 경우, 그 외적인 동작을 처리하기 위한 이벤트를 발생 시키고, 발생한 이벤트를 처리하는 로직에서 필요한 동작을 수행할 수 있다.
🚀 Spring Boot Event 적용하기
- [SNS] A사용자가 B사용자를 팔로우 중
- B사용자가 새로운 게시글 업로드
- A사용자에게 B사용자의 게시글 업로드 되었음을 알림 발행 후 전달
🧑🏻💻 주요 코드 설명
PostEvent
public record PostEvent(Post post) {}
Event Publisher
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class DefaultPostService implements PostService {
private final ApplicationEventPublisher eventPublisher;
@Overrid
@Transactional
public void addPost(requestDto request) {
// ...게시글 등록 로직
// savePost = 저장한 게시글 정보
PostEvent postEvent = new PostEvent(savePost);
eventPublisher.publishEvent(postEvent);
}
}
NotificationEventListener
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationEventListener {
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void issueNotify(PosEvent postEvent) {
Thread.sleep(5000);
// ... 발송 로직 추가
log.info("[알림이 발송 완료] : {}", LocalDateTime.now());
}
}
- 기본적으로 Spring Event는 동기 방식으로 작동하지만 예제 코드는 비동기 방식으로 구현했다.
- 비동기 처리는 @Async와 ApplicationMain클래스에 @EnableAsync를 사용하면 된다.
- 비동기 결과 확인을 위해 이벤트 발행 시 쓰레드를 5초 sleep한 뒤 결과를 확인했다.
API 반환 시간은 32초, 알림 발송은 37초에 발송된 것을 확인할 수 있다.(시간은 UTC 반영으로 인해 오차가 있다.) - 기본적으로 동기 방식은 하나의 트랜잭션의 범위로 묶을 수 있으나, 비동기 방식으로 동작할 경우 이벤트 발행 전의 로직과 후의 로직이 하나의 트랜잭션으로 묶일 수 없다.
따라서 정밀하게 트랜잭션을 사용하는 경우라면 꼭 이러한 부분을 고려해봐야한다. - Spring 4.2부터는 @EventListener을 확장한 @TransacitionEventListener를 제공하고 있으며, 아래와 같은 옵션이 존재한다.
예제의 경우 TransactionPhase.AFTER_COMMIT를 사용했다.- TransactionPhase.AFTER_COMMIT : 트랜잭션이 커밋되기 직전에 실행
- TransactionPhase.AFTER_COMPLETION : 트랜잭션 커밋 완료 후 실행(성공 시)
- TransactionPhase.BEFORE_COMMIT : 트랜잭션 롤백 완료 후 실행
- TransactionPhase.AFTER_ROLLBACK : 트랙잭션 커밋 또는 롤백 완료 후 실행
👀 Spring Event 동작 원리 알아보기
Spring Event에 대한 참고 자료를 여럿 찾아보던 중 Application Context가 왜 지속적으로 언급되는지 궁금했다
상단의 이미지를 보면 ApplicationEventPulisher의 이벤트를 발행한 뒤 ApplicationEventMulticaster에 의해 구독된 이벤트 리스너들을 호출한다. 상세하게 코드를 따라가보자
1. ApplicationContext를 보니 ApplicationEventPublisher를 상속 받고 있었고, ApplicationContext를 주입 받아 publishEvent메서드를 사용할 수 있던 것이였다. 하지만 명시적으로 ApplicationEventPublisher를 주입받는 것이 더 명확하고, 다른 개발자들도 이해하기 쉽고, ApplicationContext 주입 시 과도한 권한(컨텍스트 전체 접근)을 방지할 수 있다.
2. AbstractApplicationContext는 ApplicationContext 인터페이스의 구현체로 이벤트를 발행할 때 사용했던 publishEvent의 구체적인 로직을 구현한다. 일반 객체를 ApplicationEvent로 타입을 변환하고, ApplicationEventMulticaster를 통해 이벤트 브로드캐스팅(동시 전달)을 한다.
3. SimpleApplicationEventMulticaster에 구현되어있는 multicastEvent를 통해 등록된 모든 리스너를 찾아 이벤트를 전파한다.
ApplicationListener 빈 중에서 이벤트 타입과 @EventListener의 조건을 만족하는 리스너만 선택되어 호출한다.
4. 이벤트 리스너들이 호출되는 과정으로 에러가 발생하지 않는다면, listener.onApplicationEvent를 호출하여 이벤트를 실행한다.
📝 정리와 느낀점
Spring Event를 사용했을 때의 장점은 두 도메인 간의 의존성을 완전히 분리할 수 있다는 것이다. 서로 다른 도메인끼리 존재를 몰라도 되기 때문에 느슨하게 결합되고, 재사용성도 높아진다.
위 예제에서 Spring Event를 사용하지 않고 하나의 서비스에서 모두 처리했을 경우 알림 발송에 실패하면 분명 게시글 작성도 Rollback 될 것이다. 궁금했던 부분을 해소하려 하나하나 디버깅하면서 많은 공부를 했고, 오늘도 나의 자산이 생겼다 !
'프로그래밍 > Java' 카테고리의 다른 글
[Java, Spring] 코드 문서화를 위한 주석 규칙 알아보기 (0) | 2024.10.31 |
---|---|
[Spring, SpringBoot] 다중 DB 연동하기 (1) | 2024.10.08 |
[Java, Spring] Linux 파일 서버 경로 연결하기 (5) | 2024.10.01 |
[Spring, Swagger] Swagger 적용하기 및 기본 경로 변경하기 (2) | 2024.09.21 |
[Java, 자바] Mybatis Map foreach로 반복하기 (0) | 2024.09.10 |
개발의 모든 것 !
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!