도메인 주도 설계(DDD)는 사용자를 위한 프로젝트 설계 방법 중 하나입니다.
도메인을 중심으로 설계를 하며 개발자는 반드시 도메인에 대한 이해가 필요하다.
설계 단계에서 부터 참여하여 비지니스와 꾸준히 소통하는 Agile방법론에 적합합니다.
도메인이란 프로그램이 쓰이는 대상분야이다. 쇼핑몰로 예시를 들자면 장바구니 담기, 상품 발송 등등이 도메인이 될 수 있다. 도메인 객체가 도메인 모델을 충분히 반영하고 있다면 도메인의 변화를 보다 코드로 옮기기 쉬워진다.
“운전기사가 차량을 운행하여 물건을 실어나른다” 라는 도메인을 모델링 하면 운전기사, 운행, 차량정보와 같은 도메인 객체가 생성된다.
DDD를 왜 써야 할까? 개발자에게 보다 프로젝트를 이해하기 쉽게하여 코드를 손쉽게 구성하기 위해서다. 특정 행위를 한다면 엔티티가 반드시 있어야 할테고 이를 기반으로 DB에 엔티티를 생성할 수 있다. DB설계가 되어있어야 개발이 가능한데 DB가 왜이렇게 설계되었는지 비지니스와 소통으로 알수있다. 즉, 소프트웨어의 유연성 확보와 비지니스 변화에 빠르게 대응할 수 있게 된다.
도메인 모델 패턴은 아래와 같다.
도메인 모델은 Entity와 Value Object로 분류 할 수 있다.
Entity | VO | DTO | |
---|---|---|---|
정의 | 식별자를 가지는 도메인을 표현한 객체 | 도메인을 표현한 값 표현용 객체 | Layer간 데이터 전송용 객체 |
상태 변경 여부 | 상태를 변경할 수 없는 가변성 | 상태를 변경할 수 없는 불변성. 값을 변경하려면 새 객체 생성 필요 | 가변 또는 불변 객체 |
동등 값 비교 기준 | 두 객체의 식별자가 같다면 같은 객체로 간주 | 같은 값의 객체가 존재한다면 두 객체는 같은 객체로 간주 | - |
로직 포함 여부 | 로직 포함 가능 | 로직 포함 가능 제약 조건을 걸거나 가능한/가능하지 않은 행위 구현 | 로직 포함할 수 없음 |
즉, 식별자를 갖고있냐 아니냐에 따라 Entity, VO로 나눌 수 있으며 Entity는 DB를 생성하는 컬럼들의 집합이다.
Aggregate는 데이터의 변경을 다루는 Entity와 VO의 군집이다. 도메인이 커지며 Entity와 VO가 많아지며 모델이 복잡해지는데, 상위 수준에서 모델을 식별할 수 있어 도메인 모델에 대한 이해를 돕는다.
Aggregate Root는 전체를 관리할 주체이다. Aggregate 구성 항목의 일관성이 깨지지 않도록 한다.
즉, Aggregate 외부에서 Aggregate 내부 객체를 직접 변경하면 안되고 Root Entity를 통해서 직접 접근해야 한다.
이 예시에서 주문자(Aggregate 내부 객체)를 변경하려면 주문(Root Entiy)을 통해서 직접 변경해야 한다.
Set Method 사용하지 않기
Value Object는 불변으로 구현하기
응집도 향상을 위해 Entity 식별자, 값을 Value Object 사용하기
RouteChargeCalcService는 운행결과 승인 및 운임 조정율 변경, 운임을 계산하기 위한 서비스로써 도메인 서비스에 구현되어있다.
retrieveAdjustRatio는 RoutCharge 도메인 내에서도 구현이 충분히 가능하므로 도메인 내부에 구현되어야 한다.
searchTemplateDetail은 단순 조회임에도 불구하고 도메인 레벨에 생성되었다. 이는 애플리케이션 레벨에 구현되어야 한다.
A. 도메인에 대한 자기 서술적인 코드가 작성될 수 있음. 예를 들어 ‘승인’로직이 도메인 객체 내부에 존재할 경우, 해당 도메인 객체에 대한 ‘승인을 위한 제약조건’과 ‘승인이 되었을 때 변경되는 값’이 도메인 객체 내부에 포함되어 코드만으로 제약조건과 비지니스 로직을 파악할 수 있음.
따라서 승인에 대한 제약조건이 변경되거나 승인 시 변경될 값이 변경되었을 때도 해당 도메인 객체만 변경하면 되기 때문에 높은 응집도, 높은 유지보수성을 가질 수 있음.
A. 도메인 서비스를 남용할 경우, 도메인 객체는 단순히 데이터를 저장하고 조회하는 Getter / Setter 역할만 수행하고 별다른 정보를 제공할 수 없는 객체가 됨. 이렇게 아무런 정보가 없는 객체를 ‘Anemic Domain Model(빈약한 도메인 모델)’ 이라고 함.
Anemic Domain Model은 ‘데이터와 행위를 함께 모아 놓는다’는 객체 지향 설계의 기본 원칙을 거스른다. 데이터와 행위가 단절되어 로직이 흩어지고, 로직 변경에 대응하는 유연성이 저해되어 변화하지 않고 정체되는 서비스가 될 가능성이 높음.
→ 도메인 서비스에 로직등은 전부 담으려 하지말고 도메인 객체에 로직을 적절히 담아야 한다. 도메인 서비스에 작성할 코드와 도메인 객체에 작성할 코드를 잘 분리해서 작성하자.
Aplication Service에선 도메인 로직을 구현해서는 안된다. 도메인 로직 구현시 코드 중복, 로직 분산등 코드 품질 저하를 야기한다. 서비스 크기가 커진다면 2~3개의 기능으로 나눠서 구현하길 추천한다.
ex) MemberSaveApplication Service와 MemberApproveApplicationService에 중복되는 멤버 조회 메소드가 있다면 MemberServiceHelper에다 구현하자.
또한 Presentation Layer에서 의존하면 안된다. Application Service 다독 테스트가 어려워지고, Presentation Layer의 구현이 변경되면 Application Service도 함께 변경 되어야 한다.
Request, Response Validation도 해주자. 입력값 유효성 검증과 도메인 로직 검증이 이루어지면 코드가 늘어날지라도 완성도는 높아진다.
Specification Pattern이란 객체가 도메인 조건을 만족하는지 확인하는 역할만 수행한다.
도메인 값 변경, 도메인의 값 조회 및 리턴은 Specification의 역할이 아니다. 값 변경 또는 조회는 Application Service에서 수행하자.
→ Specification 객체 내 Repository를 호출하자. 단 I/O가 많아질 경우를 대비하자.
→ first-class collection을 사용하자. 일급 컬렉션이란 클래스 내에 Collection을 제외한 다른 멤버 변수가 없는 객체이다. 이른 다른 글에서 더욱 다뤄보겠습니다.
→ Repository에서 findAll()하고 필터링을 건다. 다만 불필요한 데이터를 전체 조회하게 되는데 이게 맞을까…?
→ 그렇다면 Specification에 조건 메소드를 구현하고 명세를 전달하여 원하는 값만 가져오도록 하자!