도메인 주도 개발 시작하기 : 3장 애그리거트

일단 해볼게·2025년 7월 20일
0

book

목록 보기
20/31

객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재

3.1 애그리거트

  • 상위 수준에서 모델을 정리하면 도메인 모델의 복잡한 관계를 이해하는 데 도움이 된다.

  • 개별 객체 수준에서 모델을 바라보면 상위 수준에서 관계를 파악하기 어렵다.

  • 애그리거트

    • 복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만든다.

    • 복잡도가 낮아지는 만큼 도메인 기능을 확장하고 변경하는 데 필요한 노력도 줄어든다.

    • 한 애그리거트에 속한 객체는 유사하거나 동일한 라이프 사이클을 갖는다.

    • ‘A가 B를 갖는다’로 해석할 수 있는 요구사항이 있다고 하더라도 이것이 반드시 A와 B가 한 애그리거트에 속한다는 것을 의미하는 것은 아니다.

      • 예시) 상품과 리뷰
        • 상품 상세 페이지에 들어가면 상품 상세 정보와 함께 리뷰 내용을 보여줘야 한다는 요구사항이 있을 때 Product 엔티티와 Review 엔티티가 한 애그리거트에 속한다고 생각할 수 있다. 하지만 Product와 Review는 함께 생성되지 않고, 함께 변경되지도 않는다.
          • 상품이 생성되어도 리뷰는 생성되지 않을 수 있다.

3.2 애그리거트 루트

  • 도메인 규칙을 지키려면 애그리거트에 속한 모든 객체가 정상 상태를 가져야한다.

    • OrderLine을 변경하면 Order의 totalAmounts도 다시 계산해서 총 금액이 맞아야한다.
  • 애그리거트 루트

    • 애그리거트 전체를 관리할 주체

    • 애그리거트 외부에서 애그리거트에 속한 객체를 직접 변경하면 안된다.

      • 모델의 일관성을 깨는 원인이 된다.
        • 일관성을 지키기 위해 응용 서비스에 구현할 수도 있지만, 동일한 검사 로직을 여러 응용 서비스에서 중복으로 구현할 가능성이 높아져 유지 보수에 도움이 되지 않는다.
    • 불필요한 중복을 피하고 애그리거트 루트를 통해서만 도메인 로직을 구현하게 만드려면?

      • set 메서드를 public으로 만들지 않는다.
        • 도메인의 의미나 의도를 표현하지 못한다.
      • 밸류 타입은 불변으로 구현한다.
        public class Order {
        	private Shippinginfo shippinginfo;
        	public void changeShippingInfo(ShippingInfo newShippinglnfo) {
        		verifyNotYetShipped();
        		setShippinglnfo(newShippinglnfo);
        }
        
        	// set 메서드의 접근 허용 범위는 private
        	private void setShippingInfo(ShippingInfo newShippinglnfo) {
        	// 밸류가 불변이면 새로운 객체를 할당해서 값을 변경해야 한다.
        	this.shippinginfo = newShippinglnfo;
        }
  • 애그리거트 루트는 애그리거트 내부의 다른 객체를 조합해서 기능을 완성한다.

    • 애그리거트 외부에서 애그리거트를 변경할 수 없도록 불변으로 구현하자.
      • 불변으로 구현할 수 없다면, protected 범위로 한정해서 외부에서 실행할 수 없도록 제한
  • 트랜잭션 범위는 작을수록 좋다.

    • 여러 개의 테이블을 수정하면 트랜잭션 충돌을 막기 위해 잠그는 대상이 많아진다.
      • 그만큼 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다. → 전체적인 성능을 떨어트린다.
    • 애그리거트 내부에서 다른 애그리거트의 상태를 변경하는 기능을 실행하면 안 된다.
      • 예를 들어 배송지 정보를 변경하면서 동시에 배송지 정보를 회원의 주소로 설정하는 기능이 있다고 해보자.
        public class Order {
        	private Orderer orderer;
        
        	public void shipTo(ShippingInfo newShippinglnfo,
        		boolean useNewShippingAddrAsMemberAddr) {
        			verifyNotYetShipped();
        			setShippinglnfo(newShippinglnfo);
        			if (useNewShippingAddrAsMemberAddr) {
        			// 다른 애그리거트의 상태를 변경하면 안 됨!
        				orderer.getMember().changeAddress(newShippingInfo.getAddress());
        }
        • 자신의 책임 범위를 넘어 다른 애그리거트의 상태까지 관리하는 꼴이 된다.
          • 애그리거트는 최대한 서로 독립적이어야한다. 그렇지 않으면 애그리거트 간 결합도가 높아진다.
        • 부득이하게 한 트랜잭션으로 두 개 이상의 애그리거트를 수정해야 한다면 애그리거트에서 다른 애그리거트를 직접 수정하지 말고 응용 서비스에서 두 애그리거트를 수정하도록 구현한다.
          public class ChangeOrderService {
          // 응용 서비스에서 각 애그리거트의 상태를 변경한다.
          	@Transactional
          	public void changeShipping!nfo(OrderId id. Shippinginfo newShippinglnfo,
          	boolean useNewShippingAddrAsMemberAddr) {
          		Order order = orderRepository.findbyld(id);
          		if (order == null) throw new OrderNotFoundException();
          		order.shipTo(newShippinglnfo);
          		if (useNewShippingAsMemberAddr) {
          			Member member = findMember(order.getOrdererO);
          			member.changeAddress(newShippinglnfo.getAddressO);
          	}
          }
          ...
        • 도메인 이벤트를 사용하면 한 트랜잭션에서 한 개의 애그리거트를 수정하면서도 동기나 비동기로 다른 애그리거트의 상태를 변경하는 코드를 작성할 수 있다.

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

  • 객체의 영속성을 처리하는 리포지터리는 애그리거트 단위로 존재
    • Order, OrderLine을 위한 리포지터리는 각각 만들지 않는다.
    • Order 애그리거트를 저장할 때 애그리거트 루트와 매핑되는 테이블뿐만 아니라 애그리거트에 속한 모든 구성 요소에 매핑된 테이블에 데이터를 저장해야한다.
      • 모든 변경을 원자적으로 저장소에 반영

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

  • 한 애그리거트 내부에서 다른 애그리거트 객체에 접근할 수 있으면 다른 애그리거트의 상태를 쉽게 변경할 수 있게 된다.
    • Orderer 내부에 Member 객체를 가지고 있는 경우
      • order.getOrderer().getMember().getId()
    • 문제점
      • 편한 탐색 오용
        • 다른 애그리거트에 쉽게 접근 및 수정 가능
      • 성능에 대한 고민
        • 지연로딩/즉시로딩 전략 결정
          • N+1 문제 발생 위험
      • 확장 어려움
        • 트래픽이 늘어나면 도메인 별로 시스템을 분리 시 어려움
    • 해결 방법
      • 객체가 아닌 외래키 참조
        • Member 객체가 아닌 memberId 참조
        • 물리적 연결 제거 → 모델의 복잡도 낮춰준다.
        • 애그리거트별로 다른 구현 기술 사용 가능
          • Order는 오라클, Product는 몽고DB
    • ID 참조 방식에서 N+1 문제가 발생한다면?
      • 조회를 위한 별도의 DAO를 만들고 조인을 이용해 한 번의 쿼리로 조회

        @Repository
        public class JpaOrderViewDao implements OrderViewDao {
        	@PersistenceContext
        	private EntityManager em;
        
        	@0verride
        	public List<OrderView> selectByOrderer(String ordererld) {
        		String selectQuery =
        			"select new com.myshop.order.application.dto.OrderViewCOj m, p) "+
        			"from Order o join o.orderLines ol. Member m. Product p " +
        			"where o.orderer.memberld .id = :ordererld "+
        			"and o.orderer.memberld = m.id "+
        			"and index(ol) = 0 " +
        			"and ol.productld = p.id "+
        			"order by o.number.number desc";
        		
        		TypedQuery<OrderView> query =
        			em.createQuery(selectQuery, OrderView.class);
        
        		query.setParameter("ordererld", ordererld);
        
        		return q니ery.getResultl_ist();
        	}
        }
      • 한 대의 DB 장비로 대응할 수 없는 수준의 트래픽이 발생하는 경우 조회 성능을 높이기 위해 캐시를 적용하거나 조회 전용 저장소를 따로 구성

3.5 애그리거트 간 집합 연관

  • 카테고리 - 상품 관계인 경우
    • 카테고리가 상품을 가지는게 아닌 상품이 카테고리를 가지도록 한다.
      public class Product {
      		private CategoryId categoryId;
      }
    • 양방향보단 단방향

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

  • 애그리거트가 갖고 있는 데이터를 이용해서 다른 애그리거트를 생성해야 한다면 애그리거트에 팩토리 메서드를 구현하는 것을 고려해보자
    public class RegisterProductService {
    	public Productld registerNewProduct(NewProductRequest req) {
    		Store store = storeRepository.findByld(req.getStoreldO);
    		checkNull(store);
    		Productld id = productRepository.nextId()
    		
    		// 생성하는 메서드에서 검증까지 시도
    		Product product = store.createProduct(id, ...생략);
    		
    		productRepository.save(product);
    		return id;
    	}
    }
    • Product의 경우 제품을 생성한 Store의 식별자를 필요로 한다.
      • 즉 Store의 데이터를 이용해서 Product를 생성한다. 게다가 Product를 생성 할 수 있는 조건을 판단할 때 Store의 상태를 이용한다.
      • 따라서 Store에 Product를 생성하는 팩토리 메서드를 추가하면 Product를 생성할 때 필요한 데이터의 일부를 직접 제공하면서 동
        시에 중요한 도메인 로직을 함께 구현
    • Store 애그리거트가 Product 애그리거트를 생성할 때 많은 정보를 알아야 한다면 ProductFactory에 위임하는 방법도 있다.
profile
시도하고 More Do하는 백엔드 개발자입니다.

0개의 댓글