티스토리 뷰

Spring

[Spring] 스프링이 구현한 Aggregate Root(애그리거트 루트)와 도메인 이벤트

망나니개발자 2023. 12. 26. 10:00
반응형

 

 

1. 스프링이 구현한 Aggregate Root(애그리거트 루트)와 도메인 이벤트


[ Aggregate(애그리거트)와 도메인 이벤트 ]

도메인을 중심으로 하는 애플리케이션을 개발할 때 등장하는 개념으로 애그리거트가 있다. 애그리거트(Aggregate)란 관련된 객체들을 모아둔 하나의 단위로, 값 객체(Value Object)와 엔티티(Entity)로 구성된다. 그리고 애그리거트의 중심에는 애그리거트 루트가 존재하는데, 이를 애그리거트 루트(Aggregate Root)라고 부른다. 객체들은 애그리거트 루트를 중심으로 관리된다. (해당 포스팅은 이러한 내용을 자세히 다루는 것이 주목적이 아니므로 간략히만 살펴보도록 하자.)

 

 

이를 주문 도메인을 기준으로 그려보면 다음과 같다.

 

 

애그리거트 루트가 영속화 될 때 변경에 대한 처리를 해야 하는 경우가 있다. 이때 도메인 이벤트(DomainEvent)를 발행하여 영속화 시에 필요한 작업들을 처리할 수 있다.

spring-data-commons는 spring-data 관련 프로젝트들을 위한 공통 기능들을 제공한다. 그리고 그 중에는 애그리거트(Aggregate)를 추상화한 AbstractAggregateRoot와 이에 대한 이벤트 처리 기능도 존재한다. 스프링은 이러한 애그리거트와 도메인 이벤트를 어떻게 구현했는지 살펴보도록 하자.

 

 

 

[ 스프링이 구현한 Aggregate Root(애그리거트 루트)와 도메인 이벤트 ]

스프링(spring-data-commons)은 애그리거트 영속화와 그에 따른 이벤트 발행의 반복 작업을 공통으로 제공해주는 기능을 구현하였다. 애그리거트 루트는 AbstractAggregateRoot 클래스로 구현되어 있다.

스프링은 다음과 같은 기능을 제공하고 있다.

  • 이벤트 등록
  • 다른 애그리거트의 이벤트들 등록
  • 이벤트 초기화
  • 도메인 이벤트 목록 조회

 

 

public class AbstractAggregateRoot<A extends AbstractAggregateRoot<A>> {

    private transient final @Transient List<Object> domainEvents = new ArrayList<>();

    protected <T> T registerEvent(T event) {
        Assert.notNull(event, "Domain event must not be null");
        this.domainEvents.add(event);
        return event;
    }

    @AfterDomainEventPublication
    protected void clearDomainEvents() {
        this.domainEvents.clear();
    }

    @DomainEvents
    protected Collection<Object> domainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }

    @SuppressWarnings("unchecked")
    protected final A andEventsFrom(A aggregate) {
        Assert.notNull(aggregate, "Aggregate must not be null");
        this.domainEvents.addAll(aggregate.domainEvents());
        return (A) this;
    }

    @SuppressWarnings("unchecked")
    protected final A andEvent(Object event) {
        registerEvent(event);
        return (A) this;
    }
}

 

 

 

그리고 JPA Repository에 AOP를 적용하여 호출 시에 도메인 이벤트가 발행되도록 구현(EventPublishingRepositoryProxyPostProcessor)하였다. 이벤트가 발행되는 케이스는 다음과 같다.

  • 파라미터가 1개이며 save 메소드 혹은 delete, deleteAll, deleteAllInBatch 메소드인 경우
  • deleteById의 경우에는 호출되지 않음
private static boolean isEventPublishingMethod(Method method) {
        return method.getParameterCount() == 1 //
            && (isSaveMethod(method.getName()) || isDeleteMethod(method.getName()));
    }

private static boolean isSaveMethod(String methodName) {
    return methodName.startsWith("save");
}

private static boolean isDeleteMethod(String methodName) {
    return methodName.equals("delete") || methodName.equals("deleteAll") || methodName.equals("deleteInBatch")
        || methodName.equals("deleteAllInBatch");
}

 

 

당연하게도 deleteById의 경우에는 PK로 엔티티를 지우기 때문에 이벤트가 발행되지 않는다. 왜냐하면 도메인의 이벤트는 애그리거트 루트에 저장되는데, deleteById는 Id 파라미터 하나 뿐이므로 도메인 이벤트를 발행할 수 없기 때문이다. 이를 구현하기 위한 제안도 있었지만, 라이프사이클 관련 문제로 인해 합리적으로 이벤트를 포착할 수 없어서 구현을 하지 않기로 하였다고 한다.

 

 

 

 

 

2. 스프링의 Aggregate Root(애그리거트 루트)와 도메인 이벤트 사용법


[ 스프링의 Aggregate Root(애그리거트 루트)와 도메인 이벤트 사용법 ]

스프링이 제공하는 애그리거트 루트와 도메인 이벤트를 사용하기 위해서는 먼저 엔티티(JPA Entity)가 AbstractAggregateRoot를 상속받아야 한다. 그러면 해당 클래스의 registerEvent 기능을 사용할 수 있다.

import org.hibernate.annotations.CreationTimestamp;
import org.springframework.data.domain.AbstractAggregateRoot;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Member extends AbstractAggregateRoot<Member> {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    public void add() {
        this.registerEvent(new MemberEvent(this));
    }
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberEvent {
    private Member member;
}

 

 

이벤트가 등록되고 spring-data 프로젝트가 제공하는 repository를 통해 영속화가 되었을 경우, 이벤트가 발행된다. 이벤트가 발행된 후에는 AbstractAggregateRoot 클래스에 존재하는 clearDomainEvents에 의해 이벤트 목록이 초기화된다. clearDomainEvents가 호출되는 이유는 @AfterDomainEventPublication 때문이다. @AfterDomainEventPublication는 이름 그대로 도메인 이벤트가 발행된 후에 호출될 메소드를 지정한다.

@AfterDomainEventPublication
protected void clearDomainEvents() {
    this.domainEvents.clear();
}

 

 

발행된 이벤트를 수신하려면 이벤트 리스너를 통해 수신자를 구현해주면 된다. 이벤트 리스너에 대한 자세한 내용은 앞선 포스팅에서 살펴보도록 하자.

@Component
class ApplicationEventListener {

    @EventListener
    public void onMemberEvent(MemberEvent e) {
        log.info("======================")
        log.info("event: {}", e)
        log.info("======================")
    }
}

 

 

 

 

 

 

스프링은 추상 애그리거트 루트(AbstractAggregateRoot)와 도메인 이벤트(DomainEvent) 기능을 제공하고 있다. 이를 사용하면 보다 도메인 중심적인 애플리케이션을 개발할 수 있을 것이다.

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함