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

Domain-Driven Design: The Identifier Type Pattern[번역]

by simplify-len 2021. 5. 24.

원문 - https://medium.com/@gara.mohamed/domain-driven-design-the-identifier-type-pattern-d86fd3c128b3

 

Domain-Driven Design: The Identifier Type Pattern

A Typescript version

medium.com

 

Photo by Victor Dementiev on Unsplash

Introduction

Entity와 ValueObject 가 다른 점은 식별자이다.

Entity는 식별자를 포함 내재하고 있지만, ValueObject는 식별자가 없다.

 

Entity의 식별자는 다음 기술들 중에 하나로 설명할 수 있다.

- Primitive Type

- Special type defined as a value object or an alias type

 

도메인 주도 설계 원칙은 2번째 기술을 크게 옹호하고 있습니다. 그럼 어떤 장점이 있는지 살펴봅시다.

 

Primitive Identifier vs. Type Identifier

적극적으로 ID 유형의 장점을 설명하기 전에, 먼저 분석에 사용할 코드베이스를 소개합니다.

 

우리의 예는 매우 간단합니다. 두 개의 모듈이 있습니다.

 

Version 1

 

첫번째 버전에서 우리는 문자열 CustomID type을 사용할 것입니다.

 

export class Customer {
    id: string;
    name: string;
    ...
}
export class Order {
    id: string;
    customerId: string;
    creatorId: string;
    ...
}

 

Version 2

 

두번째 버전에서는, 우리는 별칭 타입의 CustomerId 를 custom ID 로 사용하겠습니다.

 

export class Customer {
    id: CustomerId;
    name: string;
    ...
}

export type CustomerId = string;
import { CustomerId } from './customer';
import { UserId } from './user';

export class Order {
    id: OrderId;
    customerId: CustomerId;
    creatorId: UserId;
    ...
}
    
export type OrderId = string;

 

자, 이제 위 예시에 대해서 조금 더 깊게 파헤쳐 봅시다.

 

Why we prefer the dedicated type pattern(왜 특별한 타입의 패턴을 좋아할까)?

 

아래에 특별한 타입의 식별자가 왜 좋은지 나열해보겠습니다.

 

잠재적인 버그를 피하자(Avoid the classic bug)

첫번째 버전에서 우리는  the primitive obsession code smell 맡을 수 있습니다. 이 코드는 다음과 같은 클래식한 버그를 일으킬 수 있습니다.

 

다음 아래와 같은 함수가 있다고 가정해보자

export function findOrdersBy(creatorId: string, customerId: string): Order[] {
    ...
}

 

만약 우리가 다음과 같은 실수를 할 경우, 컴파일러는 우리를 도와주지 못하고 이 문제는 컴파일 타임에도 잡히지 않을 것입니다.

const order: Order = findOrderById('123456'); 
const orders = findOrdersBy(order.customerId, order.creatorId);

 

Allow function/method overloading

이 섹션에서는 단일 매개 변수와 함께 이전 함수 findOrdersBy를 사용하려고합니다. 따라서 기본 ID를 사용하여 고객의 주문을 찾으려면 다음 메서드를 정의해야합니다.

export function findOrdersBy(customerId: string): Order[] {
    ...
}

이제 작성자별로 주문을 가져 오는 메소드를 오버로드하려는 경우 customerId와 creatorId의 유형이 동일한 유형이므로이를 수행 할 수 없습니다. 두 가지 옵션이 있습니다. 두 가지 다른 함수 (findOrdersByCustomer 및 findOrdersByCreator)를 사용하거나 오버로드를 사용합니다.

function findOrdersBy(customerId: CustomerId): Order[];
function findOrdersBy(creatorId: UserId): Order[];
function findOrdersBy(id): Order[] {
    ...
}

따라서 특별한 id 유형을 사용하면 덜 장황하고 우아한 코드를 작성할 수 있습니다.

const customerOrders = findOrdersBy(customerId); // instead of findOrdersByCustomer(customerId)
const userOrders = findOrdersBy(creatorId); // instead of findOrdersByCreator(creatorId)

 

논리적 의존성을 물리적으로 만듭니다.(Make logical dependencies physical)

첫 번째 버전에서 주문 클래스에는 customerId가 있습니다. 고객도 동일한 정보를 가지고 있습니다. 따라서 고객 클래스와 주문 클래스간에 논리적 관계가 있지만 두 클래스는 정적으로 독립적입니다.

 

In his famous book Clean Code, Uncle Bob states that:

If one module depends upon another, that dependency should be physical, not just logical.

두 번째 버전은 물리적 종속성을 생성하여 논리적 종속성을 구체화합니다. 따라서 고객 ID의 유형을 변경하면 Order 클래스에서 customerId 유형을 변경하는 것을 잊으면 컴파일러가 중지합니다. 하지만 첫 번째 버전에서는이 보안 기능이 없었습니다.

 

Validate id

ValueObject를 사용할 때 생성자에 가드 조건을 추가하여 id 값이 유효한지 확인할 수 있습니다.

export class CustomerId {
  private value: string;

  constructor(value: string) {
      if (!!value || value.length < 10) throw new Error(`The customer id <<${value}>> is not valid.`);
      
      this.value = value;
  }
}

이런 접근방법을 사용하여, 우리는 모든 id 인스턴스는 유효하다는 것을 확신할 수 있습니다.

 

Integrate applications

일반적으로 기업용 애플리케이션은 다른 애플리케이션과 격리되지 않습니다. 따라서 동일한 엔티티가 서로 다른 애플리케이션 사이를 이동할 수 있습니다. 때로는 동일한 엔티티 식별자가 다른 응용 프로그램에서 다른 형식과 표현을 갖습니다.

 

For example, our application can communicate with two other applications A and B:

  • In our application, the customer id is stored as a string.
  • In A, the id is stored as a number.
  • In B, the id is stored as a string of 15 digits with leading zeros.

값 개체를 사용하여 통합 API에서 고객 ID의 발생 횟수와 관계없이 단일 위치에서 형식 변환을 제어 할 수 있습니다.

 

export class CustomerId {
  private value: string;

  constructor(value: string | number) {
      if (!!value) throw new Error(`The customer id <<${value}>> is not valid.`);
        
      if (typeof value === 'number') {
          this.value = value.toString();  
      } else if (typeof value === 'string') {
          this.value = removeLeadingZeros(value);
      } else {
          throw new Error(`The customer id <<${value}>> has an unkown format.`)
      }
  }
}

만약 A또는 B의 어플리케이션에 Customer id 의 내용이 변경된다면- 우리는 위 코드만 변경하면 됩니다.

댓글