[책]DDD START! #3. 애그리거트

bien·2024년 9월 3일
0

DDD_START!

목록 보기
3/3

애그리거트

주요 도메인 개념 간의 관계를 파악하기 어렵다는 것은 곧 코드를 변경하고 확장하는 것이 어려워진다는 것을 의미한다. 상위 수준에서 모델이 어떻게 엮여있는지 알아야 전체 모델을 망가뜨리지 않으면서 추가 요구사항을 모델에 반영할 수 있는데 세부적인 모델만 이해한 상태로는 코드를 수정하기가 두렵기 때문에 코드 변경을 최대한 회피하는 쪽으로 요구사항을 협의하게 된다.

  • 애그리거트(Aggregate)
    • 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만들려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 바로이다.
      • 수많은 객체를 애그리거트로 묶어서 바라보면 좀 더 상위 수준에서 도메인 모델 간의 관계를 파악할 수 있다.
    • 애그리거트는 모델을 이해하는 데 도움을 줄 뿐만 아니라 일관성을 관리하는 기준이 된다.
    • 애그리거트는 관련된 모델을 하나로 모은 것이기 때문에 속한 객체는 유사하거나 동일한 라이프사이클을 갖는다.
      • 도메인 규칙에 따라 최초 주문 시점에 일부 객체를 만들 필요가 없는 경우도 있지만, 애그리거트에 속한 구성요소는 대부분 함께 생성하고 함께 제거한다.
  • 경계를 설정할 때 기본이 되는 것은 도메인 규칙과 요구사항이다.
    • 도메인 규칙에따라 함께 생성되는 구성요소는 한 애그리거트에 속할 가능성이 높다.
  • 다수의 애그리거트가 한 개의 엔티티 객체만 갖는 경우가 많으며 두 개 이상의 엔티티로 구성되는 애그리거트는 드물게 존재한다.

애그리거트 루트

  • 루트 엔티티
    • 애그리거트에 속한 모든 객체가 일관된 상태를 유지하기 위해 애그리거트 전체를 관리할 주체
      • 총 금액인 totalAmounts를 갖고 있는 Order 엔티티
      • 개별 구매 상품의 개수인 quantity와 금액인 price를 갖고 있는 OrderLine 벨류
        • '주문 총 금액은 개별 상품의 주문 개수 * 가격의 합이다'라는 도메인 규칙을 위해 함께 변경되어야 하는 정보

도메인 규칙과 일관성

  • 애그리거트 루트의 핵심 역할은 애그리거트의 일관성이 깨지지 않도록 하는 것이다.
    • 이를 위해 애그리거트 루트는 애그리거트가 제공해야 할 도메인 기능을 구현한다.

Order.java

public class Order {
	
    // 애그리거트 루트는 도메인 규칙을 구현한 기능을 제공한다.
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
    	verifyNotYetShipped();
        setShippingInfo(newSippingInfo);
    }
    
    private void verifyNotYetShipped() {
    	if (state != OrderState.PAYMENT_WATING && state != OrderState.WAITING)
        	throw new IllegalStateException("already shipped");
    }
    
    ...
  • 배송이 시작되기 전까지만 배송지 정보를 변경할 수 있다는 규칙이 있는 경우
    • 애그리거트 루트인 OrderchangeShippingInfo()메서드가 이 규칙에 따라, 배송 시작 여부를 확인하고 변경이 가능한 경우에만 배송지 정보를 변경해야 한다.

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

  • 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.
    • 예를들어, Order는 총 주문 금액을 구하기 위해 OrderLine 목록을 사용한다.
public class Order {
	private Money totalAmounts;
    private List<OrderLine> orderLines;
    
    private void calculateTotalAmounts() {
    	int sum = orderLines.stream()
        			.mapToInt(o1 -> o1.getPrice() * o1.quantity())
                    .sum();
        this.totalAmounts = new Money(sum);
    }
}
  • 애그리거트 루트가 구성요소의 상태를 참조하는 것이 아니라, 기능 실행을 위임할 수도 있다.
    • OrderLine 목록을 별도 클래스로 분리하는 경우, OrderchangeOrderLines()메서드는 orderLines 필드에 상태 변경을 위임하는 방식으로 기능을 구현한다.
public class OrderLines {
	private List<OrderLine> lines;
    
    public Money getTotalAmounts(); { ...구현; }
    public void changeOrderLines(List<OrderLine> newLines) {
    	this.line = newLines;
    }
}

public class Order {
	private OrderLines orderLines;
    
    public void changeOrdrLines(List<OrderLines> newLines) {
    	orderLines.changeOrderLines(newLines);
        this.totalAmounts = orderLines.getTotalAmounts();
    }
}
  • 주의) 애그리거트 외부에서 내부 구성요소 상태를 변경하지 못하도록 제한해야 한다.
    • 예시에서는 OrderLines 목록이 변경되는데, 총 합은 계산하지 않는 버그를 만들게 된다.
OrderLines lines = order.getOrderLines();

// 외부에서 애그리거트 내부 상태 변경!
// order의 totalAmounts가 값이 OrderLines가 일치하지 않게 됨
lines.changeOrderLines(newOrderLines);

트랜잭션 범위

  • 트랜잭션의 범위는 작을수록 좋다.
    • DB테이블을 기준으로 한 트랜잭션이 1개 테이블을 수정하는 것과 3개의 테이블을 수정하는 것은 성능에서 차이가 발생한다.
    • 1개 테이블을 수정할 때에는 트랜잭션 충돌을 막기 위해 잠그는 대상이 한 개 테이블 한 행으로 한정되지만, 세 개의 테이블을 수정하면 잠금 대상이 더 많아진다.
    • 잠금 대상이 많아진다는 것은 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 주어든다는 것을 뜻하고 이는 전체적인 성능을 떨어뜨린다.
  • 한 트랜잭션에서는 한 개의 애그리거트만 수정해야 한다.
    • 한 트랜잭션에서 두 개 이상의 애그리거트를 수정하면 트랜잭션 충돌이 발생할 가능성이 더 높아지기 때문에, 한번에 수정하는 애그리거트 개수가 많아질 수록 전체 처리량이 떨어지게 된다.
  • 한 애그리거트에서 다른 애그리거트를 변경하지 않는다.
    • 이는 애그리거트가 자신의 책임 범위를 넘어 다른 애그리거트의 상태까지 관리하는 것이 된다.
    • 애그리거트는 서로 최대한 독립적이어야 하는데, 한 애그리거트가 다른 애그리거트에 의존하기 시작히면, 애그리거트 간 결합도가 높아지게 된다.
      • 결합도가 높아지면 높아질수록 향후 수정 비용이 증가하므로 애그리거트에서 다른 애그리거트의 상태를 변경하지 말아야 한다.
public class Order {
	private Orderer orderer;
    
    public void shipTo(ShippingInfo newShippingInfo,
    				   boolean useNewShippingAsMemberAddr) {
    	verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
        if (useNewShippingAddrAsMemberAddr) {
        	// 다른 애그리거트의 상태를 변경하면 안 됨!
            orderer.getCustomer().changeAddress(newShippingInfo.getAddress());
        }
    }
}

리포지터리와 애그리거트

  • 애그리거트: 개념상 완전한 한 개의 도메인 모델을 표현
    • 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재한다.
    • OrderOrderLine을 물리적으로 각각 별도의 DB에 저장하더라도, Order가 애그리거트 루트이므로 Order를 위한 리포지터리만 존재한다.
  • 리포지터리는 적어도 다음의 두 메서드를 제공해야 한다.
    • save: 애그리거트 저장
    • findById: ID로 애그리거트를 구함
  • 리포지터리는 애그리거트 전체를 저장소에 영속화해야 한다.
    • 애그리거트는 개념적으로 하나이므로, 리포지터리가 완전한 애그리거트를 저장하고 제공하지 않으면, 데이터의 일관성이 깨지게 된다.
    • RDBMS를 이용해서 리포지터리를 구현 시, 트랜잭션을 이용해 애그리거트의 변경이 저장소에 반영되는것을 보장할 수 있다.
// 리포지터리에 애그리거트를 저장하려면 애그리거트 전체를 영속화해야 한다.
orderRepository.save(order);

// 리포지터리는 완전한 order를 제공해야 한다.
Order order = orderRepository.findById(orderId);

// order가 와전한 애그리거트가 아니면
// 기능 실행도중 NullPointException과 같은 문제가 발생한다.
order.cancel();

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

  • 객체간의 참조와 같이, 애그리거트간의 참조도 있을 수 있다.
    • 애그리거트의 관리 주체가 애그리거트 루트이므로, 다른 애그리거트를 참조한다는 것은 다른 애그리거트의 루트를 참조한다는 것을 의미한다.
    • JPA를 활용하면 연관객체를 필드에 설정하고, 다른 애그리거트를 쉽게 참조할 수 있다.
public class Order {
	private Orderer orderer;
    ...
}

public class Orderer {
	private Member member;
    private String name;
}

public class Member {
	...
}

order.getOrderer().getMember().getId();
  • ORM기술이 제공하는 필드를 통한 참조는, 다음과 같은 문제를 야기할 수 있다.
    • 편한 탐색 오용
    • 성능에 대한 고민
    • 확장 어려움

문제점1. 편한 탐색 오용

  • 한 애그리거트에서 다른 애그리거트 객체에 접근할 수 있으면, 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다.
    • 트랜잭션 범위에서 언급한 것 처럼, 한 애그리거트가 관리하는 범위는 자기 자신으로 한정해야 한다.
public class Order {
	private Orderer orderer;
    
    public void changeShippingInfo(ShippingInfo newShippingInfo,
    								boolean useNewShippingAddrAsMemberAddr) {
		...
        if (useNewShippingAddrAsMemberAddr) {
        	// 한 애그리거트 내부에서 다른 애그리거트에 접근할 수 있으면,
            // 구현이 쉬워진다는 것 때문에 다른 애그리거트의 상태를 변경하는
            // 유횩에 빠지기 쉽다.
            orderer.getCustomer().changeAddress(newShippingInfo.getAddress());
        }
	}
}

아... 그 당시에 알고있다는 사실로 그쳐야 하는구나.
중요한것은, 단순히 조회만 가능하고 수정이 불가능해야 하는구나.
상태 변경 자체를 애그리거트에 가두는게 가장 주요한 목적이구나.
그 부분을 가두어야지만, 복잡도 증가를 예방할 수 있게 되는거구나.

문제점2. 성능에 대한 고민

  • JPA를 사용할 경우, 참조 객체 로딩에 대해 2가지 선택권을 가진다.
    1. 지연(lazy) 로딩
      • 애그리거트 상태를 변경해야 하는 경우, 불필요한 객체를 함께 로딩할 필요가 없으므로 지연로딩이 성능에 유리하다.
    2. 즉시(eager) 로딩
      • 단순히 연관된 객체의 데이터를 함께 화면에 보여주어야 하는 경우, 즉시 로딩이 성능에 유리하다.
  • 이 같은 다양한 경우의 수를 고려해 연관매핑과 JPQL/Criteria 쿼리의 로딩 전략을 결정해야 한다.

문제점3. 확장의 어려움

  • 초기에는 단일 서버에서 단일 DBMS로 서비스를 제공하는 것이 가능하다.
    • 그러나 사용자가 늘어나 트래픽이 증가하면, 자연스럽게 부하를 분산하기 위해 하위 도메인별로 시스템을 분리하기 시작한다.
  • 이 과정에서 하위 도메인별로 다른 DBMS를 사용할 가능성이 높다.
    • 한 하위 도메인은 마리아DB를 사용하고, 다른 하위 도메인은 몽고DB를 사용하는 식으로 분리되는 경우, 다른 애그리거트를 참조하기 위해 JPA와 같은 단일 기술을 사용할 수 없게 된다.

🚀 해결법; ID를 이용한 애그리거트 참조

이런 경우, ID를 이용해 다른 애그리거트를 참조함으로써 문제를 해결할 수 있다.

  • DB 테이블에서 외래키를 사용해서 참조하는것과 비슷하게, 다른 애그리거트를 참조할 때 ID 참조를 사용한다.
    • 단, 애그리거트 내의 엔티티를 참조할 때는 객체 레퍼런스로 참조한다.
    • 즉, 모든 객체가 참조로 연결되지 않고 한 애그리거트에 속한 객체들만 참조로 연결된다.
public class Order {
	private Orderer orderer;
    ...
}

public class Orderer {
	private MemberId memberId; //**
    private String name;
    ...
}

public class Member {
	private MemberId id; // **
  	...
}
  • 장점
    • 애그리거트의 경계를 명확히 할 수 있다.
    • 애그리거트간의 물리적 연결을 제거하기 때문에 모델의 복잡도를 낮춰준다.
      • 애그리거트 간의 의존을 제거하므로 응집도를 높여주는 효과도 있다.
    • 구현 복잡도가 낮아진다.
      • 타 애그리거트를 직접 참조하지 않으므로 지연 로딩, 즉시로딩을 고민하지 않아도 된다.
      • 참조하는 애그리거트가 필요한 경우 응용서비스에서 아이디를 이용해 로딩하면 된다.
    • 한 애그리거트에서 다른 애그리거트를 수정하는 문제를 원천적으로 방지할 수 있다.
    • 애그리거트별 다른 구현 기술 선택이 가능해진다.
      • 예) 중요 데이터인 주문 애그리거트는 RDBMS에, 조회 성능이 중요한 상품 애그리거트는 NoSQL에 저장할 수 있다.
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.changeShipingInfo(newShippingInfo);
        if (useNewShippingAddrAsMemberAddr) {
        	// ID를 이용해서 참조하는 애그리거트를 구한다.
            Customer customer = customerRepository.findById(order.getOderer().getCustomerId());
            customer.changeAddress(newShippingInfo.getAddress());
        }
    }
}

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

  • 다른 애그리거트를 ID로 참조하면, 참조하는 여러 애그리거트를 읽어야할 때 조회 속도가 문제가 될 수 있다.
    • 예) 주문 목록을 보여줄 때 상품 애그리거트와 회원 애그리거트를 함께 읽어야 하는 경우
      • 쿼리에서 조인을 이용해 한번에 모든 데이터를 가져올 수 있음에도, 주문마다 상품 정보를 읽어오는 쿼리를 실행하게 된다.
        • 즉, 주문 개수가 10개면, 주문을 읽어오기 위한 1번의 쿼리와 주문별로 각 상품을 읽어오기 위한 10번의 쿼리가 실행되게 된다.
        • 이 같은 문제를 N+1문제라고 부른다.
Customer customer = customerRepository.findById(ordererId);
List<Order> orders = orderRepository.findByOrderer(ordereId);
List<OrderView> dtos = orderers.stream()
		.map(order -> {
        	PorductId prodId = order.getOrderLines().get(0).getProductId();
            // 각 주문마다 첫 번째 주문 상품 정보 로딩 위한 쿼리 실행
            Product product = productRepository.findById(prodId);
            return new OrderView(order, customer, product);
        }).collect(toList());
  • N+1문제
    • 조회 대상이 N개일 때, N개를 읽어오는 한 번의 쿼리(1)와 연관된 데이터를 읽어오는 쿼리를 N번 실행한다 해서 이를 N+1조회 문제라고 부름.
      • ID를 이용한 애그리거트 참조는 지연 로딩과 같은 효과를 만드는데, 지연로딩과 관련된 대표적인 문제가 N+1 문제이다.
    • 더 많은 쿼리를 실행해 조회 속도가 느려진다.
      • 이 문제를 해결하기 위해서는 조인을 사용하도록 해야 한다.
        • 따라서, ID 참조방식을 객체 참조 방식으로 바꾸고 즉시 로딩을 사용하도록 매핑 설정을 바꾸면 해당 N+1 문제는 해결되게 된다.
  • ID 참조 방식을 사용하면서 N+1 문제를 해결하려면, 조회 전용 쿼리를 사용하면 된다.

조회전용 쿼리

@Repository
public clss JpaOrderViewDao implements OrderViewDao {
	@PersistenceContext
    private EntityManager em;
    
    @Override
    public List<OrderView> selectByOrderer(String ordererId) {
    	String selectQueiry =
        	"select new com.myshop.order.application.dto.OrderView(o, m, p) " +
            "from Order o join o.orderLines ol, Member m, Product p " +
            "where o.orderer.memberId.id = :orderId " +
            "and o.orderer.memberId = m.id " +
            "and index(ol) = 0 " +
            "and ol.productId = p.id " +
            "order by o.number.number desc";
        TypedQuery<OrderView> query =
        	em.createQuery(selectQuery, OrderView.class);
        query.setParameter("ordererId", ordererId);
        return query.getResultList();
    }
}
  • JPQL을 사용해서 Order, Member, Product 애그리거트를 조인해 한 번의 쿼리로 로딩한다.
    • 즉시 로딩, 지연로딩과 같은 로딩 전략을 고려하지 않고 바로 한 번의 쿼리로 데이터를 로딩할 수 있다.
    • 이 처럼 복잡하거나 SQL 특화된 기능을 사용해야 한다면, 조회만 MyBatis 같은 기술을 사용할 수 있다.
  • 애그리거트마다 서로 다른 저장소를 사용하는 경우
    • 한 번의 쿼리로 관련 애그리거트를 조회할 수 없다.
    • 조회성능 향상을 위해, 캐시를 적용하거나 조회 전용 저장소를 따로 구성한다.
      • 단점: 코드가 복잡해짐
      • 장점: 시스템 처리량 향상 가능
    • 한대의 DB 장비로 대응할 수 없는 수준의 트래픽이 발생하는 경우, 캐시나 조회 전용 저장소는 필수선택.

애그리거트 간 집합 연관

  • 애그리거트간 1:N, M:N 연관
    • 컬렉션을 통해 표기

1:N 관계

  • ex) 카테고리 - 상품
    • 카테고리 입장에서, 한 카테고리에 한 개 이상의 상품이 속할 수 있음.
    • 따라서, 카테고리와 상품은 1:N 관계.
public class Category {

	private Set<Product> products; // 다른 애그리거트에 대한 1:N 연관
    ...
}
  • 개념적으로 존재하는 애그리거트간의 1:N 연관을 구현에 반영하는 것이, 실제 요구사항 충족과 상관없는 경우가 있다.

특정 카테고리에 있는 상품 목록을 보여주는 요구사항

  • 해당 기능을 카테고리 입장에서 1:N 연관을 이용해 구현하면 다음과 같이 코드가 전개된다.
    • 해당 코드를 실제 DBMS와 연동해 구현하면, Category에 속한 모든 Product를 조회하게 된다.
      • 이는 Product의 갯수에 따라 성능을 급격하게 저하시키게 된다.
    • 개념적으로는 1:N의 연관이 있더라도, 이런 성능상의 문제 땜누에 애그리거트 간의 1:N 연관을 실제 구현에 반영하는 경우는 드물다.
public class Category {
	private Set<Product> products;
    
    public List<Product> getProducts(int page, int size) {
    	List<Product> sortedProducts = sortById(products);
        return sortedProducts.subList((page - 1) * size, page * size);
    }
    ...
  • 대신, 상품 입장에서 자신이 속한 카테고리를 가져, N:1로 구현할 수 있다.
    • 즉, ProductCategory로의 연관을 추가하고, 그 연관을 이용해 특정 Category에 속한 Product목록을 구할 수 있다.
public class Product { 
	...
    private CategoryId category;
    ...
}

public class ProductListService {
	public Page<Product> getProductOfCategory(Long categoryId, int page, int size) {
    	Category category = categoryRepository.findById(categoryId);
        checkCategory(category);
        List<Product> products =
        	productRepository.findByCategoryId(category.getId(), page, size);
        int totalCount = productRepository.countsByCategroyId(category.getId());
        return new Page(page, size, totalCount, products);
    }
...        

M:N 관계

  • 개념적으로 양쪽 애그리거트에 컬렉션으로 연관을 만든다.
    • 상품이 여러 카테고리에 속할 수 있다고 가정하면, 카테고리와 상품은 M:N 연관을 맺는다.
    • 실제 요구사항을 고려해서 M:N 연관을 구현에 포함시킬지 여부를 결정해야 한다.
  • 보통 특정 카테고리에 속한 상품 목록을 보여줄 때, 목록 화면에서 각 상품이 속한 모든 카테고리를 상품 정보에 표시하지는 않는다.
    • 제품이 속한 모든 카테고리가 필요한 화면은 상품 상세화면이다.
    • 이 요구사항을 고려할 때, 카테고리에서 상품으로의 집합연관은 필요하지 않다.
  • 즉, 개념적으로는 상품과 카테고리의 M:N 양방향 연관이 존재하지만, 실제 구현에서는 상품에서 카테고리로의 단방향 M:N 연관만 적용하면 된다.
public class Product { 
	private Set<CategoryId> categoryIds;
    ...
  • RDBMS
    • 조인 테이블을 사용하여 구현할 수 있다.
  • JPA
    • 매핑 설정을 이용하여 M:N 단방향 연관을 구현할 수 있다.
      • 카테고리 ID 목록을 보관하기 위해 밸류 타입에 대한 컬랙션 매핑을 이용
      • 이 매핑을 사용하는 경우, JPQL의 member of 연산자를 이용해 특정 Category에 속한 Product 목록을 구하는 기능을 구현할 수 있다.
@Entity
@Table(name = "product")
public class Product {
	@EmbeddedId
    private ProductId id;
    
    @ElementCollection
    @CollectionTable(name = "product_category",
    				joinColumns = @JoinColumn(name = "product_id"))
    private Set<CategoryId> categoryIds;
    ...
@Repository
public class JpaProductRepository implements ProductRepository {
	@PersistenceContext
    private ENtityManager entityManager;
    
    @Override
    public List<Product> findByCategoryId(CategoryId categoryId, int page, int size) {
    	TypedQuery<Product> query = entityManager.createQuery(
        	"select p from Product p " +
            "where :catId member of p.categoryIds order by p.id.id desc ",
            Product.class);
        query.setParaemntet("catId", categoryId);
        query.setFirstResult((page - 1) * size);
        query.setMaxResults(size);
        retury query.getResultList();
     }
     ...

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

  • 온라인 쇼핑몰에서 고객이 여러 차례 신고를 해서 특정 상점이 더 이상 상품을 등록하지 못하도록 차단한 상태
    • 상점 계정이 차단 상태가 아닌 경우에만 상품을 생성할 수 있도록 구현할 수 있다.
public class RegisterProductService {
	public ProductId registerNewProduct(NewProductRequest req) {
    	Store account = accountRepository.findStoreById(req.getStoreId());
        checkNull(account);
        if (account.isBlocked()) {
        	throw new StroeBlockedException();
        }
        ProductId id = productRepository.nextId();
        Product product = new Product(id, account.getId(), ... 생략);
        productRepository.save(product);
        return id;
    }
    ...
}
       
  • 이 코드는 Product를 생성할 수 있는지 판단하는 코드와 생성하는 코드가 분리되어 있다.
    • 주요한 도메인 로직 처리가 응용 서비스에 노출되어있다.
    • StoreProduct를 생성하는 것은 논리적으로 하나의 도메인 기능인데, 이 도메인 기능을 응용 서비스에서 구현하고 있다.
      • 이 기능을 구현하기에 더 좋은 장소는 Store 애그리거트이다.

팩토리 역할을 수행하는 도메인 구현

public class Stroe extends Member {
	public Product createProduct(ProductId newProductId, ...생략) {
    	if (isBlocked()) throw new StoreBlockedExcpetion();
        return new Product(newProductId, getId(), ...생략);
    }
}

public class RegisterProductService {
	public ProductId registerNewProduct(NewProductRequest req) {
    	Store account = accountRepository.findStoreById(req.getStoreId());
        checkNull(account);
        ProductId id = productRepository.nextId();
        Product product = account.createProduct(id, ...생략);
        productRepository.save(product);
        return id;
    }
}
  • Store 애그리거트의 createProduct()
    • Product 애그리거트를 생성하는 팩토리 역할 수행
    • 중요한 도메인 로직 구현
  • 응용 서비스
    • 팩토리 기능을 이용해서 Poduct를 생성한다.
  • 차이점
    • 응용 서비스에서 더 이상 Store의 상태를 확인하지 않는다.
      • Product 생성 가능 여부 확인 가능 로직 변경시, Store만 변경하면 된다.
        • 결과적으로, 도메인의 응집도가 증가했다.
  • 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면, 애그리거트에 팩토리 메서드를 구현하는 것을 고려해보자.
    • Product의 경우 Store의 식별자를 필요로 한다.
    • 즉, StoreProduct를 생성하는 팩토리 메서드를 추가하면, Product를 생성할 때 필요한 데이터의 일부를 직접 제공하면서 중요한 도메인 로직을 함께 구현할 수 있게 된다.

Reference

  • [책] DDD START!: 도메인 주도 설계 구현과 핵심 개념 익히기 - 최범균
profile
Good Luck!

0개의 댓글