객체지향

[DDD, 도메인 주도 개발] 3.애그리거트와 애그리거트 루트

마디니 2023. 3. 20. 22:13
반응형

애그리거트

서비스가 커지고 도메인이 복잡해지게 되면

=> 개별 구성요소 위주로 도메인을 바라보게 된다.

=> 전체 구조나 상위 수준에서의 관계 파악이 어려워진다.

=> 상위수준에서 모델이 어떻게 엮여 있는지 몰라 코드 수정을 꺼려하게 된다. 

=> 코드변경을 회피하는 쪽으로 요구사항을 협의하게 된다.

=> 코드 변경이나 확장이 어려워 진다.

 

"애그리거트" 로 위와 같은 문제를 해결 할 수 있다.

 

애그리거트는 관련된 객체를 하나의 군으로 묶어 준다.

많은 객체들을 애그리거트로 묶어서 바라보면 상위 수준에서 도메인 모델 관계들을 파악하기 쉽다.


                                     [애그리거트 적용 전 도메인 모델]                                                               [애그리거트 적용 후 도메인 모델]          

  


애그리거트의 특징

  • 복잡한 도메인을 단순한 구조로 만들어 준다. ->  모델을 보다 잘 이해할 수 있다.
  • 일관성을 관리하는 기준
  • 애그리거트에 속한 객체들은 라이프 사이클이 같거나 비슷하다.
  • 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않는다.
  • 각 애그리거트는 독립적이며, 다른 애그리거트를 관리하지 않는다.
  • 애그리거트의 경계는 도메인 규칙과 요구사항을 통해 판단한다.
※ 객체가 함께 사용되어질때 한 애그리거트에 포함되는 것은 아니다. 라이프 사이클을 기준으로 판단하는 것이 좋다.
예를 들어 상품과, 리뷰는 함께 사용될 때가 많지만 함께 생성되거나 변경되지 않고, 상품의 변경이 리뷰에 영향을 주거나 하지 않으므로  다른 애그리거트다.

 

애그리거트 루트와 역할

애그리거트에 속한 모든 객체가 일관된 상태를 유지하려면 전체를 관리할 주체가 필요한데, 애그리거트의 루트 엔티티가 그 역할을 한다.

 

애그리거트 루트의 역할

  • 애그리거트가 제공해야할 도메인 기능을 구현한다.
  • 애그리거트의 일관성이 깨지지 않도록 한다.
  • 즉, 애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안된다.
    • 필드 변경하는 public set 메서드를 만들지 않는다.
    • 밸류를 불변으로 만들고 변경할 일이 있을때는 새로운 밸류 객체를 전달한다.

애그리거트 루트 기능 구현

애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다. 또한 구성요소의 상태를 참조하고 있는 것 뿐 아니라 구성요소에게 기능 실행을 위임하기도 한다.

public class OrderLines {
	private List<OrderLine> lines;
    
    public Money getTotalAmounts() {
    ...(생략)...
    }
    
    public vlid changeOrderLines(List<OrderLine> newLines) {
    	this.lines = newLines;
    }
}

public class Order {
	private OrderLines orderLines;
    
    // orderLine 에게 변경 기능 실행 위임
    public void changeOrderLInes(List<OrderLine> newLines) {
    	orderLines.changeOrderLines(newList);
        this.totalAmounts = orderLines.getTotalAmounts(); //총합계산
    }
}

// 밸류(orderLine)를 불변으로 만들지 않을 때
OrderLines lines = order.getORderLines();
// totalAmounts 재계산 하는 로직이 빠져 정상 상태가 보장 안됌.
lines.changeOrderLines(newOrderLine);

애그리거트 트랜잭션 범위

  • 트랜잭션 범위는 작을 수록 좋다. => 여러개의 테이블 수정시 잠금 대상이 더 많아지고 통시 처리량을 떨어뜨린다.
  • 비슷한 이유로 한 트랜잭션에서 한 애그리거트만 수정하는 것이 좋다.
  • 한 트랜잭션내에서 여러 애그리거트가 수정된다는 것은 다음을 의미
    • 애그리거트가 다른 애그리거트의 상태까지 관리하는 모양새
    • 한 애그리거트가 다른 애그리거트의 기능에 의존
    • 애그리거트간 결합도 증가
  • 부득이하게 한 트랜잭션으로 두개 이상의 애그리거트를 수정해야 할 경우 -> 응용 서비스에서 두 애그리거트를 수정하도록 구현
public class Order{
	private Orderer orderer;

	public void shipTo(ShippingInfo newshippingInfo, boolean useNewShippingAddrAsMemberAddr){
		verifyNotYetShipped();
		setShippingInfo(newShippingInfo);
		if (useNewShippingAddrAsMemberAddr){
			//order가 다른 애그리거트를 변경하고 있다!!!!!
			orderer.getcustomer().changeAddress(newShippingInfo.getAddress());
		}
	}
}


//위 코드 개선 => 응용서비스에서 Member 상태 변경하기
public class ChangeOrderService{

	@Transactional
	public void changeShippingInfo(OrderId id, ShippingInfo newShippingInfo, boolean useNewSHippingAddrAsMemberAddr){
		Order order = orderRepository.findbyId(id);
		if (order == null) throw new OrderNotFoundException();
		order.shipTo(newShippingInfo);
        
		if (useNewShippingAddrAsMemberAddr) {
         	Member member = findMember(order.getOrderer(());
			member.changeAddress(newShippingInfo.getAddress());
		}
	}
}
  • 도메인 이벤트를 사용하면 한 트랜잭션에서 한개의 애그리거트를 수정하면서 동기나 비동기로 다른 애그리거트의 상태를 변경하는 코드를 작성 할 수 있음
  • 다음과 같은 상황에서는 한 트랜잭션이 두 개 이상의 애그리거트를 변경하는 것을 고려할 수 있다.
    • 팀 표준: 조직의 표준에 따른 유스케이스에 따라
    • 기술 제약: 
    • UI 구현 : 운영자의 편리함을 위해

애그리거트와 리포지토리

  • 객체의 영속성을 처리하는 리포지토리는 애그리거트 단위로 존재한다.
  • 애그리거트 전체를 저장소에 영속화 해야한다. (=애그리거트 루트와 관련 구성요소에 매핑된 테이블에 데이터를 저장해야 한다.)
  • 일반적으로 save, findById 두개의 메소드 기본 제공
  • Order order = findById(id) 로 구하는 order객체는 order, OrderLine등 모든 구성요소들이 완전한 필드를 가지고 있어야 한다.
  • 저장할 때도 모든 변경을 영속화할 저장소에 상관 없이 원자적으로 저장해야한다.

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

애그리거트도 다른 애그리거트를 참조할 수 있다. 

ORM기술 덕분에 애그리거트 루트 참조가 쉬워졌고, 필드를 이용해 참조하면 다른 애그리거트 데이터를 쉽게 조회할 수 있다. -> 이에 따른 문제도 있다.

 

[필드를 이용한 애그리거트 참조 문제점]

  • 편한 탐색 오용: 쉽게 접근 가능해짐 => 구현의 편리함 때문에 쉽게 변경 할 수 있다.
  • 성능 고민: 객체 직접 참조시 상황에 따른 즉시/지연 로딩을 고민 해야한다.
  • 확장 어려움: 하위도메인에 다른 서비스 분리시 DB 다른거 쓰고 싶다면?

[ID를 이용해 다른 애그리거트를 참조하기]

  • 모든 객체가 참조되지 않고 한 애그리거트에 속한 객체들만 참조한다.
  • 애그리거트의 응집도를 높여준다.
  • 구현 복잡도는 낮아진다 -> 참조하는 애그리거트가 필요한 경우 id를 가지고 findById()로 구할 수 있다.
  • 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 근원적으로 방지함
  • N+1 조회 문제
    • N+1은 조인을 사용해 해결해야 한다.
    • 조회 전용 쿼리를 작성해 한번의 쿼리로 필요한 데이터를 로딩할 수 있다.
✅ 조회용 기술 적용
쿼리가 복잡한 경우나 SQL특화 기능이 필요한 경우 조회를 위한 부분만 마이바티스와 같은 기술을 이용할 수 있다.
✅서로 다른 저장소를 사용할 때 한번에 관련 애그리거트 조회하기
-> 조회 성능을 높이기 위해 캐시를 적용하거나 조회 전용 저장소를 따로 구성
-> 시스템 복잡도는 증가하지만 처리량은 높아짐

애그리거트간 집합 연관

카테고리 안의 상품 → 카테고리 : 상품  = 1 : N

상품이 여러 카테고리를 가질 수 있다면 → 카테고리 : 상품 = M : N

 

위와 같이 애그리거트간 연관관계에 컬렉션을 필요로 할 때

 

개념적으로는 카테고리와 상품이 M:N이지만 일반적으로 상품에 모든 카테고리를 표현하지는 않기 때문에

구현할 때는 상품에서 카테고리로의 단방향 연관만 적용하면  된다. 이때 @ElementCollection 을 사용할 수 있다.

 

애그리거트 팩토리 사용하기

한 애그리거트가 갖고 있는 데이터로 다른 애그리거트를 생성할 때는 애그리거트에 팩토리 메서드 고려해 보기

[개선 전 - 도메인 로직이 응용 서비스 영역에 있다.]

public class RegisterProductService {

  public ProductId registerNewProduct(NewProductRequest req) {
  
 	//상품 생성 가능 판단
    Store store = storeRepository.findStoreById(req.getStoreId());
    checkNull(store);
    if(store.isBlocked()) { //도메인 로직!!!!!!!!!!!!!!
    	throw new StoreBlockedException();
    }
    
    //상품 생성
    ProductId id = productRepository.nextId();
    Product product = store.createProduct(id, store.getId(), ...);
    productRepository.save(product);
    return id;
  }
  ...
}

[개선 후 - 도메인 로직을 도메인 영역으로 이동시켜 변경 여역을 최소화 && 도메인 응집도를 높임]

public class Store {
  public Product createProduct(ProductId newProductId, ...) {
    if (isBlocked()) {
      throw new StoreBlockedException();
    }
    return new Product(newProductId, getId(), ...);
    // 상품 생성에 더 많은 정보가 필요하다면 
    // 상품 생성로직을 Store가 아닌 다른 객체(ProductFactory)에 위임해도 된다.
    // return ProductFactory.create(newProductId, getId(), pi);
  }
}

public class RegisterProductService {
  public ProductId registerNewProduct(NewProductRequest req) {
    Store store = storeRepository.findStoreById(req.getStoreId());
    checkNull(store);
    ProductId id = productRepository.nextId();
    //도메인 로직 감춤
    Product product = store.createProduct(id, store.getId(), ...);
    productRepository.save(product);
    return id;
  }
}

 

Reference

도메인 주도개발 시작하기, 최범균 저

반응형