애그리거트

2023. 5. 15. 12:30개발 관련 책 읽기/DDD 시작하기

✅ 아래 내용들에 대해서 알아보자

- 애그리거트
- 애그리거트 루트
- 트랜잭션 범위

 

 

애그리거트

 애그리거트(aggregate, 집합)는 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 그림으로 같이 보자.

아래 그림을 보면 주문, 분류, 상품, 리뷰, 결제, 회원 등으로 개념적으로 나눈 것을 볼 수 있다.

 

애그리거트 단위

 

 애그리거트로 단위로 분류하게 되면 복잡한 도메인을 이해,관리하기 쉽고 수많은 객체들을 애그리거트 단위로 묶어서 바라보면 상위 수준에서 도메인 모델 간의 관계를 파악하기가 용이하다.

 

그리고 모델을 이해하는데 도움을 줄 뿐만 아니라 일관성을 관리하는 기준이 된다. 그로 인해 복잡도가 낮아지고 도메인 기능을 확장하고 변경하는데 필요한 노력도 줄어들게 된다.

 

애그리거트는 관련된 모델을 하나로 모았기 때문에 한 애그리거트에 속한 객체는 유사하거나 동일한 사이클을 갖는다.

예를 들어, 주문 애그리거트는 배송지를 변경하거나 주문 상품 개수를 변경하지만 회원 정보 등을 변경하지는 않는다.

 

 

애그리거트 루트

 애그리거트 루트는 애그리거트에 속한 모든 객체들을 일관된 상태를 유지하고 관리하는 역할을 하는 루트 엔티티이다.

 

 

주문 애그리거트의 루트는 Order이다.

 

위 그림처럼 주문 애그리거트에서 루트 엔티티는 Order가 된다. 루트 엔티티의 핵심 역할은 애그리거트의 일관성을 깨지지 않도록 하는 것이다.

 

이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현하고(ex. 배송지 변경, 상품 변경 등) 애그리거트 루트인 Order가 이 기능을 구현한 메서드를 제공해야 한다. 코드로 같이 보자!!

 

//에그리거트 루트 엔티티
public class Order {

	private String orderNumber;
    private List<OrderLine> orderLines;
    private Money totalAmounts; //총 가격

    private ShippingInfo shippingInfo;

    private OrderState state;
    private Orderer orderer;


	/**
     * 애그리거트 루트는 도메인 규칙을 구현한 기능 제공
     */
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }

	/**
     * 주문 취소가 가능 여부 확인(출고 전인지 상태 확인)
     */
    public void verifyNotYetShipped(){
        if(!this.state.isOrderCanclable())
            throw new IllegalStateException("이미 배송된 주문입니다.");
    }
    
    /**
     * set 메서드를 사용하는 대신 접근 제한자는 private이다. 즉 외부에서 사용 x
     */
    private void setShippingInfo(ShippingInfo shippingInfo) {
        if (Objects.isNull(shippingInfo)) {
            throw new IllegalArgumentException("배송정보가 없습니다");
        }

        //밸류가 불변이면 새로운 객체를 할당, 받아서 변경해줘야한다.
        //따라서 new ShippingInfo()는 안됨
        this.shippingInfo = shippingInfo;
    }


}

 

 

위 코드를 보면 배송지 변경 기능을 하는(changeShippinInfo) 메서드가 있다. ShippinInfo Value타입 객체 필드 값을 변경하는 게 아니라, Order 엔티티(루트 엔티티)에서 배송지를 변경할 수 있게 메서드를 제공한다. 아래 코드를 잠깐 보자

 

ShippinInfo si = order.getShippinInfo();
si.setAddress(newAddress);

 

위 코드는 Order에서 ShippinInfo를 가져와 직접 정보를 변경하는 코드이다. 이는 논리적인 데이터의 일관성을 꺠지게 되는 것을 야기하고 추후 유지보수에도 장점이 되지 않는다.  왜냐하면 ShippinInfo는 불변 타입인데 값을 변경하는 행위를 하는 것이고 이 값변경을 통해 데이터의 일관성을 깨지게 될 원인을 제공하게 된다.

 

 

따라서, 루트 엔티티에서 도메인 로직으로 구현해야 하는데..  불필요한 중복을 피하고 루트 엔티티에서 도메인 로직 구현하려면 밸류 타입(여기선 ShippinInfo)을 불변으로 구현하고 set 메서드를 public범위로 만들지 않게 하여 메서드를 도메인 내부에서만 사용할 수 있게 막는 방식으로 적용해야 한다. (아래 코드처럼)

 

    /**
     * set 메서드를 사용하는 대신 접근 제한자는 private이다. 즉 외부에서 사용 x
     */
    private void setShippingInfo(ShippingInfo shippingInfo) {
        if (Objects.isNull(shippingInfo)) {
            throw new IllegalArgumentException("배송정보가 없습니다");
        }

        //밸류가 불변이면 새로운 객체를 할당, 받아서 변경해줘야한다.
        //따라서 new ShippingInfo()는 안됨
        this.shippingInfo = shippingInfo;
    }

 

 

트랜잭션 범위

 트랜잭션 범위는 작을수록 좋다. 한 트랜잭션이 한 개 테이블 수정하는 것과 세 개의 테이블을 수정하는 것을 비교하면 성능에서 차이가 발생한다. 테이블이 많아질수록 Lock 대상이 많아지게되고, 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 의미하고 전체적인 성능(처리량)을 떨어드린다.

 

그래서 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다. 즉, 한 애그리거트만 수정한다는 것은 다른 애그리거트를 변경하지 않는다는 것을 의미하며, 한 애그리거트에서 다른 애그리거트를 수정하게 되면 결과적으로 두 개의 에그리거트를 한 트랜잭션에서 수정하게 되므로, 애그리거트 내부에서 다른 애그리거트의 상태를 변경하는 기능을 실행하면 안 된다!

 

Order - Member 애그리거트 상태 변경 예시

 

애그리거트는 최대한 서로 독립적으로 관리되어야 하고 다른 애그리거트에 의존하기 시작하면 애그리거트 간 결합도가 높아져 향후 수정 비용이 증가하므로 애그리거트에서는 다른 애그리거트의 상태를 변경하지 말아야 한다!

 

만약 각 애그리거트의 수정이 필요하면 응용 서비스에서 각 애그리거트의 상태를 변경한다.

도메인 이벤트를 통해 동기나 비동기로 다른 애그리거트의 상태를 변경하는 코드를 작성할 수 있다. 이로 인해 트랜잭션 관리도 각각 가능하게 된다

 

서비스 레이어에서 두개 이상 에그리거트 변경

 

반응형

'개발 관련 책 읽기 > DDD 시작하기' 카테고리의 다른 글

DDD 아키텍처  (0) 2023.04.29