[Spring Data] 도메인 이벤트 (AbstractAggregateRoot)
이 글은 Spring Data Common 에서 지원하는 도메인 이벤트를 정리하기 위한 글입니다.
우선 Spring 에서 지원하는 이벤트 관련 기능을 간단하게 알아보고 (혹시 스프링의 이벤트 관련해서 더 알아보고 싶은 분은 여기를 참고해주세요!)
다음으로 Spring Data Common 에서 지원하는 도메인 이벤트를 예시와 함께 알아보겠습니다.
Spring 에서 지원하는 이벤트 관련 기능
Spring은 이벤트 관련 기능을 지원해줍니다.
- ApplicationEventPublisher - 이벤트 발행자
- ApplicationEvent - 이벤트 객체
- ApplicationListener - 이벤트 리스너
- @EventListener
위의 기능을 활용하면 이벤트를 발행하고, 이벤트 리스너가 이벤트에 대한 처리를 담당할 수 있습니다.
먼저 간단하게 순서를 설명하면 아래와 같습니다.
1. ApplicationEvent(이벤트) 객체를 생성
2. 1에서 생성한 ApplicationEvent 객체를 ApplicationEventPublisher(이벤트 발행자)에게 발행을 요청.
3. 이벤트가 발행되면 ApplicationListener 가 이벤트를 처리
그럼 간단한 예시로 먼저 알아보겠습니다.
먼저 예시용 엔티티인 Post 클래스입니다. Post는 하나의 글을 의미합니다.
PostRepository도 함께 사용하겠습니다. (JpaRepository 사용)
@Entity
public class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Lob
private String content;
protected Post() {
}
public Post(String content) {
this.content = content;
}
... Getter
}
public interface PostRepository extends JpaRepository<Post, Long> {
}
여기까지 예시에서 사용할 도메인 객체를 만들었습니다.
다음으로 Post의 이벤트 객체를 생성합니다.
Post의 발행(저장)할 때의 이벤트를 만듭니다.
public class PostPublishedEvent extends ApplicationEvent {
private final Post post;
public PostPublishedEvent(Object source) {
super(source);
this.post = (Post) source;
}
public Post getPost() {
return post;
}
}
ApplicationEvent를 상속받으며 필드로 발행된 Post를 가집니다.
다음으로 이벤트를 받아서 처리하는 이벤트 리스너를 만들겠습니다.
이벤트 리스너는 두 가지 스타일로 만들 수 있습니다. (스프링 버전에 따라 애너테이션 방식은 안될 수 있습니다.)
- 생성한 Listener 클래스가 ApplicationListener 를 implements(구현) 한다.
- 이벤트 처리 메서드에 @EventListener 애너테이션을 추가한다.
아래 예시에서 두 가지 방법을 모두 보시고 마음에 드시는 방법을 사용하시면 됩니다!
단, 두 가지 방법 모두 빈으로 등록되어야 합니다. @Component 또는 설정 클래스(@Configuration)에서 빈으로 등록해주셔야 합니다.
@Component
public class PostListener {
@EventListener
public void onApplicationEvent(PostPublishedEvent event) {
System.out.println("======================");
System.out.println("Post 이벤트 발행, Post Id = " + event.getPost().getId() + ", Content = '" + event.getPost().getContent() + "'");
System.out.println("======================");
}
}
Post의 이벤트 객체와 이벤트 리스너를 모두 만들었으니 이벤트를 발행하고 처리하는 테스트를 작성해보겠습니다.
@SpringBootTest
class PostEventTest {
@Autowired
ApplicationEventPublisher applicationEventPublisher;
@Test
void eventListener() {
Post post = new Post("hello world");
PostPublishedEvent event = new PostPublishedEvent(post);
applicationEventPublisher.publishEvent(event);
}
}
// 실행결과
// ======================
// Post 이벤트 발행, Post Id = null, Content = 'hello world'
// ======================
여기까지 스프링에서 지원하는 이벤트 관련 기능 예시였습니다.
Spring Data 에서 지원하는 이벤트 자동 발행 기능
Spring Data에서는 Repository에서 save할 때 이벤트 자동 발행 기능을 제공합니다.
AbstractAggregateRoot를 사용해서 구현할 수 있습니다.
AbstractAggregateRoot를 사용하면 다음과 같은 것들이 가능합니다.
- Post 객체를 저장 시(save 메서드 호출 시) 이벤트를 발행, 처리할 수 있다.
- Post 객체에 이벤트 객체를 여러개 저장해두고 저장 시(save 메서드 호출 시) 여러개의 이벤트를 발행, 처리할 수 있다.
- 이벤트 발행 후 모아놨던 모든 이벤트를 제거한다. (메모리 누수 차단)
AbstractAggregateRoot의 내부에 대해서 좀 더 자세히 알아보겠습니다.
- List<Object> domainEvents - 이벤트 객체를 모아놓는 필드입니다.
- <T> T registerEvent(T event) - domainEevents 에 이벤트를 추가하고 추가한 이벤트를 반환합니다.
- A andEvent(Object event) - domainEevents 에 이벤트를 추가하고 현재 엔티티 객체(Aggregate)를 반환합니다.
- clearDomainEvents() - 이벤트 발행 후 모아놨던 모든 이벤트를 제거합니다.
- domainEvents() - 현재 쌓여있는 모든 이벤트(domainEvents)를 반환합니다.
이해가 가지 않는다면 예시와 함께 봐주세요!
엔티티 클래스가 AbstractAggregateRoot를 상속하도록 합니다.
@Entity
public class Post extends AbstractAggregateRoot<Post> {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Lob
private String content;
protected Post() {
}
public Post(String content) {
this.content = content;
}
// 이벤트 추가 후 자기 자신을 반환
public Post publish() {
return this.andEvent(new PostPublishedEvent(this));
}
... Getter
}
publish() 메서드를 보면 이벤트를 추가합니다.
이제 해당 Post 객체를 save() 메서드로 영속화할 때 이벤트가 발행됩니다.
@SpringBootTest
class PostEventTest {
@Autowired
PostRepository postRepository;
@Test
void domainEvent1() {
Post newPost = new Post("hello world");
postRepository.save(newPost.publish());
System.out.println("newPost 저장!!!");
Post newPost2 = new Post("hello charlie!!");
postRepository.save(newPost2.publish());
System.out.println("newPost2 저장!!!");
}
}
// 실행 결과
// ... insert 쿼리 생략
// ======================
// Post 이벤트 발행, Post Id = 1, Content = 'hello world'
// ======================
// newPost 저장!!!
// ... insert 쿼리 생략
// ======================
// Post 이벤트 발행, Post Id = 2, Content = 'hello charlie!!'
// ======================
// newPost2 저장!!!
다음의 순서로 이벤트가 발행, 처리됩니다.
1. 이벤트 객체(PostPublishedEvent)를 생성, 엔티티 객체의 domainEvents 에 추가합니다. (Post의 publis() 메서드에서 일어남)
2. Repository에서 해당 엔티티 객체로 save 메서드를 실행할 때, domainEvents의 모든 이벤트 발행(정확히는 데이터가 영속화 된 후)
3. EventListener(PostListener) 에서 이벤트를 처리.
그럼 여러개의 이벤트를 추가해놓으면 Repository의 save 메서드 호출시 한번에 이벤트가 발행될까요?
@Test
void domainEvent2() {
Post newPost = new Post("hello world");
newPost.addPublishEvent();
newPost.addPublishEvent();
System.out.println("newPost save 메서드 호출!!!");
postRepository.save(newPost);
System.out.println("newPost 저장!!!");
}
// 실행결과
// ... insert 쿼리 생략
// ======================
// Post 이벤트 발행, Post Id = 1, Content = 'hello world'
// ======================
// ======================
// Post 이벤트 발행, Post Id = 1, Content = 'hello world'
// ======================
// newPost 저장!!!
위 예시처럼 여러개의 이벤트를 넣어 놓고 save를 호출하면 여러개의 이벤트가 발행됩니다.