Spring

[Spring Data] 도메인 이벤트 (AbstractAggregateRoot)

에드박 2022. 2. 1. 23:20

이 글은 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를 호출하면 여러개의 이벤트가 발행됩니다.