본문 바로가기
스터디/도메인 주도 개발 시작하기

[도메인 주도 개발 시작하기] 01. 도메인 모델 시작하기

by 디토20 2022. 6. 19.
반응형

 

 

 

[도메인 주도 개발 시작하기] 01. 도메인 모델 시작하기

 

 

1. 도메인

1.1 도메인이란?

- 온라인 서점 소프트웨어는 온라인으로 책을 판매하는 데 필요한 상품조회, 구매, 결제, 배송 추적 등의 기능을 제공해야 한다. 이때, '온라인 서점'은 소프트웨어로 해결하고자 하는 문제 영역, 즉 도메인에 해당된다. 한 도메인은 다시 하위 도메인으로 나눌 수 있다.

온라인 서점 도메인

  • 하나의 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
  • ex) 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.
  • 모든 도메인을 직접 구현해야 하는 것은 아니고 외부 업체의 시스템을 이용하기도 함. ex) PG사, 배송 업체

 

2. 도메인 모델

2.1 도메인 모델이란?

- 다양한 정의가 존재하지만, 기본적으로 도메인 모델은 도메인을 이해하기 위해 특정 도메인을 개념적으로 표현한 것

객체를 이용한 주문 도메인

 

 

다이어그램을 이용한 주문 도메인

 

 

  • 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유할 수 있다.
  • 도메인 모델은 정해진 형식이 없고, 도메인을 이해하는 데 도움이 된다면 그래프나 수학공식등을 활용해도 된다.

 

2.2 도메인 모델 설계시 주의 할 점

  • 각 하위 도메인이 다루는 영역은 서로 다르기 때문에 같은 용어라도 하위 도메인마다 의미가 달라질 수 있다.
  • 도메인에 따라 용어 의미가 결정되므로, 여러 하위 도메인을 하나의 다이어그램에 모델링 하면 안 된다.
  • 모델의 각 구성요소는 특정 도메인으로 한정될 때 비로소 의미가 완전해지기 때문에 각 하위 도메인아마 별도로 모델을 만들어야 한다.

 

2.3 도메인 모델 패턴

일반적인 애플리케이션의 아키텍처는 아래와 같이 네개의 영역으로 구성된다.

 

  • 표현(Presentation) 또는 UI : 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부시스템일 수도 있다.
  • 응용(Application) : 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합헤서 기능을 실행한다.
  • 도메인 : 시스템이 제공할 도메인 핵심 규칙을 구현한다.
  • 인프라스트럭쳐 : 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.

 

주문 도메인의 경우 '출고 전에 배송지를 변경할 수 있다'는 규칙과 '주문 취소는 배송 전에만 할 수 있다'는 규칙을 구현한 코드가 도메인 계층에 위치하게 된다.

예를 들어, 다음 코드를 보자
public class Order {
	private OrderState state;
	private ShippingInfo shippingInfo;

	public void changeShippingInfo(ShippingInfo newShippingInfo) {
		if (!state.isShippingChangeable()) {
			throw new IllegalStateException("can't change shipping in " + state);
		}
		this.shippingInfo = newShippingInfo;
	}
   -
}

public enum OrderState {
	PAYMENT_WAITING {
		public boolean isShippingChangeable() {
			return true;
		}
	},
	PREPARING {
		public boolean isShippingChangeable() {
			return true;
		}
	},
	SHIPPED, DELIVERING, DELIVERY_COMPLETED;

	public boolean isShippingChangeable() {
		return false;
	}
}

 

  • 위의 코드는 주문 도메인의 일부 기능을 도메인 모델 패턴으로 구현한 것이다. 큰 틀에서 보면 OrderState는 Order에 속한 데이터이므로 배송지 정보 변경 가능 여부를 판단하는 코드를 Order로 이동할 수도 있다.
  • 배송지 변경 가능 여부를 판단하는 기능이 Order에 있든, OrderState에 있든 중요한 점은 주문과 관련된 중요 업무 규칙을 주문 도메인 모델에서 구현한다는 점이다.
  • 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

 

2.4 도메인 모델 도출

도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이고 이것은 요구사항에서 출발한다.

 

주문 도메인과 관련 요구사항
  • 최소 한 종류 이상의 상품을 주문해야 한다.
  • 한 상품을 한 개 이상 주문할 수 있다.
  • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
  • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
  • 주문할 때 배송지 정보를 반드시 지정해야 한다.
  • 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
  • 출고를 하면 배송지를 변경할 수 없다.
  • 출고 전에 주문을 취소할 수 있다.
  • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.

 

위 요구사항을 토대로 Order에 대략적인 관련 기능을 메서드로 추가할 수 있다.
public class Order {
	public void changeShipped() { ... }
	public void changeShippingInfo(ShippingInfo newShipping) { ... }
	public void cancel() { ... }
	public void completePayment() { ... }
}
 
 
다음 요구사항은 주문 항목이 어떤 데이터로 구성되는지 알려준다.
  • 한 상품을 한 개 이상 주문할 수 있다.
  • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
두 요구사항에 따르면 주문 항목을 표현하는 OrderLine은 적어도 주문할 상품, 상품의 가격, 구매 개수를 포함하고 있어야 한다.
public class OrderLine {
	private Product product;
	private int price;
	private int quantity;
	private int amount;

	...
}
 
 
다음 요구사항은 Order와 OrderLine과의 관계를 알려준다.
 
  • 최소 한 종류 이상의 상품을 주문해야 한다.
  • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
public class Order {
	private List<OrderLine> orderLines;
	private int totalAmounts;

	private void setOrderLines(List<OrderLine> orderLines) { ... }
	private void verifyAtLeastOneOrMoeOrderLines(List<OrderLine> orderLines) { ... }
	private void calculateTotalAmounts() { ... }

	...
}

 

위와 같이 주문과 관련된 요구사항에서 도메인 모델을 점진적으로 만들어 나갈 수 있다. 이렇게 만든 모델은 요구사항 정련을 위해 도메인 전문가나 다른 개발자와 논의하는 과정에서 공유하기도 한다. 모델을 공유할 때는 화이트보드나 위키와 같은 도구를 사용해서 누구나 쉽게 접근할 수 있도록 하면 좋다.

 

 

 

3. 엔티티와 밸류

도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다. 엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있다.

 

엔티티와 밸류가 섞여있는 도메인 모델

 

3.1 엔티티

  • 엔티티의 가장 큰 특징은 식별자를 가진다는 것이다.
  • 엔티티의 식별자는 엔티티를 생성하고 속성을 바꾸고 삭제할 때까지 바뀌지 않는다.
  • 엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티의 식별자가 같으면 두 엔티티는 같다고 판단 할 수 있다.

 

엔티티의 식별자를 생성하는 네가지 방법
  • 특정 규칙에 따라 생성
  • UUID나 Nano ID와 같은 고유 식별자 생성기 사용
  • 값을 직접 입력
  • 일련번호 사용(시퀀스나 DB의 자동 증가 컬럼 사용)

 

 

3.2 밸류 타입

밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다. 예를 들어 받는 사람을 위한 밸류 타입인 Receiver를 다음과 같이 작성할 수 있다.

 

public class Receiver {
   private String name;
   private String phoneNumber;
   
   // ...

}

 

  • name과 phoneNumber 필드는 받는 사람과 관련된 데이터이므로 Receiver는 그 자체로 받는 사람을 뜻한다.
  • 밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기 보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다.
  • 불변성을 유지하는 이유 중 가장 큰 이유는 안전한 코드를 작성할 수 있다는 점에 있다.

 

 

3.3 도메인 모델에 set 메서드 넣지 않기

set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 하고, 도메인 객체를 생성할 때 온전하지 않은 상태로 생성할 수 있기 때문에 외부에서 set 메서드를 사용할수 없도록 설계하는 것이 좋다.

 

 

 

 

4. 도메인 용어와 유비쿼터스 언어

코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다. 도메인에서 사용하는 용어를 코드에 반영하지 않으면, 그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.

 

예를 들어, 주문 상태의 정보가 아래와 같다고 가정하자.

public OrderState {
	STEP1, STEP2, STEP3, STEP4, STEP5, STEP6
}

개발자는 해당 코드를 보고 아무것도 파악할 수 없다. 그러나 도메인 용어를 사용해서 아래와 같이 구현하면 의미가 훨씬 명확해 진다.

public OrderState {
	PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}

 

에반 에릭스는 도메인 주도 설계에서 언어의 중요함을 강조하기 위해 유비쿼터스 언어라는 용어를 사용했다. 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 같은 용어를 사용한다. 이렇게 하면 소통 과정에서 발생하는 용어의 모호함을 줄일 수 있고 개발자는 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다.

728x90
반응형

댓글