- SpringBoot Events
- SpringBoot @DomainEvent, AbstractAggregateRoot 이용한 Domain Event 발행
이번 실습에서는 도메인 중심 설계에서 Aggregate에 의해 생성된 Domain Event를 쉽게 발행하고 처리하는 방법에 대해 실습합니다. 스프링에서 제공하는 @DomainEvent, AbstractAggregateRoot를 사용하면 간편하게 Domain Event를 처리 할 수 있습니다.
Dependancy 추가
build.gradle
plugins { id 'org.springframework.boot' version '2.4.3' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.daddyprogrammer' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }
Aggregate / Repository 작성
실습에서는 User Aggregate를 작성하고 해당 Aggreate에 수정이 발생할 경우 이벤트를 발행해보겠습니다.
User
다음과 같이 User Aggregate Entity를 작성하고 나이를 한 살 증가시키는 간단한 Domain Operation을 작성합니다.
package com.daddyprogrammer.springevents.entity; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import javax.persistence.*; import java.time.LocalDateTime; @Getter @Builder @AllArgsConstructor @Table(name = "user") @Entity public class User { public User() {} @Id // pk @GeneratedValue(strategy = GenerationType.IDENTITY) private long userNo; private String userId; private int age; private LocalDateTime created; // domain operation public void addAge() { this.age++; } }
UserRepository
package com.daddyprogrammer.springevents.repository; import com.daddyprogrammer.springevents.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUserId(String userId); }
Service에서 수동으로 Event 게시하기
도메인 이벤트를 게시할 수 있는 부분은 2가지가 존재하는데 첫 번째는 서비스 레이어에 게시하는 것이고 두 번째는 Aggregate안에 게시하는 것입니다. 아래와 같이 도메인 서비스 레이어에서 Repository save를 진행하고 간단하게 event를 게시할 수 있습니다.
DomainService
@RequiredArgsConstructor @Service public class DomainService { private final UserRepository userRepository; private final CustomEventPublisher customEventPublisher; @Transactional public void addAge(String userId) { userRepository.findByUserId(userId) .ifPresent(entity -> { entity.addAge(); userRepository.save(entity); customEventPublisher.publish("user age is "+entity.getAge()); }); } }
다음 코드로 이벤트 검증을 수행합니다.
유저 정보 저장 -> 서비스 레이어에서 유저의 나이를 한 살 증가 + 이벤트 발행 -> CustomEventLister가 1회 동작하는지 확인
package com.daddyprogrammer.springevents.service; import com.daddyprogrammer.springevents.entity.User; import com.daddyprogrammer.springevents.event.CustomEvent; import com.daddyprogrammer.springevents.event.CustomEventListener; import com.daddyprogrammer.springevents.repository.UserRepository; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import java.time.LocalDateTime; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @SpringBootTest class DomainServiceTest { @Autowired private UserRepository userRepo; @Autowired private DomainService domainService; @MockBean private CustomEventListener customEventListener; @Test void serviceEventTest() { User user = User.builder().userNo(1).userId("happydaddy").age(30).created(LocalDateTime.now()).build(); userRepo.save(user); // when domainService.addAge(user.getUserId()); // then Mockito.verify(customEventListener, times(1)).onApplicationEvent(any(CustomEvent.class)); } }
Aggregate 내에서 Domain event 게시하기
이번에는 서비스 레이어가 아닌 Domain Operation이 발생한 Aggregate안에서 Domain Event를 게시해보겠습니다. 그런데 User Aggregate의 경우 Plain Java이므로 ApplicationEventPublisher를 주입받기가 어렵습니다. 이를 위해 Spring에서는 @DomainEvent 어노테이션을 지원합니다. 아래와 같이 작성하면 ApplicationEventPublisher에 이벤트를 발행할 수 있습니다.
간단히 설명하면 도메인 이벤트 발행을 위한 CustomEvent 목록을 만들고 이벤트를 하나 발행하면 @DomainEvent 어노테이션에 의해 해당 Event 목록이 ApplicationEventPublisher에 발행되는 것입니다.
도메인 이벤트가 발행된 후 @AfterDomainEventsPublication 어노테이션이 달린 메서드가 호출됩니다. 모든 이벤트 목록을 지움으로써 향후 중복으로 이벤트가 발행되는 것을 막습니다.
User 도메인 작성
@Getter @Builder @AllArgsConstructor @Table(name = "user") @Entity public class User { @Transient private final Collection<CustomEvent> customEvents; public User() { customEvents = new ArrayList<>(); } @DomainEvents public Collection<CustomEvent> events() { return customEvents; } @AfterDomainEventPublication public void clearEvents() { customEvents.clear(); } @Id // pk @GeneratedValue(strategy = GenerationType.IDENTITY) private long userNo; private String userId; private int age; private LocalDateTime created; // domain operation public void addAge() { this.age++; customEvents.add(new CustomEvent(this,"user age is "+this.age)); } }
DomainService에서 수동으로 이벤트를 발행하는 코드를 제거하고 위에서 작성한 Test 코드를 다시 수행하면 이벤트 발행이 성공하는 것을 확인할 수 있습니다.
@RequiredArgsConstructor @Service public class DomainService { private final UserRepository userRepository; private final CustomEventPublisher customEventPublisher; @Transactional public void addAge(String userId) { userRepository.findByUserId(userId) .ifPresent(entity -> { entity.addAge(); userRepository.save(entity); // 삭제 또는 주석 customEventPublisher.publish("user age is "+entity.getAge()); }); } }
AbstractAggregateRoot Template 사용
@DomainEvents를 사용하면 Aggregate에 작성해야 하는 코드가 많아지는데 AbstractAggregateRoot를 사용하면 작성해야 하는 코드를 매우 단순화할 수 있습니다. 아래와 같이 AbstractAggregateRoot를 상속받고 registerEvent를 통해 이벤트가 등록되도록 처리만 하면 됩니다. 보시다시피 훨씬 적은 코드를 생성하고 정확히 동일한 효과를 얻을 수 있습니다.
User 도메인 작성
@Getter @Builder @AllArgsConstructor @Table(name = "user") @Entity public class User extends AbstractAggregateRoot<User> { public User() { } @Id // pk @GeneratedValue(strategy = GenerationType.IDENTITY) private long userNo; private String userId; private int age; private LocalDateTime created; // domain operation public void addAge() { this.age++; registerEvent(new CustomEvent(this,"user age is "+this.age)); } }
실습을 통해 Aggregate 도메인 이벤트를 관리하는 몇 가지 방법을 알아보았습니다. 이 접근 방식은 이벤트 인프라를 크게 단순화하여 도메인 로직에만 집중할 수 있도록 해줍니다.
실습에서 사용한 코드는 아래 Github에서 확인할 수 있습니다.
https://github.com/codej99/spring-event