Spring Data JPA - 도메인 이벤트

Spring Data JPA

백기선님의 강의인 Spring Data JPA 강의를 듣고 공부한 내용을 정리한 글

도메인 이벤트 퍼블리싱?

@Entity로 정의해둔 객체를 도메인이라고 부른다.

이러한 도메인의 변화를 감지하여 이벤트로 발생시키고 이벤트 리스너가 이 이벤트를 감지하게 하는 것이다.

놀랍게도(?) 이벤트 기반의 프로그래밍을 스프링에서 지원해주고 있기 때문에 이러한 도메인 관련 이벤트를 발생시키고 처리하는 것도 가능하다.

예제

직접 이벤트를 발생시키고 리스너가 받는 예제를 만들어보자.

이벤트 발행을 위해 이벤트를 발행하는 클래스를 만든다.

class PostPublishedEvent(
        source: Any
) : ApplicationEvent(source) {

    val post = source as Post

}

이 클래스는 ApplicationEvent라는 추상 클래스를 상속받아 만든다.

이 이벤트 발행 객체의 생성자로 Post 객체를 전달해주면 된다.

그리고 이 발행한 이벤트를 귀기울고 있을 리스너를 만들어준다.

class  PostListener: ApplicationListener<PostPublishedEvent> {

    override fun onApplicationEvent(event: PostPublishedEvent) {
        println("---------------------------------")
        println("${event.post} is published!!!")
        println("---------------------------------")
    }
}

이 리스너는 앞서 만든 PostPublishedEvent 타입을 리쓴하고 있게 된다.

그리고 해당 이벤트가 발행되면 이를 알려주기 위해 stdout으로 찍어보았다.

참고로 으레 스프링에서 관리되는 기능들이 그렇듯 이 객체는 빈으로 등록이 되어야 사용할 수 있기 때문에 빈으로 등록을 해주어야한다.

@Configuration
class PostRepositoryTestConfig {

    @Bean
    fun postListener(): PostListener {
        return PostListener()
    }
}

준비는 끝났다 테스트 코드를 작성하고 테스트해보자.

    @Autowired
    lateinit var applicationContext:ApplicationContext

    @Test
    fun event() {
        val post = Post()
        post.title = "event"
        val event = PostPublishedEvent(post)
        applicationContext.publishEvent(event)
    }
---------------------------------
Post(id=null, title=event content= created=2020-06-21T21:12:07.259703) is published!!!
---------------------------------

그럼 요로코롬 출력이 잘 되는 것을 확인할 수 있다.

그럼 Spring Data JPA는 뭘하는데?

방금은 Spring Framework에서 제공해주는 이벤트 리스너의 예제였다.

그럼 지금부터는 Spring Data JPA에서 제공하는 기능에 대해서 살펴보도록 하자.

Spring Data JPA의 경우 이벤트 자동 퍼블리싱 기능을 제공해준다. (와-우!)

이 이벤트 발행은 레포지토리에 save()할 때 그동안 쌓여있던 이벤트들을 한번에 보내주게 된다.

이벤트를 모아놓는 곳이 @DomainEvents라는 애노테이션을 갖고 있는 메서드이고,

이벤트 발행 후 쌓여있던 이벤트들을 다 비우는데 사용하는 것이 @AfterDomainEventPublication이다.

비우는 이유는 알다시피 컬렉션 같은 곳에 이벤트를 쌓아놨을거니까 이벤트가 발행되고 나서 비워주어야 메모리 릭을 발생시키지 않을 수 있다.

이러한 스프링 데이터의 도메인 이벤트 퍼블리셔를 위의 애노테이션을 사용해서 직접 구현할 수도 있지만,

스프링 데이터에서 이미 제공하고 있는 AbstractAggregateRoot가 이미 있으니까 이걸 사용하도록 하자.

AbstractAggregateRoot의 내부를 살펴보면 앞서 언급한 애노테이션들이 이미 다 구현되어있는 것을 확인할 수 있다.

구현

@Entity
class Post (
        @Id @GeneratedValue
        val id: Long? = null,
        var title: String = "",
        @Lob
        val content: String = "",

        val created: LocalDateTime = LocalDateTime.now()

):AbstractAggregateRoot<Post>() {
    @OneToMany(mappedBy = "post", cascade = [CascadeType.ALL])
    val comments: MutableSet<Comment> = HashSet()

    fun addComment(comment: Comment) {
        this.comments.add(comment)
        comment.post = this
    }


    override fun toString(): String {
        // toString이 찍을 때 comment도 가져옴. 이게 문제되는 이유는 하이버네이트가 쿼리로 comment를 가져옴 (쓰잘데기 없는 일을 함)
        return "Post(id=$id, title=$title content=$content created=$created)"
    }

    fun publish(): Post {

        this.registerEvent(PostPublishedEvent(this))

        return this
    }
}

이 코드에서 주요하게 살펴봐야할 부분은 AbstractAggregateRoot 클래스와 publish() 메소드이다.

잘보면 registerEvent(AbstractAggregateRoot의 메소드)를 사용해 이벤트를 등록해주기만 하면 된다.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(PostRepositoryTestConfig::class)
class  PostRepositoryTest {

    @Autowired
    lateinit var postRepository:PostRepository

    @Test
    fun crud() {
        val post = Post()
        post.title = "hibernate"
        postRepository.save(post.publish())

        // insert, select 쿼리는 동작한다. 왜? 현재 트랜잭션에서 select로 가져오려면 insert된 데이터가 필요하니까.
        postRepository.findMyPost()

        // 여기서 delete Query를 별도로 날리지 않는 이유는 위 트랜잭션을 그냥 롤백하면 되니깐?
        // 하이버네이트의 경우 불필요한 쿼리는 아예 날리질 않는다.
        postRepository.delete(post)

        // flush를 하면 강제적으로 delete 쿼리를 날릴 수 있다.
        postRepository.flush()
    }
}

그리고 테스트 코드에서 DB에 저장할 때 publish() 메소드를 호출하게 하면 된다.

참고로 리스너를 만들 때도 상속이 아닌 애노테이션으로도 리스너 등록이 가능하다.

이 기능은 스프링 프레임워크에서 제공해주는 기능이다.

class  PostListener {

    @EventListener
    fun onApplicationEvent(event: PostPublishedEvent) {
        println("---------------------------------")
        println("${event.post.title} is published!!!")
        println("---------------------------------")
    }
}

Reference

인프런 백기선님의 스프링 Data JPA



© 2022. by minkuk

Powered by minkuk