IT recording...
[DDD START!] 1장 - 도메인 본문
1. 도메인 모델
: 특정 도메인을 개념적으로 표현한 것
ex) 주문 도메인
주문 도메인 내에서 사용하는 기능들을 대략적으로 확인할 수 있다.
→ 이후 구현 모델로의 변환 과정이 필요하다.
2. 도메인 모델 패턴
- 표현계층 : 사용자의 요청을 처리하고 사용자에게 정보를 보여준다.
- 응용계층 : 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
- 도메인 : 시스템이 제공할 도메인의 규칙을 구현한다.
- 인프라스트럭처 : 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.
- 도메인 계층
- ex) 주문 도메인 - ‘출고 전에 배송지를 변경할 수 있다.’ , ‘주문 취소는 배송 전에만 할 수 있다.’
-
public class Order{ private OrderState state; private ShippingInfo shippingInfo; //출고 전에 배송지를 변경할 수 있다. public void changeShippingInfo(ShippingInfo newShippingInfo){ if (!isShippingInfoChangable()){ throw new IllegalStateException("can't change shipping in" + state); } this.shippingInfo = newShippingInfo; } //현재 배송 상태 체크 private boolean isShippingInfoChangeable(){ return state == OrderState.PAYMENT_WAITING || state == OrderState.WAITING; } ... } public enum OrderState { PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED; }
- : 도메인의 핵심 규칙을 구현한다.
3. 도메인 모델 도출
: 요구사항을 분석하여 도메인 모델을 도출한다.
요구사항
a) 최소 한 종류 이상의 상품을 주문해야 한다.
b) 한 상품을 한 개 이상 주문할 수 있다.
c) 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
d) 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
e) 주문할 때 배송지 정보를 반드시 지정해야 한다.
f) 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
g) 출고를 하면 배송지 정보를 변경할 수 없다.
h) 출고 전에 주문을 취소할 수 있다.
i) 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
1단계) Order에서 제공할 기능 생각하기
public class Order {
public void chageShipped() {...} //출고 상태로 변경하기
public void changeShippingInfo(ShippingInfo newShipping) {...} //배송지 정보 변경
public void cancel() {...} //주문 취소
public void completePayment() {...} //결제 완료로 변경하기
}
2단계) OrderLine 구성하기
- 요구사항 b,d → 주문할 상품, 상품의 가격, 구매 개수를 포함해야 한다.
- 요구사항 c → 총 주문 금액을 포함해야 한다.
public class OrderLine {
private Product product;
private int price;
private int quantity;
private int amounts;
public OrderLine(Product product, int price, int quantity){
this.product = product;
this.price = price;
this.quantity = quantity;
this.amounts = calculateAmounts();
}
private int calculateAmounts(){
return price * quantity;
}
public int getAmounts() {...}
...
}
3단계) Order - OrderLine 관계 살피기
- 요구사항 a → Order는 최소 한 개 이상의 OrderLine을 포함해야 한다.
- 요구사항 c → Order는 OrderLine으로부터 총 주문 금액을 구할 수 있다.
public class Order{
private List<OrderLine> orderLines;
private int totalAmounts;
public Order(List<OrderLine> orderLines){
setOrderLines(orderLines);
}
private void setOrderLiens(List<OrderLine> orderLines) {
verifyAtLeastOneOrMoreOrderLines(orderLines); //최소 한 개 이상의 주문 포함
this.orderLines = orderLines;
calculateTotalAmounts(); //총 주문 금액 구하기
}
private void verifyAtLeastOneOrMoreOrderLines(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();
}
...
}
4단계) ShippingInfo 구성하기
- 요구사항 f → 배송지 정보는 이름, 전화번호, 주소 데이터를 가진다.
public class ShippingInfo {
private String receiverName;
private String receiverPhoneNumber;
private String shippingAddress1;
private String shippingAddress2;
private String shippingZipcode;
... //생성자,getter
}
- 요구사항 e → 주문할 때 배송지 정보를 반드시 지정해야 한다.
public class Order {
private List<OrderLine> orderLines;
private int totalAmounts;
private ShippingInfo shippingInfo;
public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo){
setOrderLiens(orderLines);
setShippingInfo(shippingInfo);
}
private void setShippingInfo(ShippingInfo shippingInfo){
if (shippingInfo == null)
throw new IllegalArgumentException("no shippingInfo");
this.shippingInfo = shippingInfo;
}
...
}
5단계) 특정 조건이나 상태에 따른 제약 표현
- 요구사항 g,h → 출고상태에 따라 제약이 다르게 적용되므로 출고 상태를 표현할 수 있어야 한다.
- 요구사항 i → 결제상태에 따라 제약이 다르게 적용된다.
public enum OrderState {
PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED, CANCELED;
}
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");
}
...
}
4. 엔티티와 밸류
- 엔티티 : 고유한 식별자를 갖는 값
- ex) 주문 도메인 → 식별자 : ‘주문번호’
- → 엔티티 속성을 바꾸고 엔티티를 삭제할 때까지 식별자는 유지된다.
public class Order { **public String orderNumber;** public List<OrderLine> orderLines; public int totalAmounts; public ShippingInfo shippingInfo; public OrderState state; @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; } }
엔티티의 식별자 생성
- 특정 규칙에 따라 생성 - 주문번호,운송장번호 등 특정 규칙에 따라 생성
- UUID 사용
UUID uuid = UUID.randomUUID(); // 615fsdf34-c342-5scd-d33d-123145sadfa 와 같은 문자열
- 값을 직접 입력 - 회원 아이디, 이메일
- 일렬번호 사용 (시퀀스나 DB의 자동 증가 칼럼 사용)
5. 밸류 타입
: 개념적으로 완전한 하나를 표현한다.
- 두 개 이상의 값을 하나로
public class ShippingInfo {
//개념적으로 받는 사람 덩어리
private String receiverName;
private String receiverPhoneNumber;
//개념적으로 주소 덩어리
private String shippingAddress1;
private String shippingAddress2;
private String shippingZipXode;
}
⇒
public class ShippingInfo{
private Receiver receiver;
private Address address;
//생성자, getter
}
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;
}
}
public class Address{
private String address1;
private String address2;
private String zipcode;
//생성자, getter
}
- 의미를 명확하게 하기 위해 밸류 타입을 사용하기도 한다.
public class OrderLine {
private Product product;
private int price;
private int quantity;
private int amounts;
...
}
⇒ price와 amounts는 int 타입의 숫자를 사용하고 있지만, 의미하는 값은 ‘돈’ 이다.
public class OrderLine{
private Product product;
private Money price;
private int quantity;
private Money amounts;
...
}
public class Money{
private int value;
public Money(int value){
this.money = money;
}
//... getter
//새로운 기능 추가 가능
public Money add(Money money){
return new Money(this.value + money.value);
}
public Money multiply(int multiplier) {
return new Money(this.value * multiplier);
}
}
→ Money 내에서 특정 기능을 추가할 수 있다.
- 밸류 타입은 immutable(불변)으로 구현한다.
: 데이터 변경 기능을 제공하지 않고 새로운 객체를 생성하여 전달하는 등의 방법 사용
- 불변 객체는 참조 투명성과 스레드에 안전한 특징을 갖고 있다.
- 두 밸류 객체가 같은지 비교할 때는 모든 속성이 같은지 비교해야 한다.
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);
}
}
6. 앤티티 식별자와 밸류 타입
: 식별자가 어떤 도메인의 식별자인지를 분명히 나타내기 위해서 밸류 타입을 사용할 수 있다.
public class Order{
//private String id;
private OrderNo id;
...
}
→ 밸류 타입을 통해 식별자의 의미를 분명히 드러낼 수 있다.
7. 도메인 모델에 set 메서드 넣지 않기
- set메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
- set 메서드를 사용하게 되면 불완전한 도메인을 사용할 수 있기 때문에 , 생성자를 통해 모든 데이터를 받아야 한다. → 생성시점에 필요한 데이터가 올바른지 검사할 수 있다.
- set메서드를 사용하려면 private으로 사용하여 생성자에서만 사용하자.
public class Order{
public Order(Orderer orderer, List<OrderLine> orderLines, ShippingInfo shippingInfo, OrderState state){
setOrderer(orderer);
setOrderLiens(orderLines);
...
}
//생성 시점에 조건 검사
private void setOrderer(Orderer orderer) {
if (orderer == null) throw new IllegalArgumentException("no orderer");
this.orderer = orderer;
}
private void setOrderLines(List<OrderLine> orderLines){
verifyAtLeaseOneOrMoreOrderLines(orderLines);
this.orderLines = orderLines;
calculateTotalAmouts();
}
private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLines){
if (orderLines == null || orderLines.empty()) {
throw new IllegalArgumetException("no OrderLine");
}
}
private void calculateTotalAmouts(){
this.totalAmounts = orderLines.stream().mapToInt(x -> x.getAmounts()).sum();
}
}
'DevBookReview' 카테고리의 다른 글
[DDD START!] 5장 - 리포지터리의 조회 기능(JPA중심) (0) | 2022.01.07 |
---|---|
[DDD START!] 4장 - 리포지터리와 모델 구현 (JPA중심) (0) | 2022.01.07 |
[DDD START!] 3장 - 애그리거트 (0) | 2021.12.31 |
[DDD START!] 2장 - 아키텍처 (0) | 2021.12.28 |
Comments