엔티티와 밸류
도출한 모델은 크게 엔티티(Entity)와 밸류(Value)로 구분한다.
엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있다.
예)
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 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; } } |
밸류타입
public class ShippingInfo { private String receiverName; private String receiverPhoneNumber; private String shippingAddress1; private String shippingAddress2; private String shippingZipcode; } |
ShippingInfo 클래스의 receiverName 필드와 receiverPhoneNumber 필드는 서로 다른 두 데이터를 담고 있지만 두 필드는 개념적으로 받는 사람을 의미한다.
두 필드는 실제로 한 개의 개념을 표현하고 있다.
비슷하게 shippingAddress1 필드 shippingAddress2 필드, shippingZipcode 필드는 주소라는 하나의 개념을 표현한다.
밸류타입은 개념적으로 완전한 하나를 표현할 때 사용한다.
받는 사람을 위한 밸류 타입은 Receiver를 만들수 있다.
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; } } |
Receiver는 받는 사람이라는 도메인 개념을 표현한다.
ShippingInfo의 receiverName 필드와 receiverPhoneNumber 필드가 필드 이름을 통해서 받는 사람을 위한 데이터라는 것을 유추한다면 Receiver는 그 자체로 받는 사람을 뜻한다.
밸류 타입을 사용함으로써 개념적으로 완전한 하나를 잘 표현할 수 있는 것이다.
ShippingInfo의 주소 관련 데이터도 다음의 Address 밸류타입을 사용해서 보다 명확하게 표현할 수 있다.
public class Address { private String address1; private String address2; private String zipCode; } |
밸류타입을 통해 ShippingInfo 클래스를 다시 구현해보자
public class ShippingInfo { private Receiver receiver; private Address address; } |
밸류타입이 꼭 두개 이상의 데이터를 가져야 하는 것은 아니다.
의미를 명확하게 표현하기 위해 밸류 타입을 사용하는 경우도 있다.
public class OrderLine { private Product product; private int price; private int quantity; private int amounts; } |
OrderLine의 price와 amounts는 int 타입의 숫자를 사용하고 있지만 이들이 의미하는 값은 '돈'이다.
따라서 돈을 의미하는 Money 타입을 만들어서 사용하면 코드를 이해하는데 도움이 된다.
public class Money { private int value; public Money(int value) { this.value = value; } public int getValue() { return value; } } |
Money를 사용하도록 OrderLine을 변경한다.
Money타입 덕에 price나 amounts가 금액을 의미한다는 것을 쉽게 알수 있다.
public class OrderLine { private Product product; private Money price; private int quantity; private Money amounts; } |
밸류타입을 사용할 때의 또 다른 장점은 밸류 타입을 위한 기능을 추가 할 수 있다.
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를 사용하는 코드는 이제 '정수 타입 연산'이 아니라 '돈 계산'이라는 의미로 코드를 작성할 수 있게 된다.
코드의 가독성 향상
밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식으로 선호한다.
Money 클래스의 add()메서드를 보면 Money를 새로 생성하고 있다.
Money처럼 데이터 변경 기능을 제공하지 않는 타입을 불변(immutable)이라고 표현한다.
밸류 타입을 불변으로 구현한 이유는 여러가지가 있는데 가장 중요한 이유는 불변타입을 사용하면 보다 안전한 코드를 작성할 수 있다.
public OrderLine(Product product, Money price, int quantity) { this.product = product; // Money가 불변 객체가 아니라면 // price 파라미터가 변경 될때 발생하는 문제를 방지 하기 위해 // 데이터를 복사한 새로운 객체를 생성해야 한다. this.price = new Money(price.getValue()); this.quantity = quantity; this.amounts = calculateAmounts(); } |
Money의 데이터를 바꿀 수 없기 때문에 파라미터로 전달 받은 price는 안전하게 사용 할 수 있다.
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; } @Override 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); } } |
public class Order { // OrderNo 타입 자체로 id가 주문번호임을 알 수 있다. private OrderNo id; public OrderNo getId() { return id; } } |
OrderNo 대신 String 타입을 사용한다면 'id'라는 이름만으로는 해당 필드가 주문번호인지 여부를 알수 없다.
public class UserInfo { private String id; private String name; public UserInfo() { } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } } |
get/set 메서드를 습관적으로 만드는 이유는 여러가지가 있겠지만 가장 큰 이유라면 프로그래밍에 입문할 때 읽은 책의 예제 코드 때문이라고 생각한다.
처음 프로그래밍을 배울 때 익힌 예제 코드를 그대로 따라하다보니 상황에 관계없이 get/set 메서드를 습관적으로 추가하는 것이다.
도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않는 버릇이다.
특히 set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
public class Order { private String orderNumber; private OrderState OrderState; private ShippingInfo shippingInfo; public void setOrderState(OrderState state) { this.state = state; } public void setShippingInfo(ShippingInfo shippingInfo) { this.shippingInfo = shippingInfo; } |
changeShippingInfo()가 배송지 정보를 새로 변경한다는 의미를 가졌다면 setShippingInfo()메서드는 단순히 배송지 값을 설정한다는 것을 뜻한다.
complatePayment()는 결제가 완료 햇다는 의미를 갖는 반면에 setOrderState()는 단순히 주문 상태값을 설정한다는 것을 뜻한다.
구현할 때에도 complatePayment()는 결제 완료와 관련된 처리 코드를 함께 구현하기 때문에 결제 완료와 관련된 도메인 지식을 코드로 구현하는 것이 자연스럽다.
setOrderState()는 단순한 상태 값만 변경할지 아니면 상태 값에 따라 다른 처리를 위한 코드를 함께 구현할지 애매하다.
습관적으로 코드를 작성하는 경우라면 필드 값만 변경하고 끝나는 경우가 많기 때문에 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다.
set메서드의 또 다른 문제는 도메인 객체를 생성할 때 완전한 상태가 아닐 수도 있다는 것이다.
//set 메서드로 데이터를 전달하도록 구현하면 // set 메서드로 피료한 모든 값을 전달해야 함 order.setOrderLine(lines); order.setShippingInfo(shippingInfo); //주문자(Orderer)를 설정하지 않는 상태에서 주문 완료 처리 order.setState(OrderState.PREPARING); |
주문자를 설정하는 것을 누락하고 있다. 주문자 정보를 담고 있는 필드인 orderer가 null인 상황에서 order.setState() 메서드로 상품 준비 중 상태로 바뀌는 것이다.
orderer가 정상인지 확인하기 위해 orderer가 null인지 검사하는 코드를 setState()메서드에 위치하는 것도 맞지 않다.
도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다.
즉 생성자를 통해 필요한 데이터를 모두 받아야 한다.
public class Order { private ShippingInfo shippingInfo; private Orderer orderer; public Order(Orderer orderer, List<OrderLine> orderLines, ShippingInfo shippingInfo, OrderState state) { setOrderer(orderer); setOrderLines(orderLines); setShippingInfo(shippingInfo); } private void setOrderer(Orderer orderer) { if(orderer == null) { throw new IllegalArgumentException("no orderer"); } this.orderer = orderer; } private void setShippingInfo(ShippingInfo shippingInfo) { if(shippingInfo == null) { throw new IllegalArgumentException("no ShippingInfo"); } this.shippingInfo = shippingInfo; } 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() { this.totalAmounts = orderLines.stream().mapToInt(x->x.getAmounts()).sum(); } } |
set메서드는 앞서 set 메서드와 중요한 차이점이 있는데 그것은 바로 접근 범위가 private라는 점이다.
set메서드는 클래스 내부에서 데이터를 변경할 목적으로 사용된다. private이기 때문에 외부에서 데이터를 변경할 목적으로 set 메서드를 사용할 수 없다.
불변 밸류 타입을 사용하면 자연스럽게 밸류 타입에는 set메서드를 구현하지 않는다.
set 메서드를 구현해야 할 특별한 이유가 없다면 불변 타입의 장점을 살릴 수 있도록 밸류 타입은 불변으로 구현한다.
public OrderState { STEP1, STEP2, STEP3, STEP4, STEP5, STEP6 } |
실제 주문 상태는 '결제 대기중', '상품준비중', '출고 완료됨', '배송 중', '배송완료됨','주문 취소됨'인데
이 코드는 개발자가 전체 상태를 6단계를 보고 코드로 표현한 것이다.
이 개발자는 Order 코드를 다음과 같이 작성할 가능성이 높다.
public void changeShippingInfo(ShippingInfo newShippingInfo) { verifyStep1OrStep2(); setShippingInfo(newShippingInfo); } private void verifyStep1OrStep2() { if(state != OrderState.STEP1 && state != OrderState.STEP2) { throw new IllegalStateException("aleady shipped"); } |
배송지 변경은 '출고 전'에 가능한데, 이 코드의 verifyStep1OrStep2라는 이름은 도메인의 중요 규칙이 드러나지 않는다.
그저 STEP1과 STEP2인지 검사만 할 뿐이다.
실제 이 코드의 의미를 이해하려면 STEP1과 STEP2가 각각 결제 대기중 상태와 상품 준비중 상태를 의미한다는 것을 알아야한다.
public enum OrderState { PAYMENT_WATING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLATED, CANCELD; } |
코드를 도메인 용어로 해석하거나 도메인 용어를 코드로 해석하는 과정이 줄어든다.
코드의 가독성을 높여서 코드를 분석하고 이해하는 시간을 절약된다.
'JAVA > DDD' 카테고리의 다른 글
트랜잭션 범위 (0) | 2017.12.23 |
---|---|
Aggregate 애그리거트 (0) | 2017.12.19 |
아키텍처 (0) | 2017.12.18 |
도메인 모델 도출 (0) | 2017.12.15 |
DDD - 도메인 모델 패턴 (0) | 2017.12.15 |