도메인
- 예를 들어 온라인 서점을 구현한다고 하면, 개발자 입장에서는 온라인 서점은 구현해야 할 소프트웨어의 대상이 된다.
- 온라인 서점 소프트웨어는 온라인으로 책을 판매하는 데 필요한 상품조회, 구매, 결제, 배송 추적 등의 기능을 제공해야한다.
- 이때 ‘온라인 서점’은 소프트웨어로 해결하고자 하는 문제, 즉 도메인에 해당한다.
- 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
- 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 구현하는 것은 아니다.
- 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
- 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.
- 한 도메인은 다시 하위 도메인으로 나눌 수 있다.
도메인 모델
- 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것이다.
- 도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다.
- 도메인을 이해하려면 도메인이 제공하는 기능과 도메인의 주요 데이터 구성을 파악해야 한다.
- 도메인을 이해하는 데 도움이 된다면 표현 방식이 무엇인지는 중요하지 않다.
- 도메인 모델은 기본적으로 도메인 자체를 이해하기 위한 개념 모델이다.
도메인 모델 패턴
- 일반적인 애플리케이션의 아케텐처는 네 개의 계층으로 구성된다.
계층(Layer) | 설명 |
---|---|
사용자인터페이스(UI) 또는 표현(Presentiation) | 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부 시스템도 사용자가 될 수 있다. |
응용(Application) | 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다. |
도메인 | 시스템이 제공할 도메인의 규칙을 구현한다. |
인프라스트럭처(Infrastructure) | 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다. |
-
도메인 계층은 도메인의 핵심 규칙을 구현한다. 예를 들어, 주문 도메인인 경우 ‘출고 전에 배송지를 변경할 수 있다.’는 규칙과 ‘주문 취소는 배송 전에만 할 수 있다.’는 규직츨 구현한 코드가 도메인 계층에 위차하게 된다.
- 이런 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다.
- 아래 코드는 주문 도메인의 일부 기능을 도메인 모델 패턴으로 구현한 것이다.
public class Order {
private OrderState state;
private ShippingInfo shippingInfo;
public void changeShippingInfo(ShippingInfo newShippingInfo) {
//변경 가능 여부를 확인
if(!state.isShippingChangeable()) {
throw new Exceiption("can't chage shipping in "+sate);
}
this.shippingInfo = newShippingInfo;
}
public void changeShippped() {
//로직검사
this.state = OrderState.SHIPPED;
}
//...
}
//배송지를 변경할 수 있는지 여부를 검사할 수 있는 메서드 제공
//주문 대기 중, 상품 준비 중에는 배송지를 변경할 수 있다는 규칙을 구현
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클래스에서 판단하도록 수정한 코드이다.
public class Order {
private OrderState state;
private ShippingInfo shippingInfo;
public void changeShippingInfo(ShippingInfo newShippingInfo) {
if(!isShippingChangeable()) {
throw new Exception("can't change shipping in "+state);
}
this.shippingInfo = newShippingInfo;
}
private boolean isShippingChangeable() {
return state == OrederState.PAYMENT_WAITING ||
state == OrderState.WAITING;
}
//...
}
public enum OrderState {
PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}
-
배송지 변경이 가능한지 여부를 판단할 규칙이 주문 상태와 다른 정보를 함께 사용한다면 배송지 변경 가능 여부 판단을 OrderState만으로 할 수 없으므로 로직 구현을 Order에서 해야 한다.
-
핵심 규칙을 구현한 코드는 도메인 모델에만 위지하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.
도메인 모델 도출
- 도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.
* 주문 도메인과 관련된 요구사항
1. 최소 한 종류 이상의 상품을 주문해야 한다.
2. 한 상품을 한 개 이상 주문할 수 있다.
3. 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
4. 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
5. 주문할 때 배송지 정보를 반드시 지정해야 한다
6. 배송지 정보는 받는 사람 이름, 전화번호, 주소로 구성된다.
7. 출고를 하면 배송지 정보를 변경할 수 없다.
8. 출고 전에 주분을 취소할 수 있다.
9. 고객이 결제를 완료하기 전에는 상품을 준비하지 않는다.
- 요구사항에서 알 수 있는 것은 주문은 ‘출고 상태로 변경하기’, ‘배송지 정보 변경하기’, ‘주문 취소하기’, ‘결재 완료로 변경하기’의 네 기능을 제공한다는 것이다.
- Oder에 관련 기능을 메소드로 추가할 수 있다.
public class Order {
public void changeShipped() { }
public void changeShippingInfo(ShippingInfo newShipping) { }
public void cancel() { }
public void completePayment() { }
}
- 아래 항목은 주문 항목이 어떤 데이터로 구성되는지 알려준다.
- 한 상품을 한 개 이상 주문할 수 있다.
- 각 상품의 구매 가격 합은 상품 가격에 구매 개수를 곱한 값이다.
- 주문 항목을 표현하는 OrderLine은 적어도 주문할 상품, 상품의 가격, 구매 개수를 포함하고 있어야 한다.
- 아래는 이를 구현한 OrderLine의 코드이다.
public class OrderLine {
private Product product; //상품
private int price; //가격
private int quantity; //수량
private int amounts; //구매가격
public OrderLine(Prodect product, int price, int quantity) {
this.product = product;
this.private = price;
this.quantity = quantity;
this.amounts = calculateAounts();
}
//구매 가격 구하기
private int calculateAounts() {
return price * quantity;
}
public int getAmounts() { }
}
- 아래 항목은 Order와 OderLine과의 관계를 알려준다.
- 최소 한 종류 이상의 상품을 주문해야 한다.
- 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다.
- 한 종류 이상의 상품을 주문할 수 있으므로 Order는 최소 한 개 이상의 OrderLine을 포함해야 한다.
- OrderLine으로부터 총 주문 금액을 구할 수 있다.
public class Oreder {
private List<OrderLine> ordrLines;
private int totalAmounts;
public Order(List<OrederLine> orderLines) {
setOrderLines(orderLines);
}
private void setOrderLines(List<OrderLine> orderLines) {
verifyAtLeastOneOrMoreOrderLines(orderLines);
this.orderLines = orderLines;
calculateTotalAmounts();
}
private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLine) {
if(orderLines == null || orderLines.isEmpty()) {
throw new Exception("no OrderLine");
}
}
private void calculateTotalAmounts() {
this.totalAmounts = new Money(orderLines.stream()
.mapToInt(x -> x.getAmounts().getValue()).sum();
}
//....
}