티스토리 뷰
개요
# 참고
DDD START! - 최범균 지음
# 개요
DDD에 대한 기술서적을 읽고 DDD가 무엇인지 이해하고 어떻게 코드에 적용시킬 수 있는지 고민해 보려고 한다.
# 11장의 로드맵
- 도메인 모델
- 엔티티와 밸류
- 도메인 용어
1장. 도메인 모델 시작
도메인은 소프트웨어로 해결하자 하는 문제의 영역을 의미한다. 배달의 민족에서 "배달 커머스"가 도메인에 해당한다. 이런 도메인은 다시 몇 개의 하위 도메인으로 구성될 수 있다. 하위 도메인에는 주문, 상품, 결제, 회원, 배송, 리뷰 등 다시 다양한 도메인으로 나뉘게 된다. 서비스 개발에서 모든 도메인을 직접 개발할 필요가 없다. 결제와 배송은 외부 소프트웨어를 사용해도 된다. 따라서 하위 도메인은 어떻게 구성할지 여부는 상황에 따라 달라진다.
1-1. 도메인 모델
도메인 모델은 특정 도메인을 개념적으로 표현한 것이다. 모델은 도메인의 모든 내용을 담고 있지는 않지만 이 모델을 보면 큰 틀을 파악할 수 있다. 즉, 도메인 모델을 사요하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다. 도메인 모델은 객체로 모델링 할 수도 있고, 상태 다이어그램을 이용해서 모델링을 할 수도 있다.
1-2. 엔티티와 밸류
도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다. 엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있기 때문에 이 둘의 차이를 명확하게 이해하는 것은 도메인을 구현하는 데 있어 중요하다.
엔티티
엔티티의 가장 큰 특징은 식별자를 갖는다는 것이다. 식별자는 엔티티 객체마다 고유해서 각 엔티티는 서로 다른 식별자를 갖는다. 주문에서 배송지 주소가 바뀌거나 상태가 바뀌더라도 주문번호가 바뀌지 않는 것처럼 엔티티의 식별자는 바뀌지 않는다. 엔티티를 생성하고 엔티티의 속성을 바꾸고 엔티티를 삭제할 때까지 식별자는 유지된다.
엔티티의 식별자 생성
엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다. 보통은 다음과 같은 4가지를 중 한 가지 방식으로 생성한다.
- 특별한 규칙에 따라 생성
- UUID 사용
- 값을 직접 입력
- 시퀀스나 자동 증가 컬럼 사용 (Auto Increment)
밸류 타입
AS-IS | TO-BE |
public class ShippingInfo{ //배송 정보 private String name; private String phoneNumber; private String address1; private String address2; private String address3; } |
public class ShippingInfo{ //배송정보 private Reciever reciever; //주문자 정보 private Address address; //배송 정보 } public class Receiver{ //주문자 정보 private String name; private String phoneNumber; } public class Address{ //주소 정보 private String address1; private String address2; private String address3; } |
AS-IS와 TO-BE 코드의 차이가 어떻게 생각하세요? AS-IS는 각 인스턴스 변수들이 서로의 공통된 특성도 가지고 있지만 서로 다른 특성들도 가지고 있습니다. 반면 TO-BE같은 경우 배송정보 안에 명확히 공통된 특성들로만 묶여 있습니다.
즉, 밸류타입을 사용함으로써 개념적으로 완전한 하나를 잘 표현할 수 있다는 것이다. 밸류타입은 꼭 2개 이상의 데이터를 가져야 하는 것은 아니다. 데이터가 하나더라도 명확한 의미를 표현할 수 있다면 구현해도 된다.
밸류타입을 사용하면 의미를 좀 더 명확하게 표현할 수 있다. 또한 밸류 타입을 위한 특별한 기능을 추가할 수 있다. 여기서 말하는 특별한 타입은 불변기능을 의미한다. 서비스를 구현하다 보면 절대 변경되지 않아야 하는 기능들이 있다. 즉, 데이터 변경 기능을 제공하지 않는 불변 타입을 구현할 수 있다.
AS-IS | TO-BE |
public class Money { private int value; //setter public void setValue(int value){ this.value = value } } |
public class Money { private int value; // 금액 추가 public Money add(Money money){ return new Money(this.value + money.value) } //setter } |
일반적으로 JAVA를 처음 배우거나, Spring을 통해 웹 서비스를 처음 배우게 될 때 가장 많이 사용하는 기능이 setter이다. 따라서 많은 주니어 개발자가 아무 생각없이 setter를 구현하지만 이는 정말 위험하다. 엔티티는 핵심한 코어이기 때문에 값이 변경이 되면 여러 기능들에 영향을 미친다. 즉, 개발자가 의도한 변경이 아닌 실수로 변경하게 된다면 큰 문제를 야기 시킬 수 있다. 따라서 불변객체로 만들어 외부에서 함부로 값을 변경하지 못하게 하는 것이 중요하다. AS-IS를 보면 언제든지 setter가 구현되어 있어 언제든지 외부에서 값을 변경할 수 있다. 반면 TO-BE는 밸류 객체의 데이터를 변경할 때는 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다. AS-IS는 데이터 변경 기능 Setter를 구현하지 않도록 하여 불변이라는 기능을 제공한다.
엔티티 식별자와 벨류타입
Money가 단순 숫자가 아닌 도메인의 '돈'을 의미하는 것처럼 이런 식별자는 단순한 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많기 때문에 식별자를 위한 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다. 예를 들어 우리가 흔히 사용하는 시퀀스 넘버, 주문번호 역시 int를 사용하기 보다는 OrderNo 밸류타입을 사용하면 해당 필드가 주문번호라는 것을 알 수 있다.
AS-IS | TO-BE |
public class Order{ private int orderNo; } |
public class Order{ private OrderNo id; } |
이렇게 사용하면 OrderNo 타입 자체로 id가 주문번호임을 알 수 있다. (나는 AS-IS가 방식을 선호한다. 실제 서비스에서는 테이블이 수백개가 될텐데 테이블마다 해당 클래스를 만드는 것은 꼭 좋은 것은 아니라고 생각한다.)
도메인 모델에 set메서드 넣지 않기
자바를 처음 시작하는 개발자들은 get/set메서드를 습관적으로 추가한다. 도메인 모델에 get/set 메서드를 무조건 추가하는 것은 좋지 않은 버릇이다. 특히 set메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
set메서드의 또 다른 문제는 도메인 객체를 생성할 때 완전한 상태가 아닐 수도 있다는 것이다.
AS-IS |
Order order = new Order( ); order.setOrderLine(lines); order.setShippingInfo(shippingInfo); //order.setOrderName(name); // 주문자를 설정하지 않은 상태에서 주문 완료 처리 order.setState(OrderState.PREPARING); |
TO-BE |
Order order = new Order(order, lines, shippingInfo, OrderState.PREPARING ); public class Order{ public Order(Orderer orderer, List<orderLines> orderLines, ShippingInfo shippingInfo, OrderState state){ setOrder(orderer); setOrderLines(orderLines); .... //다른 값 설정 } // 주문자 null 체크 private void setOrderer(Orderer orderer){ if ( orderer = null ) { throw new IllegalArgumentException( "no orderer"); } // null 체크 or 검증 로직 ... } } |
AS-IS는 주문자를 설정하는 것을 누락하고 있다. 이 처럼 set을 무작정 사용하는 것은 매우 위험한 일이다. 도메인 객체가 불완전한 상태로 사용되는 것을 막으려면 생성 시점에 필요한 것을 전달해 주어야 한다. 즉, 생성자를 통해 필요한 데이터를 모두 받아야 한다.
생성자로 필요한 것을 모두 받으므로 다음처럼 생성자를 호출하는 시점에 필요한 데이터가 올바른지 검사할 수 있다. 이 코드의 set 메서드는 앞의 set메서드와 중요한 차이점이 있는데 그것은 접근 범위가 private라는 점이다. 이 코드에서 set메소드는 클래스 내부에서 데이터를 변경할 목적으로 사용된다. private이기 때문에 외부에서 데이터를 변경할 모적으로 set메서드를 사용할 수 없다.
불변 밸류 타입을 사용하면 자연스럽게 밸류 타입에는 set메서드를 구현할지 않는다. set메서드를 구현해야 할 특별한 이유가 없다면 불변 타입의 장점을 살릴 수 있도록 밸류타입은 불변으로 구현해야 한다.
정리1. 엔티티에는 여러 밸류타입을 선언할 수 있다. 밸류타입에서는 set메소드를 선언하지 않는다? 오직 엔티티에서만 set을 사용할 수 있지만 private로 설정하여 생성자 안에서만 사용할 수 있도록 하여 객체를 생성하는 시점에만 사용할 수 있도록 한다.
정리2. DTO에도 정리1과 같은 개념을 적용할 수 있다. 스프링 프로젝트에서 DTO는 매우 많이 사용이 된다. Request / Response를 FE에게 전달받고 전달해 주어야 하니깐.
Q. 그렇다면 우리는 DTO에는 setter를 구현해야 할까?
A. 나는 굳이 구현할 필요가 없다고 생각한다. DTO란 FE에게 데이터를 전달하거나 전달하는 역할을 수행한다고 생각한다. 그렇다면 우리가 그 상태를 변경시킬 필요가 있을까? 만약 비밀번호를 암호화 해야하는 경우 처럼 DTO에서도 setter가 필요하다면 불변객체로 만들어서 전달하는게 좋은것 같다. 생성자에서 값을 변경시켜 줄 수 있는!!