본문 바로가기
도메인 주도 설계

Value Object로서 Model Identity를 사용하는 3가지 이유[번역]

by simplify-len 2021. 5. 17.

원본 - https://buildplease.com/pages/vo-ids/

 

3 Reasons to Model Identity as a Value Object

On of the defining characteristics of an Entity is that it has identity. From the Blue Book: “Some objects are not defined primarily by their attributes. They represent a thread of identity that runs through time and often across distinct representations

buildplease.com

 

Photo by Christin Hume on Unsplash

엔티티가 가진 중요한 특성 중 하나는 Identity 를 갖는 다는 것입니다. 

 

“Some objects are not defined primarily by their attributes. They represent a thread of identity that runs through time and often across distinct representations… An object defined primarily by its identity is called an ENTITY” (Evans, 91)

일부 개체는 주로 속성으로 정의되지 않습니다. 그들은 시간을 거쳐 종종 별개의 표현에 걸쳐 실행되는 정체성의 스레드를 나타냅니다... 주로 ID로 정의된 개체를 ENTITY라고 합니다.


Identity 을 표현하는 방법에는 여러 가지가 있습니다. CRUD 세계에서는 SQL 테이블에 기본 키로 저장된 Guid 또는 int를 사용할 수 있습니다. DDD 구현에서는 Value Object를 엔터티의 식별자로 사용하는 다른 패턴을 볼 수 있습니다. 그럼 여기서 중요한 것은 무엇인가?

 

필수 사항은 아니지만 장기적으로 모델에 도움이 될 수 있는 ID에 값 개체를 사용하는 데에는 몇 가지 이유가 있습니다.

 

1: Allowing for future changes to the underlying identity values without “shotgun surgery”

1."shotgun surgery" 없이 기초를 이루는 identity를 미래에 변경을 허용합니다.

 

항상 "Soft reference" 를 통해 수행되는 복수의 AggreateRoot를 서로 관련이 있을 때, 관련 AggregateRoot의 ID에 대한 참조만 보유합니다. AggregateRoot는 도메인 모델에서 일관성 경계(consistency boundary)를 나타내기 때문에 중요합니다. 즉, 두 AggregateRoot는 서로 완전히 일치하지 않습니다. 이것이 우리가 DDD를 사용하는 이유 중 하나 입니다. 큰 Entity Graph를 검색하고 저장하는 것은 확장 가능하지 않기 때문입니다. Aggregate Root 내부에 "Hard reference(완전히 인스턴스화된 Aggregate)"가 있을 경우, AggregateRoot 외부가 세계와 강력한 일관성이 있음을 나타냈으면 이는 도메인을 모델링하는 데 실수를 했음을 의미합니다. 

 

 하나의 Aggregate Root에서 다른 Aggregate Root로 Soft Reference를 만들려면 다른 AggregateRoot의 ID를 참조하여 수행합니다. 이것이 int유형(int beanId)으로 모델링된 경우 Aggregate Root Bean의 유형을 변경하기로 결정하면 어떻게 됩니까? 

 

다음은 FirstPopCoffee Co. 에서 로스팅할 녹색 커피 원두 배치를 나타내는 배치입니다.

 

    public class Batch : Entity
    { 
        public BatchId BatchId { get; private set; }
        public RoastDayId RoastDayId { get; set; }
        public BeanId BeanId { get; private set; } // soft reference to Aggregate Root: Bean
        public BatchQuantity Quantity { get; private set; }
        ...
        

그리고 `Bean` AggregateRoot 입니다.

 

    public class Bean : AggregateRootEntity
    {
        public BeanId BeanId { get; private set; }
        public Description Description { get; private set; }
        public string UnitOfMeasure { get; private set; }
        public decimal UnitPrice { get; private set; }
        public OnHandQuantity OnHandQuantity  { get; private set; }
        ...

그리고 `BeanId`라는 ValueObject입니다.

 

    public class BeanId : ValueType<BeanId>
    {
        public readonly Guid Id;

        public BeanId()
        {
            Id = Guid.NewGuid();
        }

        public BeanId(Guid id)
        {
            Id = id;
        }

        protected override IEnumerable<object> GetAllAttributesToBeUsedForEquality()
        {
            return new List<object>() { Id };
        }

        public override string ToString()
        {
            return Id.ToString();
        }
    }

 

만약, 우리가 BeanId 를 Guid(저장 방식 중 하나?) 로 취급한다면?

    public class Batch : Entity
    { 
        public Guid BatchId { get; private set; }
        ...

 

그리고, 이걸 참조하는 코드는 아래와 같습니다.

    public class Bean : AggregateRootEntity
    {
        public Guid BeanId { get; private set; }
        ...

언젠가 (예상한다면) 이것을 int로 변경하면 두 엔티티를 모두 변경해야합니다. 이것은 Bean Entity의 클라이언트에서 의도하지 않은 Shotgun Surgery을 의미 할 수 있습니다.

 

2: You can guard against invalid values for identity

2. ID에 대한 유효하지 않은 값으로부터 보호 할 수 있습니다.

특정 ID에 대해 유효한 int 값이 무엇인지에 대해서 몇 가지 제한이 있을 수 있습니다. 다음과 같은 보호코드를 추가해야 할 수도 있습니다.

    public class BeanId : ValueType<BeanId>
    {
        public readonly int Id;

        public BeanId(int id)
        {
        	if (id < 0) throw new ArgumentException("id must be a positive integer");
            Id = id;
        }
    ...

3: The compiler can help you spot mistakes

3 : 컴파일러는 실수를 발견하는 데 도움이 될 수 있습니다.

 

 동일한 Model에서 작업하는 개발자 팀이 있는 경우 컴파일러를 유리하게 사용하여 코드 검토를 지나칠 수있는 상황을 방지하는 것에 도움이 될 수 있습니다.

 

예를 들어,아래 코드에서 어떤 문제를 즉시 발견 하셨나요?

public class Order {
	public readonly int OrderId;
	public readonly List<int> ProductIds;
	...

	public void AddProduct(int productId) {
		ProductIds.Add(productId);
	}
}

Order order = new Order();
addProduct(order.OrderId)

order.OrderId는 ProductId와 같은 int 이므로 성공적으로 컴파일되지만 ProductIds Collections에 잘못된 Identity를 추가합니다.

 

요약

 

엔티티 아이덴티티를 모델링하기 위해 Value Object를 사용하는 몇 가지 이유를 살펴 보았습니다. 필수 사항은 아니지만 장기적으로 도움이 될 수있는 몇 가지 이점이 있습니다.

 

댓글