Chapter 03. 애그리거트

beanii·2023년 3월 19일
0

DDD Study

목록 보기
3/11
post-thumbnail

3.1 애그리거트

  • 개별 객체 수준에서 모델을 바라본 모습
    • 상위 수준에서 관계 파악하기 힘듦
    • 코드를 변경하고 확장하는 것이 어려워짐
      -> 애그리거트로 해결

  • 애그리커트 단위로 묶어서 표현한 모습
    • 모델 간의 관계를 개별 모델 수준상위 수준에서 모두 이해 가능


[애그리거트의 특성]

  • 애그리거트는 일관성을 관리하는 기준도 됨
    • 복잡한 도메인을 단순한 구조로 만들어줌
    • 도메인 기능을 확장하고 변경하는 데 필요한 노력 줄어듦
      ex) 개발 시간

  • 같은 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 가짐
    • 애그리거트에 속한 구성요소는 대부분 함께 생성되고 함께 제거됨

  • 애그리거트는 경계를 가짐
    • 한 애그리거트에 속한 객체는 다른 애그리거트에 속하지 않음
    • 애그리거트는 독립된 객체 군이며 각 애그리거트는 자기 자신을 관리할 뿐 다른 애그리거트를 관리하지 않음
    • 경계 설정 기준: 도메인 규칙, 요구사항
    • 함께 변경되는 빈도가 높은 객체들은 한 애그리거트에 속할 가능성이 높음
      ex) 총 주문 금액을 계산하는 Order와 주문 상품 개수를 포함하는 OrderLine

  • 'A가 B를 갖는다'로 설계할 수 있는 요구사항 -> 대부분 A와 B는 한 애그리거트에 속함
    • 반드시 그런 것은 아님
      ex) 상품리뷰 -> 함께 생성, 변경되지도 않고 변경 주체(상품 담당자, 고객)도 다르기 때문에 서로 다른 애그리거트에 속함

  • 도메인 관련 경험이 많고 도메인 규칙을 잘 이해할수록 애그리거트의 실제 크기는 줄어듦
    • 애그리거트가 한 개의 엔티티 객체만 갖는 경우 많음
    • 두 개 이상의 엔티티로 구성되는 애그리거트는 드물었음


3.2 애그리거트 루트

  • 애그리거트 루트 엔티티
    • 애그리거트에 속한 모든 객체가 일관된 상태를 유지하기 위해 애그리거트 전페를 관리하는 주체
    • 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직접 또는 간접적으로 속하게 됨

3.2.1 도메인 규칙과 일관성

  • 애그리거트 루트의 핵심 역할 : 애그리거트의 일관성 유지 -> 애그리거트가 제공해야 할 도메인 기능을 구현
    • 애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안됨
  • 불필요한 중복 없이 애그리거트 루트를 통해서만 도메인 로직 구현하기
    1. 단순히 필드를 변경하는 set 메서드를 공개(public) 범위로 만들지 않는다.
      • 도메인 로직이 분산되므로, 유지 보수하기 힘듦
    2. 밸류 타입은 불변으로 구현한다.
      • 애그리거트 외부에서 밸류 객체 상태 변경 불가

  • 따라서 아래 코드와 같이 애그리거트 루트가 제공하는 메서드에 새로운 밸류 객체 전달해서 값 변경해야 함
  • 애그리거트 루트를 통해서만 밸류 타입의 내부 상태 변경 하능하므로, 애그리거트 루트가 도메인 규칙을 올바르게 구현하면 애그리거트 전체의 일관성 유지됨
public class Order {
	private ShippingInfo shippingInfo;
    
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
    	verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }
    
    //set메서드의 접근 허용 범위는 private이다.
    private void setShippingInfo(ShippingInfo newShippingInfo) {
    	//밸류가 불변이면 새로운 객체를 할당해서 값을 변경해야 함
        //불변이므로 this.shippinfInfo.setAddress(newShippingInfo.getAddress())와 같은 코드 사용 불가능
    	this.shippingInfo = newShipping;
    }
    ...
}

3.2.2 애그리거트 루트의 기능 구현

  • 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성

  • 애그리거트 루트가 구성요소의 상태만 참조하는 것이 아닌, 기능 실행을 위임하기도 함

  • 팀 표준이나 구현 기술의 제약으로 객체를 불변으로 구현할 수 없는 경우 -> 객체 변경 기능을 패키지protected 범위로 한정

3.2.3 트랜잭션 범위

  • 트랜잭션 범위는 작을수록 좋음
  • 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 함(= 다른 애그리거트 변경하지 않음)
  • 향후 수정 비용을 최소화하기 위해 애그리거트는 최대한 서로 독립적이어야 함
  • 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 할 경우 -> 아래 코드와 같이 응용 서비스에서 두 애그리거트 수정하도록 구현
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 구현의 편리 : 운영자의 편리함을 위해 주문 목록 화면에서 여러 주문의 상태를 한 번에 변경하고 싶을 경우.


3.3 리포지터리와 애그리거트

  • 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재
    ex) Order는 애그리거트 루트고, OrderLine은 애그리거트에 속하는 구성요소이므로 Order를 위한 리포지터리만 존재

  • 리포지터리의 기본 제공 메서드
    • save: 애그리거트 저장
    • findById: ID로 애그리거트를 구함
    • 이 외에도 검색, 삭제 등의 메서드 추가 가능

  • 리포지터리는 애그리거트 전체를 저장소에 영속화해야 함
    ex) Order 애그리거트와 관련된 테이블이 세 개라면, Order애그리거트를 저장할 때 애그리거트 루트와 매칭되는 테이블뿐만 아니라 애그리거트에 속한 모든 구성요소에 매핑된 테이블에 데이터 저장


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

  • 애그리거트가 다른 애그리거트를 참조 = 다른 애그리거트의 루트를 참조

  • 필드를 이용한 애그리거트 참조의 문제점
    1. 편한 탐색 오용
      • 다른 애그리거트 객체에 접근 가능하면, 다른 애그리거트의 상태 쉽게 변경 가능
    2. 성능에 대한 고민
      • JPA의 경우 두 가지 로딩 방식: 지연(lazy) 로딩즉시(eager) 로딩
    3. 확장 어려움
      - 사용자 늘고 트래픽 증가하면서 하위 도메인별로 시스템 분리하면 단일 기술 사용할 수 없음

  • ID를 이용한 간접 참조
    • 모든 객체가 참조로 연결되지 않고 한 애그리거ㅌ에 속한 객체들만 참조로 연결됨
    • 모델의 복잡도 낮추고 응집도 높임
    • 구현 복잡도도 낮아짐
    • 외부 애그리거트를 직접 참조하지 않으므로 다른 애그리거트의 상태 변경 불가
    • 애그리거트별로 다른 구현 기술 사용 가능
      ex) 리포지터리마다 RDBMS와 NoSQL와 같이 다른 저장소 사용 가능

3.4.1 ID를 이용한 참조와 조회 성능

  • N + 1 조회 문제
    : 조회 대상이 N개일 때 N개를 읽어오는 한 번의 쿼리와 연관된 데이터를 읽어오는 쿼리를 N번 실행함
    • 전체 조회 속도 느려지는 원인
    • 문제 해결하기 위해 조회 전용 쿼리 사용 -> 5장에서 자세히

  • 애그리거트마다 서로 다른 저장소 사용하는 경우
    • 캐시 적용 또는 조회 전용 저장소 따로 구성
    • 단점: 코드 복잡해짐
    • 장점: 시스템 처리량 높일 수 있음


3.5 애그리거트 간 집합 연관

(p.121 ~ 124 예제코드 참고, 살짝 헷갈리므로 다시 읽어보기,,)

  • 1-N 연관
    • 카테고리 입장에서 한 카테고리에 한 개 이상의 상품 솔할 수 있음 -> 카테고리와 상품은 1-N 관계
    • 한 상품이 한 카테고리에만 속할 수 있음 -> 상품과 카테고리는 N-1 관계
    • 요구사항에 따라 애그리거트 간의 1-N 연관을 실제 구현에 반영 안할 때도 있음
      ex) 성능 문제로 인해 카테고리에 속한 상품을 구할 때 상품 입장에서 자신이 속한 카테로리를 N-1로 연관 지어 구할 수 있음

  • M-N 연관
    • 개념적으로 양쪽 애그리거트에 컬렉션으로 연관 만듦
    • 상품이 여러 카테고리에 속할 수 있음 -> 카테고리와 상품은 M-N 연관
    • 요구사항과 성능 문제에 따라 개념적으로는 상품과 카테고리의 양방향 M-N 연관이 있을지라도 실제 구현에서는 상품에서 카테고리로의 단방향 M-N 연관만 적용하면 됨


3.6 애그리거트를 팩토리로 사용하기

예시 상황: 특점 상점이 신고당해서 더 이상 물건을 등록하지 못함

public class RegisterProductService {
  public ProductId registerNewProduct(NewProductRequest req) {
    Store store = storeRepository.findById(req.getStoreId());
    checkNull(store);
    if (!store.isBlocked()) {
      throw new StoreBlockedException();
    }
    ProductId id = productRepository.nextId();
    Product product = new Product(id, store.getId(), ...);
    productRepository.save(product);
    return id;
  }
}
  • 상품 등록 기능을 구현한 응용 서비스에서 상점 계정이 차단 상태인지 확인
    -> Product 생성 가능한지 판단하는 코드와 Product 생성 코드가 분리되어 있음
    -> 중요한 도메인 로직 처리가 응용 서비스에 노출됨

public class Store {
	public Product createProduct(ProductId id, newProductId, ... ) {
		if (isBlocked())
			throw new StoreBlockedException();
		return new Product(newProductId, getId(), ...);
	}
}
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;
  }
}
  • Product 생성 기능을 Store 애그리거트로 옮김
    • createProduct()는 Product 애그리거트를 생성하는 팩토리 역할
    • 응용 서비스는 팩토리 기능 이용해서 Product 생성
    • 응용 서비스에서 더 이상 Store의 상태 확인하지 않음
    • Product 생성 가능 여부를 확인하는 도메인 로직 변경해도 응용 서비스는 영향 받지 않음 -> 도메인의 응집도 높아짐

+) 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트 생성해랴 하는 경우 -> 애그리거트에 팩토리 메서드 구현

0개의 댓글

관련 채용 정보