CH05 도메인 이벤트
Last updated
Last updated
위 그림은 에서 다뤘던 이벤트 스토밍이다. 여기서 주황색이 이벤트인데, 도메인 내에서 여러 이벤트가 발생하고 있는 것을 확인할 수 있다.
뿐만 아니라 DDD 세레나데 전반에서 진행된 여러 미션에서도 많은 종류의 이벤트들이 발생했는데, 이것을 코드로 구현하진 않았다. 이벤트로서의 의미보다는 절차지향적으로 코드를 구성했다.
이번 시간에는 실제로 도메인 비즈니스 규칙이 이벤트 발생 -> 이벤트 처리의 형태로 이뤄지는 만큼 코드도 동일한 구조로 설계하는 것을 다뤘다.
먼저 강의에서는 이 형태가 최종 단계라던가 궁극적으로 옳은 것이라는 것은 아니라는 것을 짚고 넘어갔다. 이벤트도 그저 ‘패턴’ 중의 하나이며 이러한 이벤트 패턴을 적용하지 않아도 DDD 에 입각한 설계는 얼마든지 가능하다고 했다.
하지만 이러한 이벤트 기반의 패턴을 사용하면 여러 장점이 있기 때문에 이론적으로 이벤트 패턴의 장점을 학습해보고 실제로 코드로도 구현해보고자 한다.
강의에서는 회원 가입시 외부 서비스를 이용해서 축하 메세지를 보내는 것을 이벤트 패턴을 적용할 예시로 들었는데 아주 적절한 예시라는 생각이 들었다.
위 상황은 UserService가 회원가입 처리를 끝내고 이메일과 sms로 축하메세지를 보내는데 요구사항으로 카카오와 라인이 추가가 된 경우이다.
이 때 카카오나 라인이 추가되지 않고 이메일과 sms만 보내는 상황이라고 해도 생각해볼 만한 여러 문제점들이 있다. 강의 자료에 잘 정리가 되어 있어서 그대로 옮긴다.
이 중 제일 와닿는 문제점은 첫번째 문제점이다. 물론 UserService의 책임을 ‘전송시도’ 자체로 보면 외부 서비스가 400을 주든 500을 주든 제대로 전송 요청만 하면 된다는 기준으로 보면 트랜잭션 처리는 쉽다. 그리고 전송 결과를 저장해뒀다가 나중에 200이 아닌 경우를 배치로 재발송 요청을 하던가 하면 된다.
하지만 이런 상황이면 특히 비동기 이벤트를 사용한다면 UserService가 의존하고 있는 외부 서비스들에 대한 의존도 및 결합도를 구조적으로 크게 낮춰줄 수 있다.
위 이미지는 강의에서 사용된 이벤트 패턴의 구조도이다. 여기서 핵심은 이벤트 생성 주체는 이벤트를 누가 처리하는지에 대한 관심이 없다는 것이다. 이벤트 생성 주체는 메세지와 같은 식으로 디스패처에 전달만해주는 것이 관심의 끝이며 그렇게 전달된 이벤트는 이벤트 디스패처-이벤트 핸들러가 알아서 처리하는 형태이다.
결국 이벤트는 아래와 같은 용도로 사용될 수 있다고 정리할 수 있다.
이러한 이벤트 패턴을 사용하게 되면 기능 확장이 발생했을때 그저 핸들러만 더 추가를 해주면 되어서 기능 확장이 용이하다고 할 수 있고, 핸들러간에 구조적으로 책임이 분리되기 때문에 서로 다른 컨텍스트가 섞이는 것도 방지할 수 있다.
위 이벤트 패턴의 구조를 스프링에서 제공하고있는 자원에 매칭하면 아래와 같은 구조로 표현할 수 있다.
강의에서는 이 방식 말고 AbstractAggregateRoot 를 사용하는 방식도 소개하고 있는데 조금 더 DDD 에 가까운 구현 방식이었다. AbstractAggregateRoot를 이용하면 루트 엔티티가 Aggregate에 발생하는 모든 이벤트를 관장할 수 있게 되기 때문이다. 이것의 의미는 Aggregate에 발생하는 모든 이벤트를 루트 엔티티가 퍼사드하게 처리할 수 있다는 뜻이므로 DDD 철학에 가까워진다는 말이기도 하다.
ApplicationEventPublisher는 여러 곳에서 많이 사용하고 있기 때문에 AbstractAggregateRoot 를 활용한 패턴을 실습해보았다.
크게 차이는 없었다. 유일한 차이는 이벤트를 발행하는 방식이었다. ApplicationEventPublisher는 직접 ‘발행’ 처리를 하는 방식인 반면에 AbstractAggregateRoot는 이벤트를 등록해두고 save 됨과 함께 자동으로 등록된 이벤트들이 flush 되는 형식으로 이벤트를 발행한다.
위 코드에서 changePassword를 통해서 새로운 패스워드로 변경하고 이벤트를 등록했다.
이렇게 리스너를 등록해주면 준비는 끝난 상태다.
간단하게 테스트 코드에서 save 처리를 해서 검증해보았다.
changePassword() 에서 registerEvent() 를 통해 등록된 event가 save() 처리와 동시에 flush 되면서 발행되고, 정의해둔 리스너인 notifyPasswordChange가 이를 핸들링 하게 된다.
기존에 ApplicationEventPublisher를 사용하게 되면 서비스 레이어에서 이벤트를 호출해야 했는데, AbstractAggregateRoot를 사용하게 되면서 엔티티에서 직접 이벤트를 등록해주게 되니 이벤트를 도메인 레이어에서 발행할 수 있게 된 것이다.
이는 곧 브레인 스토밍 단계에서 나온 커맨드 -> 이벤트 의 플로우를 그대로 따를 수 있다는 의미이므로 조금 더 DDD 스러운 설계라 말할 수 있다.
이 그림을 다시 보자. 이미 다룬 내용이지만 파란색이 커맨드이고 주황색이 이벤트이다.
AbstractAggregateRoot를 활용함으로써 엔티티 내에서 커맨드(위 실습 예시에서는 changePassword()) 를 정의하고 이 커맨드가 원인으로서 동작하여 결과로 발생하는 이벤트(위 실습 예시에서는 ChangePasswordEvent())가 발생하게 된 것이다. 그리고 save처리와 함께 이벤트가 발행된다.
AbstractAggregateRoot의 단점은 없을까?
ApplicationEventPublisher를 쓰는 것과 대비해서 AbstractAggregateRoot를 쓰는 단점 또한 존재한다. 일단 이벤트 핸들러가 이벤트를 발행하는 발행자를 자동으로 찾지 못한다. ApplicationEventPublisher를 쓰면 리스너에서 publisher를 자동으로 찾을 수 있도록 인텔리제이가 잡아주는데, ApplicationEventPublisher에서는 이것이 불가능하다.(인텔리제이가 제공해주지 않는다)
그리고 AbstractAggregateRoot는 spirng data에서 제공하는 기능이라서 결과적으로 프레임워크에 의존적인 코드가 나오게 된다. 클린 아키텍처의 원칙에 따라서 최대한 프레임워크 의존적이지 않은 코드가 이상적이라 할 수 있는데 굉장히 프레임워크 의존적인 코드가 되는 것이다.
이벤트 리스너의 위치에 관해서 한 수강생이 라이브 강의에서 질문을 하셨는데 좋은 질문이라 생각이 되어서 그대로 옮겨본다.
도메인간의 이벤트를 주고 받을때 이벤트를 받는 쪽 도메인 리스너는 어디에 선언되어있는게 맞을까요? 예를 들어 제품 가격이 변경되면 메뉴 가격 변경을 메뉴의 응용서비스 계층에서 리스너가 있는게 맞을까요, 도메인서비스가 맞을까요, 도메인 클래스 내부가 맞을까요?
강사님의 대답은 응용 서비스 계층이 맞다는 것이었다. 이유는 응용서비스가 결국 사용자의 유스케이스를 다루기 때문이다.
강의자료에 있는 개념 설명을 그대로 옮긴다.
이벤트 소싱은 발생한 이벤트를 모두 저장해두고 이를 이용해서 필요한 데이터를 생성해내는 기법이라고 할 수 있다.
직관적인 예시로 일별 리포트, 월별 리포트와 같은 리포트성 데이터를 예로 들 수 있다. 이런 데이터들은 보통 일별, 월별로 배치를 돌아서 생산해내는데 이벤트들을 모두 저장하고 있다면 주기별로 스냅샷을 만들어내는 방식으로 리포트를 만들 수 도 있다.