Posts [도메인주도개발시작하기] Chapter1-도메인 모델 시작
Post
Cancel

[도메인주도개발시작하기] Chapter1-도메인 모델 시작

도메인

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

image

[그림1.1] 도메인은 여러 하위 도메인으로 구성된다.

  • 카탈로그 하위 도메인은 고객에게 구매할 수 있는 상품 목록을 제공하고, 주문 하위 도메인은 고객의 주문을 처리한다.
    • 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
    • 예를 들어, 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.

image

  • 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
    • 모든 온라인 쇼핑몰이 고객 혜택을 제공하는 것은 아니며 규모가 크지 않은 소규모 업체는 엑셀과 같은 도구를 이용해 수작업으로 정산을 처리할 수도 있다.
  • 하위 도메인을 어떻게 구성하지 여부는 상황에 따라 달라진다.
    • 예를 들어 기업 고객을 대상으로 대형 장비를 판매하는 곳은 온라인으로 카탈로그를 제공하고 주문서를 받는 정도만 필요할 것이다.

도메인 모델

  • 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
  • 예를 들어, 주문 도메인 모델을 살펴보자.

image

[그림1.3] 객체 기반 주문 도메인 모델

  • [그림1.3]의 모델은 도메인의 모든 내용을 담고 있진 않지만 이 모델을 보면 주문(Order)은 주문번호(OrderNumber)와 지불할 총금액(totalAmounts)이 있고, 배송정보(Shipping)를 변경할 수 있음을 알 수 있다.
    • 즉, 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는데 도움이 된다.
  • 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 하는데, 이런 면에서 기능과 데이터를 함께 보여주는 객체 모델은 도메인을 모델링하기 적합하다.
  • 도메인 모델을 객체로만 모델링할 수 있는 것은 아니고 상태 다이어그램 등 다양한 표현 방법을 사용할 수 있다.
    • 즉, 관계가 중요한 도메인이라면 그래프를 이용해서 도메인을 모델링할 수 도 있는 것처럼 적절하게 사용하는게 좋다.
  • 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다.
    • 개념 모델과 구현 모델은 서로 다르지만 구현 모델이 개념 모델을 최대한 따르도록 할 수는 있다.

하위 모데인과 모델: 모델의 각 구성 요소는 특정 도메인을 한정할 때 비로소 의미가 완전해지기 떄문에, 각 하위 도메인마다 별도로 모델을 만들어야 한다. 이는 카탈로그 하위 도메인 모델과 배송하위 도메인 모델을 따로 만들어야 한다는 것을 뜻한다.

도메인 모델 패턴

image

[그림1.5] 아키텍처 구성

  • 일반적인 애플리케이션 아키텍처는 위 이미지와 같이 네 개의 층으로 구성된다.
  • 각 계층의 역할은 다음과 같다.
    • 표현(UI) 계층: 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람 뿐 아니라 외부 시스템도 사용자가 될 수도 있다.
    • 응용(Application) 계층: 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
    • 도메인 계층: 시스템이 제공할 도메인의 규칙을 구현한다.
    • 인프라스트럭처 계층: DB나 메시징 시스템과 같은 외뷔 시스템과의 연동을 처리한다.
  • 도메인 계층은 도메인의 핵심 규칙을 구현한다.
    • 주문 도메인의 경우 ‘출고 전에 배송지를 변경할 수 있다’는 규칙과 ‘주문 취소는 배송 전에만 할 수 있다’는 규칙을 구현한 코드가 도메인 계층에 위치하게 된다.
    • 이런 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 void changeShipped() {
		// 로직 검사
		this.state = OrderState.SHIPPED;
	}
	...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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는 배송지를 변경할 수 있는지 여부를 검사할 수 있는 isShippingChangeable() 메서드를 제공하고 있다.
    • 코드를 보면 주문 대기 중(PAYMENT_WAITING) 상태와 상품 준비 중(PREPARING) 상태만 배송지를 변경할 수 있딴 것을 알 수 있다. 즉, OrderState는 주문 대기 중 이거나 상품 준비 중에는 배송지를 변경할 수 있따는 도메인 규칙을 구현하고 있다.
  • 위 코드에선 배송지 변경 가능 여부를 판단할 규칙이 OrderState 쪽에 구현되어 있지만 Order 클래스에서 이를 구현할 수 도 있다.
    • 하지만어디에 있뜬 중요한 점은 주문과 관련된 중요 업무 규칙을 주문 도메인 모델인 Order나 OrderState에서 구현하다는 점이다.
  • 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

Note: ‘도메인 모델’이란 용어는 도메인 자체를 표현하는 개념적인 모델을 의미하지만, 도메인 계층을 구현할 때 사용하는 객체 모델을 언급할 때에도 ‘도메인 모델’이란 용어를 사용한다.

개념 모델과 구현 모델: 처음부터 완벽한 개념 모델을 만들기보단 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다. 프로젝트 초기에는 개요 수준의 개념 모델로 도메인에 대한 전체 윤곽을 이해하는데 집중하고, 구현하는 과정에서 개념 모델을 구현 모델로 점진적으로 발전시켜 나가야 한다.

도메인 모델 도출

  • 기획서, 유스 케이스, 사용자 스토리와 같은 요구사항과 관련자와의 대화를 통해 도메인을 이해하고 이를 바탕으로 도메인 모델 초안을 만들어야 비로소 코드를 작성할 수 있다.
  • 도메인을 모델링할 떄 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.
  • 주문 도메인과 관련된 몇가지 요구사항을 보자
    • 최소 한 종류 이상의 상품을 주문해야 한다.
    • 한 상품을 한 개 이상 주문할 수 있다.
    • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
    • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
    • 주문할 때 배송지 정보를 반드시 지정해야 한다.
    • 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
    • 출고를 하면 배송지 정보를 변경할 수 없다.
    • 출고 전에 주문을 취소할 수 있다.
    • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
  • 위 요구사항을 통해 아직 상세 구현까지 할 수 있는 수준은 아니지만 Order에 관련 기능을 메서드로 추가할 수 있다.
1
2
3
4
5
6
public class Order {
	public void changeShipped() { ... }
	public void changeShippingInfo(ShippingInfo newShipping) { ... }
	public void cancel() { ... }
	public void completePayment() { ... }
}
  • 다음 요구사항은 주문 항목이 어떤 데이터로 구성되는지 알려준다.
    • 한 상품을 한 개 이상 주문할 수 있다.
    • 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
  • 두 요구사항에 따르면 주문 항목을 표현하는 OrderLine은 적어도 주문할 상품, 상품의 가격, 구매 개수를 포함하고 있어야 한다. 이를 OrderLine으로 구현하면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OrderLine {
	private Product product;
	private int price;
	private int quantity;
	private int amount;

	public OrderLine(Product product, int price, int quantity) {
		...
	}

	private int calculateAmounts() {
		return price * quantity;
	}

	public int getAmounts() { ... }
	...
}
  • 다음 요구사항은 Order와 OrderLine과의 관계를 알려준다.
    • 최소 한 종류 이상의 상품을 주문해야 한다.
    • 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
  • 한 종류 이상의 상품을 주문할 수 있으므로 Order는 최소 한 개이상의 OrderLine을 포함해야 한다.
  • 또한, OrderLine으로부터 총 주문 금액을 구할 수 있다. 이를 코드에 반영하면 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Order {
	private List<OrderLine> orderLines;
	private int totalAmounts;

	public Order(list<OrderLine> orderLines) {
		setOrderLines(orderLines);
	}

	private void setOrderLines(List<OrderLine> orderLines) { 
			verityAtLeastOneOrMoreOrderLines(orderLines);
			this.orderLines = orderLines;
			calculateTotalAmounts();
	}

	private void verifyAtLeastOneOrMoeOrderLines(List<OrderLine> orderLines) { 
		if (orderLines == null || orderLines.isEmpty()) {
			throw new IllegalArgumentException("no OrderLine");
		}
	 }

	private void calculateTotalAmounts() { 
		this.totalAmounts == new Money(orderLines.stream()
			.mapToInt(x -> x.getAmounts().getValue()).sum());
	}

	...
}
  • 요구사항에 따르면 최소 한 종류 이상의 상품을 주문해야 하므로 setOrderLines() 메서드는 verityAtLeastOneOrMoteOrderLines() 메서드를 이용하여 OrderLine이 한 개이상 존재하는지 검사한다.
    • 또한, calcuateTotalAmounts() 메서드를 이용해 총 주문 금액을 계산한다.
  • 배송지 정보는 이름, 전화번호, 주소 데이터를 가지므로 ShippingInfo 클래스를 다음과 같이 정의하였다.
1
2
3
4
5
6
7
8
9
public class ShipingInfo {
	private String receiverName;
	private String receiverPhoneNumber;
	private String shipingAddress1;
	private String shipingAddress2;
	private String shipingZipcode;

	... 생성자, getter
}
  • 앞서 요구사항 중에 ‘주문할 때 배송지 정보를 반드시 지정해야 한다’는 내용이 있는데 이는 Order 생성시 생성자에 OrderLine의 목록 뿐만 아니라 ShippingInfo도 함께 전달해야 함을 의미한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Order {
    private List<OrderLine> orderLines;
    private ShippingInfo shippingInfo;
    ...

    public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo) {
        setOrderLines(orderLines);
        setShippingInfo(shippingInfo);
    }

    private void setShippingInfo(ShippingInfo shippingInfo) {
    	if(shippingInfo == null) {
    	    throw new IllegalArgumentException("no ShippingInfo");
    	}
    	this.shippingInfo = shippingInfo;
    }
    ...
}
  • 생성자에서 호출하는 setShippingInfo() 메서드는 ShippingInfo가 null이면 Exception을 발생하는데, 이렇게 함으로써 배송지 정보 필수라는 도메인 규칙을 구현한다.
  • 도메인을 구현하다 보면 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우가 많다.
    • 출고를 하면 배송지 정보를 변경할 수 없다.
    • 출고 전에 주문을 취소할 수 있다.
    • 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
  • 이는 요구사항이 추가로 존재할 수 있는 상태를 분석한 뒤, 다음과 같이 열거 타입을 이용해서 상태 정보를 표현할 수 있다.
1
2
3
4
5
6
7
8
public enum OrderState {
	PAYMENT_WAITING, 
	PREPARING, 
	SHIPPED, 
	DELIVERING, 
	DELIVERY_COMPLETED, 
	CANCELED 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Order{
	private OrderState state;
	
	public void changeShippingInfo(ShippingInfo newShippingInfo) {
		verifyNotYetShipped();
		setShippingInfo(newShippingInfo);
	}
	public void cancel(){
		verifyNotYetShipped();
		this.state = OrderState.CANCELED;
	}
	private void verifyNotYetShipped(){
		if(state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
			throw new IllegalStateException("already shippped");
	}
	...
}

문서화: 문서화를 하는 주된 이유는 지식을 공유하기 위함이다. 코드를 이용해 전체 소프트웨어를 분석하려면 많은 시간이 소요되지만 전반적인 기능 목록이나 모듈 구조, 빌드 과정은 코드를 보고 직접 이해하는 것보다 상위 수준에서 정리한 문서를 참조하는 것이 소프트웨어 전반을 빨르게 이해하는데 도움이 된다. 그리고 코드가 도메인을 잘 표현해야 비로소 코드의 가독성이 높아지며 문서로서 코드가 의미를 갖는다.

엔티티와 벨류

엔티티

  • 엔티티는 식별자를 갖는다.
    • ex) 주문 도메인 -> 식별자: ‘주문 번호’
    • Order는 엔티티로서 orderNumber를 식별자로 갖는다.
  • 엔티티의 식별자는 바뀌지 않고 고유하기 떄문에 두 엔티티 객체의 식별자가 가틍면 두 엔티티를 같다고 판단할 수 있따.
  • 엔티티를 구현한 클래스는 다음과 같이 식별자를 이용해서 equals() 메서드와 hashcode() 메서드를 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Order {
	public String orderNumber;
	
	@Override
	public boolean equals(Object obj){
		if (this == obj) return true;
		if (obj == null) return false;
		if (obj.getClass() != Order.class) return false;
		Order other = (Order)obj;
		if (this.orderNumber == null) return false;
		return this.orderNumber.equals(other.orderNumber);
	}

	@Override
	public int hashCode(){
		final int prime = 31;
		int result = 1;
		result = prime * result + ((orderNumber == null) ? 0 : orderNumber.hashCode());
		return result;
	}
}

엔티티의 식별자 생성 방식

  • 1)특정 규칙에 따라 생성한다 - 주문번호,운송장번호 등 특정 규칙에 따라 생성
  • 2)UUID 사용
    • UUID uuid = UUID.randomUUID();
    • // 615fsdf34-c342-5scd-d33d-123145sadfa 와 같은 문자열
  • 3)값을 직접 입력 - 회원 아이디, 이메일
  • 4)일렬번호 사용 (시퀀스나 DB의 자동 증가 칼럼(auto_increment) 사용)

벨류 타입

1
2
3
4
5
6
7
8
9
10
public class ShippingInfo {
	private String receiverName;  // 받는 사람
	private String receiverPhoneNumber; // 받는 사람

	private String shippingAddress1; // 주소
	private String shippingAddress2; // 주소
	private String shippingAddressZipcode; // 주소

	...
}
  • ShippingInfo 클래스의 receiverName 필드와 receiverPhoneNumber 필드는 서로 다른 두 데이터를 담고 있지만 두 필드는 개념적으로 받는 사람을 의미한다.
    • 즉, 두 필드는 실제로 한 개의 개념을 표현하고 있다.
  • 비슷하게 shippingAddress1, shippingAddress2, shippingAddressZipcode 는 주소라는 하나의 개념을 표현한다.
  • 벨류 타입은 개념적으로 완전한 하나를 표현할 떄 사용한다.
  • 예를 들어, 받는 사람을 위한 벨류 타입인 Receiver와 주소 관련 데이터를 표현하는 Address 를 다음과 같은 벨류 타입으로 작성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Receiver {
	private String name;
	private String phoneNumber;
	
	public Receiver(String name, String phoneNumber){
		this.name = name;
		this.phoneNumber = phoneNumber;
	}

	public String getName(){
		return name;
	}
	public String getPhoneNumber(){
		return phoneNumber;
	}
}
1
2
3
4
5
6
7
public class Address{
	private String address1;
	private String address2;
	private String zipcode;
	
	...
}
  • 벨류 타입을 이용한 ShippingInfo 클래스는 다음과 같이 변경될 것이다.
1
2
3
4
5
6
public class ShippingInfo{
	private Receiver receiver;
	private Address address;
	
	...
}
  • 벨류 타입이 꼭 두 개 이상의 데이터를 가져야 하는 것은 아니다.
  • 의미를 명확히 표현하기 위해 사용하는 경우도 있다.
  • 이를 위한 좋은 예가 OrderLine이다.
1
2
3
4
5
6
7
public class OrderLine {
	private Product product;
	private int price;
	private int quantity;
	private int amounts;
...
}
  • price와 amounts는 int 타입의 숫자를 사용하고 있지만, 의미하는 값은 ‘돈’ 이다.
  • 따라서 ‘돈’을 의미하는 Money 타입을 만들어 사용하면 코드를 이해하는데 도움 된다.
1
2
3
4
5
6
7
8
9
10
11
public class Money{
	private int value;

	public Money(int value){
		this.money = money;
	}

	public int getValue() {
		return this.value;
	}
}
1
2
3
4
5
6
7
public class OrderLine{
	private Product product;
	private Money price; // Money 타입 덕에 price가 금액을 의미한다는 것을 쉽게 유추할 수 있음
	private int quantity;
	private Money amounts; // Money 타입 덕에 price가 금액을 의미한다는 것을 쉽게 유추할 수 있음
...
}
  • 벨류 타입을 사용할 때의 또 다른 장점은 벨류 타입을 위한 기능을 추가할 수 있다는 것이다.
    • 예를 들어, Money 타입은 다음과 같이 돈 계산을 위한 기능을 추가할 수 있다.
    • 또한 이를 통해 벨류 타입은 코드의 의미를 더 잘 이해할 수 있도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Money{
	private int value;

	public Money(int value){
		this.money = money;
	}

	public int getValue() {
		return this.value;
	}
	
	//새로운 기능 추가 가능
	public Money add(Money money){
		return new Money(this.value + money.value);
	}
	public Money multiply(int multiplier) {
		return new Money(this.value * multiplier);
	}
}

참조 투명성과 관련된 문제

  • 벨류 객체의 데이터 변경시 기존 데이터를 변경하기보단 변경한 데이터를 갖는 새로운 객체를 생성하는 방식이 좋다.
    • 예를 들어, 앞서 Money 클래스의 add 메서드를 보면 Money를 새로 생성하고 있다.
  • Money처럼 데이터 변경 기능을 제공하지 않는 타입을 불변(immutable)이라 표현한다.
  • 벨류 타입을 불변으로 구현하는 이유는 여러가지가 있는데 가장 중요한 이유는 보다 안전한 코드를 작성할 수 있다는 것이다.
  • 만약 Money 가 setValue와 같은 메서드를 제공하여 값을 변경할 수 있다면 아래 이미지처럼 OrderLine의 price값이 잘못 반영되는 상황이 발생하게 된다.
1
2
3
Money price = new Money(1000);
OrderLine line = new OrderLine(product, price, 2) // -> [price=1000, quantity=2, amounts=2000]
price.setValue(2000); // -> [price=2000, quantity=2, amounts=2000]
  • 이런 문제를 방지하려면 OrderLine 생성자는 다음과 같이 새로운 Money 객체를 생성하도록 코드를 작성해야 한다.
    • 하지만, Money가 불변이라면 이런 코드를 작성할 필요가 없다.
    • 그렇지만 개인적인 생각은 추후 어떠한 변경사항이 생길지 모르기 떄문에 안전하게 아래와 같이 처리하는게 좋은것 같다,.
1
2
3
4
5
6
7
8
9
10
11
12
public class OrderLine {
	...
	private Money price;

	public OrderLine(Product product, Money price, int quantity) {
		this.product = product;
		// Money 가 불변 객체가 아니라면,
		// price 파라미터가 변경될 떄 발생하는 문제를 방지하기 위해
		// 데이터를 복사한 새로운 객체를 생성해야 한다.
		this.price = new Money(price.getValue());
	}
}
  • 두 벨류 객체가 같은지 비교할 땐 모든 속성이 같은지 비교해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Receiver{
	private String name;
	private String phoneNumber;
	
	public boolean equals(Object other){
		if (other == null) return false;
		if (this == other) return true;
		if (! (other instanceof Receiver)) return false;
		Receiver that = (Receiver) other;
		
		return this.name.equals(that.name) && this.phoneNumber.equals(that.phoneNumber);
	}
}

엔티티 식별자와 벨류 타입

  • 식별자가 어떤 도메인의 식별자인지를 분명히 나타내기 위해서 밸류 타입을 사용할 수 있다.
    • 밸류 타입을 통해 식별자의 의미를 분명히 드러낼 수 있다.
1
2
3
4
5
public class Order{
	//private String id;
	private OrderNo id;
	...
}

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

  • 도메인 모델에 get/set 메서드를 무조건 추가하는것은 좋지 않은 버릇이다.
  • set메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
    • 앞서 changeShippingInfo()가 배송지 정보를 새로 변경하다는 의미를 가졌다면 setShippingInfo() 메서드는 단순히 배송지 값을 설정한다는 것을 뜻한다.
    • completePayment()는 결제 완료와 관련된 처리 코드를 함께 구현하기 때문에 결제 완료와 관련된 도메인 지식을 코드로 구현하는 것이 자연스럽다.
    • 하지만 setOrderState는 단순히 상태값만 변경할지 아니면 상태 값에 따라 다른 처리를 위한 코드를 함꼐 구현할지 애매하다.
  • set 메서드의 또 다른 문제점은 도메인 객체를 생성시 완전한 상태가 아닐 수도 있다는 것이다.
1
2
3
4
5
6
7
8
9
10
// set 메서드로 데이터를 전달하도록 구현하면
// 처음 Order를 생성하는 시점에 order는 완전하지 않다.
Order order = new  Order();

// set 메서드로 필요한 모든 값을 전달해야 한다. 
order.setOrderLine(lines);
order.setShippingInfo(shipingInfo);

// 주문자(Orderer)를 설정하지 않은 상태에서 주문 완료 처리 -> 문제 발생!!!
order.setState(OrderState.PREPARING);
  • 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다.
1
Order order = new Order(orderer, lines, shippingInfo, OrderState.PREPARING);
  • 생성자로 필요한 것을 모두 받으므로 다음처럼 생성자를 호출하는 시점에 필요한 데이터가 올바른지 검사할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Order {
    public Order(Orderer orderer, List<OrderLine> orderLines, ShippingInfo shippingInfo, OrderState orderState) {
		setOrderer(orderer);
		setOrderLines(orderLines);
		... // 다른 값 설정
	}

	private void setOrderer(Orderer orderer) {
		if (orderer == null) {
			throw new IllegalArgumentException("no orderer");
		}
		this.orderer = this.orderer;
	}

	private void setOrderLines(List<OrderLine> orderLines) {
		vverifyAtLeastOneOrMoeOrderLines(orderLines);
		this.orderLines = orderLines;
		calculateTotalAmounts();
	}

	private void verifyAtLeastOneOrMoeOrderLines(List<OrderLine> orderLines) { 
		if (orderLines == null || orderLines.isEmpty()) {
			throw new IllegalArgumentException("no OrderLine");
		}
	 }

	private void calculateTotalAmounts() { 
		this.totalAmounts == new Money(orderLines.stream()
			.mapToInt(x -> x.getAmounts().getValue()).sum());
	}
}
  • 이 코드의 set 메서드는 접근 범위가 private 이다.
    • 즉, 클래스 내부에서ㅔ 데이터를 변경할 목적으로 사용되며 외부에선 데이터를 호출할 수 없다.
  • 불변 밸류 타입을 사용하면 자연스럽게 밸류 타입엔 set 메서드를 구현하지 않도록 하여 불변 타입의 장점을 살리도록 한다.

DTO의 get/set 메서드: DTO가 도메인 로직을 담고 있지는 않기에 get/set 메서드를 제공해도 도메인 객체의 데이터 일관성에 영향을 줄 가능성이 높지 않다.

도메인 용어

  • 코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다. 도메인에서 사용하는 용어를 코드에 반영하지 않으면 그 코드는 개발자에게 코드의 의미를 해석해야 하는 부담을 준다.
1
2
3
public OrderState {
	STEP1, STEP2, STEP3, STEP4, STEP5, STEP6
}
1
2
3
4
5
6
7
public class Order {
	public void changeShippingInfo(ShippingInfo shippingInfo) {
		verifySetp1OrStep2();
		...
	}
	public void verityStep1OrStep2() { ... }
}
  • 기획자나 온라인 쇼핑 도메인 전문가가 개발자와의 업무 회의에서 ‘출고 전’이라는 단어를 사용하면 개발자는 머릿속으로 ‘출고 전은 STEP1과 STEP2’라고 도메인 지식을 코드로 해석해야 한다.
  • 도메인 용어는 좋은 코드를 만드는 데 매우 중요하지만 국내 개발자에게 불리한 면이 있다. 바로 영어 때문이다.
    • 분야의 특성상 알파벡과 숫자를 사용해서 클래스, 필드, 메서드 등의 이름을 작성하게 되는데 이는 도메인 용어를 영어로 해석하는 노력이 필요함을 뜻한다.
  • 알맞은 영어 단어를 찾는 것은 쉽지 않은 일이지만 시간을 들여 찾는 노력을 해야 한다. 적절한 단어를 사용하려는 노력하지 않는다면 코드는 도메인과 점점 멀어지게 된다.
This post is licensed under CC BY 4.0 by the author.

[클린코드] Chapter5-형식 맞추기

[도메인주도개발시작하기] Chapter2-아키텍처 개요