- 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














