티스토리 뷰

728x90
반응형

애그리게이트


💡 상위 수준의 관점

애플리케이션을 개발할 때 상위 수준의 개념을 이용해 전체 모델을 정리하면 전반적인 관계를 이해하는데 도움이 됩니다.

 

💡 개별 객체 수준의 관점

개별 객체의 관계가 복잡해지면 개별 구성요소 위주로 이해하게 되며, 전반적인 구조나 상위 수준에서 도메인 관계를 파악하기 힘들어집니다. 도메인 요소 간의 관계를 파악하기 힘들다는 것은 코드를 변경하고 확장하는데 어려워진다는 것을 의미합니다.

 

💡 애그리게이트 수준의 관점

복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들기 위해서는 상위 수준에서 모델을 바라볼 수 있는 방법이 필요한데 그 방법이 애그리게이트입니다. 수 많은 객체를 애그리게이트 단위로 묶어서 표현하면 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있습니다.

애그리게이트는 관련 객체를 묶어서 관리하므로 일관성을 유지할 수 있고 관련된 객체는 유사하거나 동일한 라이프 사이클을 갖습니다.

또한, 애그리케이트는 경계를 가지고 있습니다. 한 애그리게이트에 속한 객체는 다른 애그리게이트에 속해서는 안됩니다. 애그리게이트는 독립된 객체 군이며 각 애그리게이트는 자기 자신을 관리할 뿐 다른 애그리게이트는 관리하지 않습니다.

 

🤔 어떻게 경계를 정하나?

경계를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항입니다. 도메인 규칙에 따라 함께 생성, 수정되는 구성요소는 한 애그리게이트에 속할 가능성이 높아집니다. 

 

🔍 A가 B를 갖는다의 요구사항의 주의점

주문의 경우 Order가 주문 목록과 주문자를 가지므로 A가 B를 갖는다로 해석할 수 있지만 반드시 A와 B가 하나의 애그리케이트에 속한다는 것을 의미하지 않습니다. 예를들어 상품과 리뷰의 경우 Product 엔티티와 Review 엔티티가 하나의 애그리게이트라 생각할 수 있지만 이 둘은 동일한 라이프 사이클을 갖지도 않고, 주체자 또한 다름니다.

 

애그리게이트 루트


애그리게이트는 여러 객체로 구성되므로 한 객체의 상태만 정상이면 안됩니다. 하나의 애그리게이트에 속한 모든 객체의 상태가 정상이어야 합니다. 애그리게이트에 속한 모든 객체가 일관된 상태를 가지기 위해서는 애그리게이트 전체를 관리할 주체가 필요한데 이 책임을 지는것이 애그리게이트 루트 엔티티입니다. 애그리게이트에 속한 모든 객체는 이 루트 엔티티에 직접 또는 간접적으로 속해있습니다.

 

💡 도메인 규칙과 일관성

애그리게이트 루트의 역할은 단순히 관련된 모든 객체를 포함하는게 아닌 일관성이 깨지지 않도록 하는것입니다. 일관성을 유지하기 위해서는 루트 엔티티가 도메인 기능을 구현하고 이를 제공해야 합니다.

 

🧨 아래는 문제점이 있는 예제입니다. 

첫번째의 경우 setter 메서드를 사용하므로 루트 엔티티가 강제하는 규칙을 적용할 수 없어 일관성을 깨는 원인이 됩니다.

두번째의 경우 일관성을 지키기 위해 상태 확인 로직을 응용 계층에 구현하고 있지만 이는 코드의 중복을 유발할 수 있으므로 유지보수에 도움되지 않습니다.

// 좋지 않은 방법 1
@Service
public class OrderService {

    public void changeShippingInfo() {
        ShippingInfo shippingInfo = order.getShippingInfo();
        shippingInfo.setAddress(newAddress); // setter 사용 -> 불변성 깨짐
    }
}

// 좋지 않은 방법 2
@Service
public class OrderService {

    public void changeShippingInfo() {
        ShippingInfo shippingInfo = order.getShippingInfo();

        if (order.getOrderState() != OrderState.PAYMENT_WAITING && order.getOrderState() != OrderState.PREPARING) {
            throw new IllegalStateException("already shipped");
        }
        
        shippingInfo.setAddress(newAddress);
    }
}

 

👍 개선된 코드

public class Order {

    private OrderState orderState;
    private ShippingInfo shippingInfo;

    public void changeShippingInfo(ShippingInfo shippingInfo) {
        verifyNotYetShipped();
        this.shippingInfo = shippingInfo;
    }

    private void verifyNotYetShipped() {
        if (orderState != OrderState.PAYMENT_WAITING && orderState != OrderState.PREPARING) {
            throw new IllegalStateException("already shipped");
        }
    }
}

@Service
public class OrderService {

    public void changeShippingInfo() {
        order.changeShippingInfo(newAddress);
    }
}

 

💡 트랜잭션 범위

트랜잭션의 범위는 작을수록 좋다고 합니다. 한 트랜잭션이 한 개의 테이블을 수정하는 것과 다 수의 테이블을 수정하는 것을 비교하면 성능적으로 차이가 발생한다고 합니다. 한 개의 테이블을 수정하면 트랜잭션 충돌을 막기 위해 잠그는 대상이 한 개 테이블의 한 행으로 한정되지만 다수의 테이블을 수정하면 그만큼 잠금 대상이 많아지고 동시에 처리할 수 있는 트랜잭션의 개수가 줄어들며 성능을 떨어뜨린다고 합니다.

이와 마찬가지로 한 트랜잭션 안에서는 하나의 애그리게이트만을 수정해야 합니다. 한 트랜잭션에서 N개의 애그리게이트를 수정하면 트랜잭션 충돌이 발생할 가능성이 높다고 합니다.

 

🧨 Order는 다른 애그리게이트의 상태까지 관리하고 있으므로 자신의 책임을 넘어서고 있습니다. 애그리게이트는 최대한 서로 독립적이어야 하나 Order는 Member 애그리게이트에 의존하고 있으므로 결합도가 높아집니다.

@Service
public class OrderService {

    public void shipTo() {
        order.shipTo(newShippingInfo, true);
    }
}

public class Order {

    private Orderer orderer;
    private OrderState orderState;
    private ShippingInfo shippingInfo;

    public void shipTo(ShippingInfo shippingInfo, boolean useNewShippingAddrAsMemberAddr) {
        verifyNotYetShipped();
        this.shippingInfo = shippingInfo;

        if (useNewShippingAddrAsMemberAddr) {
            // 다른 애그리게이트의 상태 변경
            orderer.getMember().changeAddress(shippingInfo.getAddress());
        }
    }
    
    private void verifyNotYetShipped() {
        if (orderState != OrderState.PAYMENT_WAITING && orderState != OrderState.PREPARING) {
            throw new IllegalStateException("already shipped");
        }
    }
}

 

👍 개선된 코드

@Service
public class OrderService {

    public void changeShippingInfo() {
        order.shipTo(newShippingInfo, true);

        if (useNewShippingAddrAsMemberAddr) {
            Member member = findMember(order.getOrderer());
            member.changeAddress(newShippingInfo.getAddress());
        }
    }
}

public class Order {

    private Orderer orderer;
    private OrderState orderState;
    private ShippingInfo shippingInfo;

    public void shipTo(ShippingInfo shippingInfo) {
        verifyNotYetShipped();
        this.shippingInfo = shippingInfo;
    }

    private void verifyNotYetShipped() {
        if (orderState != OrderState.PAYMENT_WAITING && orderState != OrderState.PREPARING) {
            throw new IllegalStateException("already shipped");
        }
    }
}

 

리포지터리와 애그리게이트


애그리게이트는 개념상 완전한 한 개의 도메인 모델을 표현하므로 객체의 영속성을 처리하는 리포지터리는 애그리게이트 단위로 존재애햐 합니다. 예를들어 Order와 OrderLine을 물리적으로 별도의 DB 테이블에 저장한다고하더라도 각각의 리포지터리를 만드는게 아닌 루트 엔티티의 리포지터리만 만듭니다.

애그리게이트는 개념적으로 하나이므로 리포지터리는 애그리게이트 전체를 저장소에 영속화해야 합니다.

@Service
public class OrderService {

    @Transactional
    public void save() {
       Order order = new Order(orderer, orderState, shippingInfo, ...);
       orderRepository.save(order);
    }
}

 

ID를 이용한 애그리게이트 참조


한 객체가 다른 객체를 참조하는 것처럼 애그리게이트도 다른 애그리게이트를 참조할 수 있습니다. 애그리게이트의 관리 주체는 루트이므로 루트 애그리게이트가 다른 루트 애그리게이트를 참조해야 합니다.

 

💡 필드 참조

필드를 이용해 다른 애그리게이트에 접근할 때 개발자에게 편리함을 제공하지만 다음의 문제를 야기시킬 수 있습니다.

  1. 편한 탐색 오용 -> 트랜잭션의 범위 부분에서 언급
  2. 성능에 대한 고민
  3. 확장 어려움
Order order = orderRepository.findByid(orderId);
order.getOrderer().getMember().getId();

 

💡 ID 참조

Order order = orderRepository.findByid(orderId);
Long memberId = order.getOrderer().getMember().getId();
Member member = memberRepository.findByid(memberId);
member.changeAddress(newAddress);

 

애그리게이트를 팩토리로 사용하기


상황: 고객이 특정 상점을 여러 차례 신고하여 해당 상점이 더 이상 물건을 등록하지 못하도록 차단한 상태

public class ProductService {
    
    @Transactional
    public void saveNewProduct(Request request) {
        Store store = storeRepository.findById(request.getStoreId());
        checkNull(store);
        
        if (store.isBlocked()) { // store의 로직 노출
            throw new StoreBlockedException();
        }
        
        Product product = new Product();
        productRepository.save(product);
    }
}

 

👍 개선된 코드

앞의 코드와 차이점은 응용 서비스에서 Store의 상태를 확인하지 않는다는 것입니다. Store가 Product를 생성할 수 잇는지 확인하는 도메인 로직인 Store에서 구현하고 있음으로 이제 Product 생성 가능 여부를 변경하더라도 Store만 변경하면 되므로 다른 곳에는 영향을 안받게 됩니다. 이렇게 응집도를 높일 수 있습니다.

public class ProductService {

    @Transactional
    public void saveNewProduct(Request request) {
        Store store = storeRepository.findById(request.getStoreId());
        checkNull(store);
        Product product = store.createProduct();
        productRepository.save(product);
    }
}

public class Store {
    
    private Long storeId;

    public Product createProduct() {
        if (store.isBlocked()) {
            throw new StoreBlockedException();
        }
        return Product(storeId);
    }
}

 

 

 

 

 

 

 

 

 

728x90
반응형

'스터디 > 도메인 주도 개발 시작하기' 카테고리의 다른 글

바운디드 컨텍스트  (0) 2023.02.18
응용 서비스와 표현 영역  (0) 2023.01.26
리포지터리와 모델 구현  (0) 2023.01.06
아키텍처 개요  (0) 2022.12.25
도메인 모델 시작하기  (0) 2022.12.14