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

겔로그·2023년 10월 4일
0

3.1 애그리거트

간단한 시스템에서 상위 수준의 개념을 이용한 모델 정리를 진행한다면 전반적인 관계를 이해하는데 도움이 됩니다. 하지만 개별 객체 수준을 본다면 어떨까요?

다음 그림의 사진만 보더라도 개별 객체 수준에서 모델을 바라볼 경우 관계를 파악하기 어렵다는 것을 알 수 있습니다. 주요 도메인 요소 간의 관계를 파악하기 어렵다는 것은 코드를 변경하고 확장하는 것이 어려워진다는 것을 의미합니다.

복잡한 도메인을 이해하고 관리하기 쉬운 단위로 만드려면 상위 수준에서 모델을 조망할 수 있는 방법이 필요한데, 그 방법이 애그리거트입니다.

[그림 3.3]은 [그림 3.2]의 모델을 애그리거트 단위로 묶어서 다시 표현한 것입니다. 다음과 같이 상위 수준을 애그리거트 단위로 묶는다면 모델 간의 관계를 개별 모델 수준과 상위 수준에서 모두 이해할 수 있습니다.

애그리거트 특징

  • 한 애그리거트에 속한 객체는 다른 애그리거트 속하지 않습니다.
  • 애그리거트는 애그리거트간 경계를 갖습니다.
  • 애그리거트 경계를 설정하는 것은 도메인 규칙과 요구사항을 기반으로 진행됩니다.

애그리거트 사용시 장점

  • 모델을 이해하는데 도움을 줍니다.
  • 모델의 일관성을 관리하는 기준이 됩니다.
  • 복잡한 도메인을 단순한 구조로 만들어줍니다.

애그리거트 정의 간 주의사항 (Ex) 상품, 리뷰)

애그리거트 정의간 'A가 B를 갖는다'로 설계할 수 있는 요구사항이 있다면 A와 B를 한 애그리거트로 묶어서 생각하기 쉬운데 이런 요구사항이 있다고 하더도 반드시 A와 B가 한 애그리거트에 속한다는 것을 의미하는 것은 아닙니다.

대표적인 예시가 상품과 리뷰인데, 상품 상세 페이지에 들어갈 경우 각 상품에 대한 고객의 리뷰를 확인할 수 있습니다. 이 때, 상품에 리뷰가 포함된다고 생각해 하나의 애그리거트로 구성할 수 있지만, 좀 더 생각해보면 상품과 리뷰는 함께 생성되지도 변경되지 않는 것을 알 수 있습니다.

상품을 변경하는 것의 주체가 상품 담당자라면 리뷰를 생성하고 변경하는 주체는 고객이며, 두 객첵간의 변화가 서로에게 영향을 주지 않기 때문에 한 애그리거트에 속하기 보다는 각자 다른 애그리거트에 속하는 것이 맞습니다.

3.2 애그리거트 루트

애그리거트에 속한 모든 객체가 일관된 상태를 유지하기 위해선 애그리거트 전체를 관리할 주체가 필요한데, 이를 애그리거트 루트 엔티티라 부릅니다.

애그리거트 루트 엔티티는 애그리거트의 대표 엔티티이며 애그리거트에 속한 객체는 애그리거트 루트 엔티티에 직접 또는 간접적으로 속하게 됩니다.

도메인 규칙과 일관성

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

public class Order {
	// 애그리거트 루트는 도메인 규칙을 구현한 기능을 제공한다.
    public void changeShippingInfo(ShippingInfo newShippingInfo) {
    	verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
    }
    
    private void verifyNotYetShipped() {
    	if(state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
        	throw new IllegalStateException("already shipped");
    }
}

애그리거트 외부에서는 애그리거트에 속한 객체를 직접 변경하면 안됩니다. 애그리거트 루트가 강제하는 규칙을 적용할 수 없기 때문에 모델의 일관성을 깨는 원인이 됩니다.

ShippingInfo si = order.getShippingInfo();
si.setAddress(newAddress);

다음과 같이 직접 정보를 변경할 경우 업무 규칙을 무시하고 직접 DB 테이블의 데이터를 수정하는 것과 같은 결과를 만들어 논리적인 데이터 일관성이 깨지게 되는 것입니다.

일관성을 지키기 위해 상태 확인 로직을 응용 서비스에 구현할수도 있으나 이는 여러 응용 서비스 로직에 중복으로 구현될 가능성이 높아져 유지 보수에 도음이 되지 않습니다.

습관화해야할 부분

  • 단순히 필드를 변경하는 set 메서드를 public 으로 만들지 않습니다.
  • 밸류 타입은 불변으로 구현합니다.
public void setName(String name) {
	this.name = name;
}

트랜잭션 범위

  • 트랜잭션의 범위는 작을수록 좋습니다.
  • 잠금 대상이 많아질수록 동시에 처리할 수 있는 트랜잭션 개수가 줄어든다는 것을 의미하며 이것은 전체적인 성능을 떨어뜨리는 문제를 발생시킵니다.
  • 가능한 한 트랜잭션의 범위는 한 개의 애그리거트만 수정하도록 정의해야 합니다.
    • 특이 케이스로 두 개 이상의 애그리거트를 한 트랜잭션에서 관리해야 할 수도 있습니다.

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

고객이 특정 상점을 여러 차례 신고해 해당 상점이 더 이상 물건을 등록하지 못하도록 차단한 상태라고 해봅시다.

public class RegisterProductService {
	public ProductId registerNewProduct(NewProductRequest req) {
		Store account = accountRepository.findStoreById(req.getStoreId());
		checkNull(account);
		if (!account.isBlocked()) {
			throw new StoreBlockedException();
		}
		ProductId id = productRepository.nextId();
		Product product = new Product(id, account.getId(), ...);
		productRepository.save(product);
		return id;
	}
}

코드에 잘못된 부분은 없어 보이지만 도메인 로직 처리가 응용 서비스에 노출되었습니다. Store가 Product를 생성할 수 있는지를 판단하고 Product를 생성하는 것은 논리적으로 하나의 도메인 기능인데 해당 기능을 응용 서비스에서 구현하고 있습니다.

public class Store { 
	public Product createProduct(ProductId newProductId, ...생략) {
    	if (isBlocked()) throw new StoreBlockedException();
        return new Product(newProductId, getId(), ...생략);
    }
}

Store 애그리거트에 Product 애그리거트를 생성하는 팩토리 역할을 할 수 있도록 구성하였습니다.

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, account.getId(), ...); // Store에서 직접 생성
		productRepository.save(product);
		return id;
	}
}

Store에서 팩토리 역할을 수행하며 다음과 같은 차이점이 발생했습니다.

  • 응용 서비스에서 더 이상 Store의 상태를 확인하지 않습니다.
  • Store 관련 로직은 전부 Store에서 구현됩니다.
  • Product 생성 관련 로직 또한 Store 내부에서 변경됩니다.
  • 도메인의 응집도가 높아집니다

Reference

https://incheol-jung.gitbook.io/docs/study/ddd-start/3

profile
Gelog 나쁜 것만 드려요~

0개의 댓글