티스토리 뷰
반응형
chapter 1
1.1 도메인이란?
- 도메인: 소프트웨어로 해결하고자 하는 문제 영역
- 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
- 하나의 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
- 특정한 도메인을 위한 소프트웨어라고 해서 모든 기능을 직접 구현하지는 않는다.
- 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.
- 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
1.2 도메인 전문가와 개발자 간 지식 공유
요구사항의 중요성
- 개발자는 요구사항을 분석하고 설계하여 코드를 작성하고 테스트하고 배포한다.
- 요구사항을 처음부터 제대로 설계하지 않으면 코드를 수정해야한다.
- 잘못 개발한 코드를 수정하는 것은 많은 비용이 소모된다.
요구사항을 올바르게 이해하고 설계하는 방법
- 개발자와 전문가가 직접 대화하는 것이 가장 좋은 방법이다.
- 도메인 전문가까지는 아니더라도, 이해관계자와 개발자도 도메인 지식을 갖춰야 한다.
- 제품 개발과 관련된 도메인 전문가, 관계자, 개발자가 같은 지식을 공유하고 직접 소통할수록 도메인 전문가가 원하는 제품을 만들 가능성이 높아진다.
1.3 도메인 모델
도메인 모델의 정의
- 도메인 모델: 특정 도메인을 개념적으로 표현한 것.
- 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다.
- 개념 모델를 통해 바로 코드를 작성할 수 없기 때문에 구현 기술에 맞는 구현 모델이 필요하다.
- ex) 객체 기반 모델을 기반으로 도메인을 표현했다면 객체 지향 언어를 이용해 개념 모델에 가깝게 구현할 수 있다.
- 개념 모델를 통해 바로 코드를 작성할 수 없기 때문에 구현 기술에 맞는 구현 모델이 필요하다.
- 도메인 모델의 종류
- 객체
- 다이어그램
- 그래프
- 수학 공식
1.4 도메인 모델 패턴
- 어플리케이션 아키텍쳐의 구성
- 사용자 인터페이스 또는 표현: 사용자의 요청을 처리하고 사용자에게 정보 제공
- 응용: 사용자가 요청한 기능을 실행
- 도메인: 시스템이 제공할 도메인 규칙 구현
- 인프라스트럭쳐: 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동 처리
- 새로운 도메인 모델의 개념
- 도메인 모델 == 도메인 모델 패턴: 도메인 규칙을 객체 지향 기법으로 구현하는 패턴
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;
}
}
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;
}
private boolean isShippingChangeable(){
return state == OrderState.PAYMENT_WAITING || state == OrderState.PREPARING;
}
}
public enum OrderState {
PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}
- 위 두개의 코드에서 가장 중요한점은 배송지 변경 가능 여부를 판단하는 기능이 Order에 있든, OrderState에 있든 중요한 점은 주문과 관련된 중요 업무 규칙을 주문 도메인 모델인 Order나 OderState에서 구현한다는 점이다.
- 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.
1.5 도메인 모델 도출
도메인 모델 작성 예시
- 요구사항에서 모델을 구성하는 핵심 구성요소, 규칙, 기능을 먼저 찾는다.
- 예시 요구사항
- 최소 한 종류 이상의 상품을 주문해야 한다.
- 한 상품을 한 개 이상 주문할 수 있다.
- 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
- 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
- 주문할 때 배송지 정보를 반드시 지정해야 한다.
- 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
- 출고를 하면 배송지 정보를 변결할 수 없다.
- 출고 전에 주분을 취소할 수 있다.
- 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
- 주문의 기능 추리기
- 출고 상태로 변경하기
- 배송지 정보 변경하기
- 주문 취소하기
- 결제 오나료하기
- 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 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; } }
- OrderLine과 Order의 관계를 정의한 Order 업데이트
- ShippingInfo 생성 후 Order에 적용
public class Order { private List<OrderLine> orderLines; private Money totalAmounts; private ShippingInfo shippingInfo; public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo){ setOrderLines(orderLines); setShippingInfo(shippingInfo); } private void setOrderLines(List<OrderLine> orderLines) { verifyAtLeastOneOrMoreOrderLines(orderLines); this.orderLines = orderLines; calculateTotalAmounts(); } private void setShippingInfo(ShippingInfo shippingInfo){ if(shippingInfo == null) throw new IllegalArgumentException("no ShippingInfo"); this.shippingInfo = shippingInfo; } private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLines){ if(orderLines == null || orderLines.isEmpty()){ throw new IllegalArgumentException("no OrderLine"); } } private void calculateTotalAmounts(){ int sum = orderLines.stream().mapToInt(x -> x.getAmounts()).sum(); this.totalAmounts = new Money(sum); } }
- public class ShippingInfo { private String receiverName; private String receiverPhoneNumber; private String shippingAddress1; private String shippingAddress2; private String shippingZipcode; }
- 주문 상태 메소드 수정
public enum OrderState { PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED, CANCELED; }
- 예시 요구사항
public class Order {
private List<OrderLine> orderLines;
private Money totalAmounts;
private ShippingInfo shippingInfo;
private OrderState state;
public Order(List<OrderLine> orderLines, ShippingInfo shippingInfo){
setOrderLines(orderLines);
setShippingInfo(shippingInfo);
}
private void setOrderLines(List<OrderLine> orderLines) {
verifyAtLeastOneOrMoreOrderLines(orderLines);
this.orderLines = orderLines;
calculateTotalAmounts();
}
private void setShippingInfo(ShippingInfo shippingInfo){
if(shippingInfo == null)
throw new IllegalArgumentException("no ShippingInfo");
this.shippingInfo = shippingInfo;
}
private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLines){
if(orderLines == null || orderLines.isEmpty()){
throw new IllegalArgumentException("no OrderLine");
}
}
private void calculateTotalAmounts(){
int sum = orderLines.stream().mapToInt(x -> x.getAmounts()).sum();
this.totalAmounts = new Money(sum);
}
public void changeShippingInfo(ShippingInfo newShipping){
verifyNotYetShipped();
setShippingInfo(newShipping);
}
public void cancel(){
verifyNotYetShipped();
this.state = OrderState.CANCELED;
}
private void verifyNotYetShipped() {
if(state != OrderState.PAYMENT_WAITING && state != OrderState.PREPARING)
throw new IllegalStateException("already shipped");
}
}
public class Order {
private List<OrderLine> orderLines;
private Money totalAmounts;
private void setOrderLines(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(){
int sum = orderLines.stream().mapToInt(x -> x.getAmounts()).sum();
this.totalAmounts = new Money(sum);
}
}
1.6 엔티티와 밸류
- 도출한 모델은 Entity와 Value로 구분할 수 있다.
- 이 둘을 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 이기 때문에 이 둘의 차이를 명확핳게 이해해야한다.
1.6.1 Entity
- 도메인 모델 내에서 식별자를 가지는 하나의 Class
- Entity의 식별자를 사용하는 예시 코드
- public class Order { private 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 order = (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.6.2 엔티티의 식별자 생성
- 엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.
- 식별자 생성 방식
- 특정 규칙
- 주문번호
- 운송장번호
- 카드번호
- UUID나 Nano ID와 같은 고유 식별자 생성기 사용
- 값을 직접 입력
- 회원 아이디
- 회원 이메일
- 일렬번호 사용(시퀀스나 DB Auto Increment)
- 주로 DB에서 제공하는 기능
- 특정 규칙
1.6.3 Value type
- Value type은 개념적으로 완전한 하나를 표현할 때 사용된다.
- Receiver Value type 작성
- 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; } }
- address value type 작성 후 코드 가독성 강화
public class ShippingInfo { private Receiver receiver; private Address address; /*private String receiverName; private String receiverPhoneNumber; private String shippingAddress1; private String shippingAddress2; private String shippingZipcode;*/ }
- public class Address { private String address1; private String address2; private String zipcode; public Address(String address1, String address2, String zipcode){ this.address1 = address1; this.address2 = address2; this.zipcode = zipcode; } }
- Money value type 생성 후 가독성 강화
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.value = value; } public int getValue(){ return value; } }
- Value type의 장점 예시 (Money 타입을 활용한 기능 추가)
- Money를 사용하는 코드에서 “정수형 연산”이 아닌 “돈 계산”이라는 의미로 코드를 작성할 수 있다.
- 벨류 객체의 데이터를 변경할 때는 기존 데이터를 변경하는 것이 아닌 새로운 밸류 객체를 생성하는 방식을 선호함
- Money의 add() 메소드의 경우 new 메소드를 사용해 새로운Money객체를 생성하였다.
- public class Money { private int value; public Money(int value){ this.value = value; } public int getValue(){ return value; } public Money add(Money money){ return new Money(this.value + money.value); } public Money multiply(int multiplier){ return new Money(value * multiplier); } }
- Money처럼 데이터 변경 기능을 제공하지 않는 타입을 불변이라고 표현한다.
- Value type을 불변으로 구현하는 이유는 안전한 코드를 위해서이다.
- OrderLine에 Price값이 잘못 반영된다.
- Money price = new Money(1000); OrderLine line = new OrderLine(product, price, 2); price.setValue(2000);
1.6.4 엔티티 식별자와 밸류 타입
- Entity의 식별자 같은 경우 String과 같은 문자열로 구성된 경우가 많다.
- Money가 단순 숫자가 아닌 도매인 ‘돈’을 의미하는 것처럼 식별자도 Value type을 사용해 구현할 수 있다.
1.6.5 도메인 모델에 set 메서드 넣지 않기
- 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않다.
- 특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
- chageShippingInfo() → setShippingInfo()
- 배송지 정보를 새로 변경한다는 의미에서 단순히 배송지 값을 설정한다는 의미를 가짐.
- completePayment() → setOrderState()
- 결제 완료했다는 의미에서 주문 상태 값을 설정한다는 의미를 가짐.
- chageShippingInfo() → setShippingInfo()
- set 메서드는 도메인 객체를 생성할 때 온전하지 않은 상태가 될 수 있다.
Order order = new Order(order, lines, shippingInfo, OrderState.PREPARING);
- 첫번째 코드는 주문자 설정을 누락하고 있다.
- 마지막 코드처럼 생성 시점에 필요한 것을 전달해 주어야 한다.
- 생성자로 필요한 것을 모두 받으므로 생성자를 호출하는 시점에 필요한 데이터가 올바른지 검사할 수 있다.
- Type value의 set 메서드는 접근 범위를 private로 함으로서 외부에서는 변경할 수 없도록 한다.
- Order order = new Order(); order.setOrderLine(lines); order.setShippingInfo(shippingInfo); order.setState(OrderState.PREPARING);
- 불변 밸류 타입을 사용하면 set 메서드를 구현하지 않는다.
- set 메서드를 구현해야 할 특별한 이유가 없다면 밸류 타입은 불변으로 구현한다.
1.7 도메인 용어와 유비쿼터스 언어
- 도메인에서 사용되는 언어는 개발자의 코드 의미 해석과 관련되어 있기에 매우 중요하다.
- 위의 코드처럼 도메인의 용어를 사용해 버그와 시간을 줄일 수 있다.
public enum OrderState { STEP1, STEP2, STEP3, STEP4, STEP5, STEP6; }
- public enum OrderState { PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED, CANCELED; }
- 유비쿼터스 언어는 용어의 모호함을 줄일 수 있고 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다.
- 도메인 용어에 시간을 아까워하지 말자.
반응형
'programming > DDD' 카테고리의 다른 글
[DDD] chapter 6 (0) | 2024.02.10 |
---|---|
[DDD] chapter 5 (1) | 2024.02.07 |
[DDD] chapter 4 (0) | 2024.02.03 |
[DDD] chapter 3 (0) | 2024.01.18 |
[DDD] chapter2 (1) | 2024.01.13 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Spring Boot
- 기록지
- 코테
- C언어
- 알고리즘
- c++
- CJ 올리브네트웍스
- 안드로이드 스튜디오
- CJ
- 문자열
- 프로그래머스
- CJ Olivenetworks
- 육군
- XML
- BaekJoon
- 백준 알고리즘
- 자료구조
- 개발자
- 코딩
- Programmers
- 비트코인
- 코딩테스트
- Python
- 백준알고리즘
- 구현
- java
- spring
- 후기
- 백준
- 안드로이드 프로그래밍
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
글 보관함