책/DDD: 도메인 주도 개발
[도메인 주도 개발] Ch1.도메인 모델 시작하기
금호박
2024. 3. 18. 00:19
도메인
: 소프트웨어로 해결하고자 하는 문제 영역(domain)
- 도메인은 여러 하위 도메인으로 구성된다.
- 예를 들어 온라인 서점 도메인에서,주문 하위 도메인 : 고객의 주문 처리
- 카탈로그 하위 도메인 : 고객에게 구매할 수 있는 상품 목록 제공
- 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다.
- 고객이 물건을 구매하면 주문, 결제, 혜택 하위 도메인의 기능이 엮이게 된다.
- 소프트웨어가 도메인의 모든 기능을 제공하진 않는다.
- 많은 온라인 쇼핑몰이 자체적으로 배송 시스템을 구축하기보다는 외부 배송 업체의 시스템을 사용한다.
- 결제 시스템의 경우에도 마찮가지이다. 직접 구현하는 경우보다 결제 대행업체를 이용해서 처리하는 경우가 많다.
- 도메인마다 고정된 하위 도메인이 존재하는 것은 아니다.
- 어떤 쇼핑몰은 고객에게 다양한 혜택을 제공하지만, 모든 쇼핑몰이 그러한 혜택을 제공하는 것은 아니다.
- 하위 도메인을 어떻게 구성할지 여부는 상황에 따라 달라진다.
도메인 전문가와 개발자 간 지식 공유
Garbage in, Garbage out : 잘못된 요구사항이 들어가면 잘못된 제품이 나온다.
- 도메인 전문가의 요구를 개발자가 제대로 이해하는 것은 매우 중요하다.
- 예를 들어 회계 담당자는 정산 금액 계산을 자동화하는 기능을 개발자에게 요구할 수 있다. 이것을 제대로 이해하지 않고 개발이 이루어지면, 필요없거나 유용함이 떨어지는 시스템을 만들 수 있다. 또는 수정해야 할 코드가 많아져서 일정 등에 문제가 생기기도 한다.
- 요구사항을 제대로 이해하기 위한 방법
- 개발자와 전문가가 직접 대화하기 → 정보의 왜곡, 손실이 줄어든다.
- 개발자도 도메인 지식을 갖추어야 한다.
- 전문가나 관련자가 요구한 내용이 항상 올바른 것은 아니다. 실제 필요한 요구를 표현하지 못하는 경우도 있기 때문에 전문가와의 대화를 통해 진짜 요구사항을 찾아내야 한다.
도메인 모델
: 특정 도메인을 개념적으로 표현한 것.
- 도메인 모델을 표현하는 방법은 다양하다.
- 객체를 이용한 도메인 모델 → 기능과 데이터 함께 확인 가능.
- 상태 다이어그램 도메인 모델
- 도메인 모델은 도메인을 이해하기 위한 모델이기에 도메인 모델을 이용해서 바로 코드를 작성할 수 있는 것은 아니다. → 구현 기술에 맞는 구현 모델이 따로 필요하다.
- 어떤 도메인에 하위 도메인이 있을 경우, 하위 도메인에 대한 도메인 모델은 따로 만들어야 한다.
도메인 모델 패턴
- 사용자 인터페이스(표현) : 사용자의 요청을 처리하고 사용자에게 정보를 보여 준다. 사용자는 소프트웨어를 사용하는 사람일 수도 있고, 외부 시스템일 수도 있다.
- 응용 : 사용자가 요청한 기능을 실행한다. 업무 로직을 집접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
- 도메인 : 시스템이 제공할 도메인 규칙을 구현한다. 주문 도메인의 ‘주문 취소는 배송 전에만 가능’과 같은 규칙을 구현한 코드는 도메인 계층에 위치하게 된다.
- 인프라스트럭처 : 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.
- 이러한 방식의 도메인 모델은 아키텍처 상의 도메인 계층을 객체 지향 기법으로 구현하는 패턴을 말한다.
- 핵심을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.
도메인 모델 추출
- 도메인을 모델링 할 때 요구사항을 분석하여 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾아야 한다.
엔티티와 밸류
도출한 모델은 엔티티 와 밸류 로 구분한다.
엔티티
- 각 엔티티는 엔티티마다의 고유한 식별자를 갖는다.
- 이 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티 간의 식별자가 같으면 두 엔티티는 서로 같다.
- 엔티티를 생성하고 속성을 바꾸고 삭제할 때까지 식별자는 유지된다.
- 엔티티의 식별자 생성 방식 : 생성 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.
- 특정 규칙에 따라 생성(주문번호, 카드번호 등)
- UUID나 Nano ID와 같은 고유 식별자 생성기 사용
- 값을 직접 입력(회원의 이메일, 아이디 등)
- 일련번호 사용(시퀀스나 DB의 자동 증가 칼럼 사용)
밸류 타입
- 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.
- 예를 들어, 이 클래스의 receiverName 필드와 receiverPhoneNumber 필드는 서로 다른 데이터를 담고 있지만 개념적으로는 받는 사람을 의미함. Receiver 라는 도메인 개념을 보다 완전하게 표현할 수 있다.
ShippingInfo를 구성하는 데이터가 무엇인지를 더 쉽게 알 수 있다.public class ShippingInfo{ private Receiver receiver; ... }
public class Reciever{ private String name; private String phoneNumber; public Reciever(String name, String phoneName){ this.name = name; this.phoneNumber = phoneNumber; } public String getName(){ return name; } public String getPhoneNumber(){ return phoneNumber; } }
- 이것을 기존의 ShippingInfo 클래스에 적용하면,
- 이것을 밸류 타입을 사용하면,
- public class ShippingInfo{ /* 받는 사람 */ private String recieverName; private String receiverPhoneNumber; ... }
- 예를 들어, 이 클래스의 receiverName 필드와 receiverPhoneNumber 필드는 서로 다른 데이터를 담고 있지만 개념적으로는 받는 사람을 의미함. Receiver 라는 도메인 개념을 보다 완전하게 표현할 수 있다.
- 밸류 타입이 꼭 두 개 이상의 데이터를 가져야 하는 것은 아니다.
- 밸류 타입을 이용하면 코드의 의미를 더 잘 이해할 수 있다. (코드의 가독성이 향상된다.)
- 두 밸류 객체를 비교할 때는 모든 속성이 같은지 비교한다.
- 밸류 타입을 위한 기능을 추가하는 것도 가능하다.
- 밸류 타입은 일반적으로 불변 타입으로 구현한다.
- 불변 객체는 참조 투명성과 스레드에 안전한 특징을 가지고 있다.
- setter() 을 사용하지 않는다.
- 이렇게 구현하는 가장 큰 이유는 안전한 코드를 작성하기 위해서이다.
- 만약 밸류 타입에 setValue() 를 적용하여 값을 변경할 수 있도록 한다면, 참조 투명성과 같은 문제를 겪을 수 있다.
public class Money{ private int value; public Money add(Money money){ return new Money(this.value + money.value); /* 새로운 Money를 생성함 */ } /* value를 변경할 수 있는 메서드 없음. */ }
엔티티 식별자와 벨류 타입
- 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다.
- 식별자를 위한 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수도 있다.
- 주문번호를 표현하기 위해 Order의 식별자 타입으로 String 대신 OrderNo 밸류를 사용하면 타입을 통해 해당 필드가 주문번호라는 것을 알 수 있다.
도메인 모델에 set 메서드 넣지 않기
- set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
- set 메서드는 필드값만 변경하고 끝나기 때문에 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다.
public class Order{ ... **/* 단순히 배송지 값을 설정한다는 의미만을 전달*/ public void setShippingInfo(Shipping newShipping){ ... }** /* 이렇게 구현할 경우, 배송지 정보를 새로 변경한다는 의미를 가진다. */ public void changeShippingInfo(Shipping new Shipping){ ... } }
- set 메서드를 사용하면, 도메인 객체를 생성할 때 온전하지 않은 상태가 될 수 있다. → 이처럼 객체가 불완전한 상태로 사용되는 것을 막기 위해서는 생성 시점에 필요한 것을 모두 전달해 주어야 한다.
Order order = new Order(orderer, lines, shippingInfo, OrderState.PREPARING)
- : 생성자를 통해 필요한 데이터를 모두 받아야 한다.
- 예를 들어,이 경우, Order 를 처음 생성하는 시점에 order는 불완전하다. 또 위의 코드에서는 주문자 설정이 누락되어있다. 그럼에도 setState()를 호출하여 상품 준비 중 상태로 바꾸었다. 즉, 객체가 불완전한 상태에서 사용되고 있다.
Order order = new Order(); order.setOrderLine(lines); order.setShippingInfo(shippingInfo); order.setState(OrderState.PREPARING);
도메인 용어와 유비쿼터스 언어
- 클래스, 필드, 메서드 등의 이름을 작성할 때 알맞은 용어를 선택하기 위해 노력해야 한다~
원본 노션 링크 : https://www.notion.so/Ch1-b69f903b4dce4477af8dbefee288a9b4