🎯 이벤트 기반 아키텍처에 대해 알아보자
프로그래밍에서 이벤트란, 애플리케이션 내에서 발생할 수 있는 사건입니다. 애플리케이션에는 어떠한 이벤트를 발생시키는 주체와, 정해진 이벤트의 발생을 탐지해 동작을 처리하는 주체가 필요합니다.
그렇다면 언제 이벤트를 발생시킬까? 만약 어떤 데이터를 저장하고 사용자에게 알림 보내는 기능을 만드는 과정을 생각해봅시다. 즉, 어떠한 동작(데이터 저장) 과는 별개로 추가적으로 수행해야 하는 외적인 동작(알림 보내기) 을 처리할때 유용하게 사용할 수 있습니다.
게시글을 저장하고 알림을 보내는 기능
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final NoticeService noticeService;
@Transactional
public void save(PostRequestDto postRequestDto){
postRepository.save(postRequestDto);
noticeService.pushAlarm(postRequestDto);
}
}
해당 코드의 문제점은 다음과 같습니다.
1. Post 를 생성하는 메서드가 총 2가지 책임을 가지고 있습니다. (게시글 저장, 푸쉬 알림 전송)
2. 핵심 로직(게시글 저장)이 아닌 부수적인 로직(푸쉬 알림) 이 핵심 로직에 영향을 끼칩니다.
이런 문제점을 개선하기 위해 다음과 같이 개선해야 합니다.
Event 기반 동작 방식으로 변경된 코드
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void save(PostRequestDto postRequestDto){
postRepository.save(postRequestDto.toEntity());
eventPublisher.publishEvent(PostPublishedEvent.from(postRequestDto));
}
}
@Getter
@Builder
public class PostPublishedEvent {
private Long userId;
private String title;
public static PostPublishedEvent from(PostRequestDto postRequestDto){
return PostPublishedEvent.builder()
.userId(postRequestDto.getUserId())
.title(postRequestDto.getTitle())
.build();
}
}
ApplicationEventPublisher 에서 제공하는 publishEvent() 메서드를 이용해 이벤트를 발생시키고 있습니다. 즉 PostPublishedEvent 라는 클래스의 from 정적 팩토리 메서드를 호출하며 이벤트를 발생시키고 있습니다.
즉, 이전 코드와 비교했을때 명시적으로 어떠한 행위를 지정해주는 선언형(알림을 보내!) 방식에서 행위에 대해 호출하는 방식(이벤트만 호출) 으로 변경되었습니다.
이전 코드의 문제점이였던
1. Post 를 생성하는 메서드가 총 2가지 책임을 가지고 있습니다. -> 분리 성공
2. 핵심 로직(게시글 저장)이 아닌 부수적인 로직(푸쉬 알림) 이 핵심 로직에 영향을 끼칩니다. -> 이벤트만 발행하므로 핵심로직에 영향 x
이 두가지를 자연스럽게 해소시킬 수 있습니다. 그렇다면 발행된 이벤트에 대한 반응을 해야겠죠? ApplicationEventPublisher 의 publish() 메서드를 통해 이벤트를 발행시키기 위해서는 이벤트를 위해 별도로 정의한 객체를 전달해야 합니다. 위 코드에서는 PostPublishedEvent 가 그 객체가 되겠네요.
Event 수신
@Component
public class PostEventListener {
@EventListener
public void onApplicationEvent(PostPublishedEvent event) {
// ... 게시글 등록 푸쉬 알림 로직
}
}
이렇게 발행된 Event 는 EventListener 객체가 수신합니다. Spring 에서 EventListener 는 Bean 으로 등록되어야 하며, 특정 이벤트가 발생한 경우 EventListener 객체 내에서 @EventListener 어노테이션을 부여받고 발행된 Event 객체를 인자로 받는 메서드가 동작하는 콜백 방식으로 호출이 이루어집니다.
즉 이벤트를 발행한 로직과 이벤트 핸들러의 로직이 병렬적으로 실행되는게 아니고, 이벤트가 발행되면 발행했던 쪽 로직을 멈추고 이벤트 핸들러의 로직이 실행되고 끝날대까지 기다렸다 원래 로직이 마저 실행됩니다.
하지만 지금 리스너에서 처리하는 기능은 게시글 작성 푸쉬 알림 기능입니다. 게시글이 저장되고 푸쉬 알림을 안정적으로 보낼때까지 기다릴 필요가 없는거죠 -> 비동기로 전환
@Async 애노테이션을 붙이면 간단하게 비동기 방식으로 변환할 수 있답니다.
@Component
public class PostEventListener {
@Async
@EventListener
public void onApplicationEvent(PostPublishedEvent event) {
// ... 게시글 등록 푸쉬 알림 로직
}
}
만약, 기존 메서드 로직과 다른 트랜잭션이 엮이 있다면 어떻게 될까? 현재는 게시글이 등록됐다는 푸쉬 알림만을 보내는데, 푸쉬 알림에 대한 정보를 DB 에 저장해야 하는 상황을 가정해봅시다.
Post(게시글) 객체와 Notice(알림) 객체를 DB 에 저장해야 하는데, Post 만 저장되고 Notice 는 저장되지 않을겁니다. 이는 Post Entity 는 생성되어 영속되었지만, Notice Entity 는 영속되지 않기 때문입니다. 이 이유는 @TransactionalEventListener 에서 직접 확인할 수 있습니다.

EventListener 로직을 수행할 때 Event 를 발행한 시점의 트랜잭션은 커밋되거나 롤백이 마쳐진 상태지만, 트랜잭션 자체는 여전히 활성화 되어 있는 채로 EventListener 로직이 수행됩니다. 그렇다면 EventListener 로직에 생성되는 트랜잭션에 대해 별도의 후처리를 하지 않는 이상 해당 트랜잭션은 별개의 트랜잭션으로 실행되기 때문에 기존 트랜잭션에 합쳐집니다. 그런데 합쳐지는 트랜잭션은 이미 커밋이 끝난 상태이므로 EventListener 로직에서 수행되는 Entity 의 생성이나 조작이 무시되는 겁니다.
이러한 문제를 해결하기 위해 EventListener 에서 수행하는 로직의 @Transactional 애노테이션에 propagation 속성으로 값을 부여해야 합니다.
@Component
@Trsnactional(propagation=Pro
public class PostEventListener {
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onApplicationEvent(PostPublishedEvent event) {
// ... 게시글 등록 푸쉬 알림, 알림 데이터 저장 로직
}
}
위와 같이 @Transactional 속성에 Propagation.PEQUIRES_NEW 속성을 추가하면 이벤트 리스너 로직은 독립적인 트랜잭션으로 실행되기 때문에, 같이 영속되지 않습니다. 즉 서로 영향을 주지 않게 되어 데이터를 안정적으로 저장할 수 있습니다.
'Spring' 카테고리의 다른 글
| Outbox 패턴을 이용한 결제 원자성 보장 (0) | 2025.12.21 |
|---|