도메인 주도 개발 시작하기 - 1. 도메인 모델 시작하기
1.1 도메인이란?
1.2 도메인 전문가와 개발자 간 지식 공유
1.3 도메인 모델
1.4 도메인 모델 패턴
1.5 도메인 모델 도출
1.6 엔티티와 밸류
1.7 도메인 용어와 유비쿼터스 언어
1.1 도메인이란?
-
도메인이란, 소프트웨어로 해결하고자 하는 문제 영역이다.
ex) 온라인 서점은 상품조회, 구매, 결제, 배송 추적 등의 기능을 제공해야 한다.
- 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
- 온라인 서점은 주문, 카탈로그, 리뷰, 회원, 결제, 혜택, 정산 등 여러 하위 도메인으로 구성된다.
- 각 하위 도메인이 다루는 영역은 서로 다르기 때문에 같은 용어라도 하위 도메인마다 의미가 달라질 수 있다.
- 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
- 고객이 물건을 구매하면 주문, 결제, 배송, 혜택 하위 도메인의 기능이 엮이게 된다.
- 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 구현하는 것은 아니다.
- 쇼핑몰에서 자체적으로 배송 시스템을 구축하기보다 외부 배송 업체의 시스템을 사용하고, 배송 추적에 필요한 기능만 일부 연동한다.
- 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
- 쇼핑몰에서 혜택을 제공하지 않을 수 있고, 엑셀 같은 도구로 수작업으로 정산할 수도 있다.
- 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.
- 어떤 고객(B2B, B2C)을 대상으로 할지에 따라 하위 도메인 구성이 달라진다.
1.2 도메인 전문가와 개발자 간 지식 공유
- 각 영역의 전문가들은 도메인에 대한 지식과 경험을 바탕으로 기능 개발을 요구한다.
- 개발자는 이런 요구사항을 분석하고 설계하는데, 이때 요구사항을 올바르게 이해하는 것이 중요하다.
- 요구사항을 이해하기에 비교적 간단한 방법은 개발자와 전문가와 직접 대화하는 것이다.
1.3 도메인 모델
-
도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
- 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 한다.
- 객체 모델은 도메인을 모델링하기에 적합하다.
- 클래스 다이어그램, 상태 다이어그램, 그래프, 수학 공식 등 도메인을 이해하는데 도움이 되는 표현 방식은 무엇이든 사용할 수 있다.
- 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이고, 개념 모델이 있더라도 구현 기술에 맞는 구현 모델이 따로 필요하다.
- 개념 모델과 구현 모델은 서로 다른 것이지만 구현 모델이 개념 모델을 최대한 따르도록 할 수는 있다.
- 개념 모델은 순수하게 문제를 분석한 결과물이므음로음 처음부터 완벽한 개념 모델을 만들기보다는 전반적인 개요를 알 수 있는 수준으로 개념 모델을 작성해야 한다.
- 구현하는 과정에서 도메인을 더 잘 이해하게 되고, 새로운 지식이 쌓이면서 모델을을 보완하거나 변경하며 점진적으로 발전시켜 나가야 한다.
1.4 도메인 모델 패턴
- 일반적인 애플리케이션의 아키텍처에서 의미하는 도메인 계층에서의 도메인 모델은 마틴 파울러가 쓴 ⟪엔터프라이즈 애플리케이션 아키텍처 패턴⟫의 도메인 모델 패턴을 의미한다.
- 도메인 계층은 도메인의 핵심 규칙을 구현한다.
- 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다.
1.5 도메인 모델 도출
-
도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.
- 최소 한 종류 이상의 상품을 주문해야 한다. ➡️ List<OrderLine> - 한 상품을 한 개 이상 주문할 수 있다. ➡️ OrderLine.quantity - 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다. ➡️ Order.calculateTotalAmounts() - 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다. ➡️ OrderLine.calculateAmounts() - 주문할 때 배송지 정보를 반드시 지정해야 한다. ➡️ ShippingInfo - 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다. ➡️ ShippingInfo - 출고를 하면 배송지를 변경할 수 없다. ➡️ OrderState - 출고 전에 주문을 취소할 수 있다. ➡️ OrderState - 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다. ➡️ OrderState
-
도메인을 구현하다 보면 특정 조건이나 상태에 따라 제약이나 규칙이 달리 적용되는 경우가 많다.
- 출고를 하면 배송지 정보를 변경할 수 없다. ➡️ PAYMENT_WAITING, PREPARING - 출고 전에 주문을 취소할 수 있다. ➡️ PAYMENT_WAITING, PREPARING - 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다. ➡️ PAYMENT_WAITING
- 이렇게 도출된 도메인 모델은 도메인 전문가나 다른 개발자 등 누구나 쉽게 접근하도록 문서화 하면 좋다.
- 코드를 보면서 도메인을 깊게 이해하게 되므로 코드 자체도 문서화의 대상이 된다.
- 도메인 지식이 잘 묻어나도록 코드를 작성하지 않으면 코드의 동작 과정은 해석할 수 있어도 도메인 관점에서 왜 코드를 그렇게 작성했는지 이해하는데는 도움이 되지 않는다.
1.6 엔티티와 밸류
- 도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다.
- 엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있다.
1.6.1) 엔티티
- 엔티티의 가장 큰 특징은 식별자를 가진다는 것이다.
-
엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티 객체의 식별자가 같으면 두 엔티티는 같다고 판단할 수 있다.
@Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Order)) return false; Order order = (Order) obj; return Objects.equals(number, order.number); } @Override public int hashCode() { int result = 1; result = 31 * result + ((number == null) ? 0 : number.hashCode()); return result; }
1.6.2) 엔티티의 식별자 생성
- 엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.
- 흔히 식별자는 다음 중 한 가지 방식으로 생성한다.
- 특정 규칙에 따라 생성
- 주문번호, 운송장번호, 카드번호
- 흔히 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것인데, 같은 시간에 동시에 식별자를 생성하더라도 같은 식별자가 만들어지지 않도록 주의해야 한다.
- UUID, Nano ID와 같은 고유 식별자 생성기 사용
UUID.randomUUID()
- 값을 직접 입력
- 아이디, 이메일
- 일련번호 사용 (시퀀스나 DB의 자동 증가 컬럼)
- 주로 데이터베이스가 제공하는 자동 증가 기능을 사용한다.
- 특정 규칙에 따라 생성
1.6.3) 밸류 타입
-
밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.
public class Address { private String zipCode; private String address1; private String address2; }
-
밸류 타입이 꼭 두 개 이상의 데이터를 가져야 하는 것은 아니다.
public class Money { private int value; }
-
밸류 타입을 사용하면 밸류 타입을 위한 기능을 추가할 수 있다.
private Money calculateAmounts() { return price.multiply(quantity); }
-
밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다.
public Money multiply(int multiplier) { return new Money(value * multiplier); }
- 밸류 타입을 불변으로 구현하는 가장 중요한 이유는 안전한 코드를 작성할 수 있다는데 있다.
-
밸류 객체를 비교할 때는 모든 속성이 같은지 비교한다.
@Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Receiver)) return false; Receiver receiver = (Receiver) obj; return Objects.equals(name, receiver.name) && Objects.equals(phone, receiver.phone); }
1.6.4) 엔티티 식별자와 밸류 타입
- 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많은데, 식별자에도 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다.
- 주문번호를 표현하기 위해
String id
대신OrderNo id
를 사용하면 실제 의미를 파악하기 쉽다.
- 주문번호를 표현하기 위해
1.6.5) 도메인 모델에 set 메서드 넣지 않기
- 도메인 모델에
get
,set
메서드를 무조건 추가하는 것은 좋지 않은 버릇이다. - 특히
set
메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다. set
메서드의 또 다른 문제는 도메인 객체를 생성할 때 온전하지 않은 상태가 될 수 있다.-
도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다
즉, 생성자를 통해 필요한 데이터를 모두 받아야 한다.
-
-
set
메서드를 사용한다면, 접근 범위를private
으로 설정하여 내부에서 데이터를 변경할 목적으로 사용하고, 외부에서 데이터를 변경하지 못하도록 해야 한다.private void setOrderer(Orderer orderer) { if (orderer == null) throw new IllegalArgumentException("no orderer"); this.orderer == orderer; }
- DTO 에서도
set
메서드가 아닌private
필드에 직접 값을 할당할 수 있게 발전되어 불변의 장점을 DTO까지 확장할 수 있다.
1.7 도메인 용어와 유비쿼터스 언어
- 코드를 작성할 때 도메인에서 사용하는 용어는 매우 중요하다.
- 도메인 용어를 사용해서 코드를 작성하면, 의미 변환 과정에서 발생하는 시간과 버그가 줄어든다.
- 에릭 에반스는 유비쿼터스 언어라는 용어를 사용했는데, 모든 관계자가 같은 용어를 사용하면 소통 과정에서 용어의 모호함으로 발생하는 불필요한 해석 과정을 줄일 수 있다.
- 알맞은 영단어를 찾는 것은 쉽지 않은 일이지만 시간을 들여 찾는 노력을 해야 한다.
- 상태: state/status, 종류: kind/type/gubun(구분)