Hyun's Wonderwall

[EFUB 4기 BE Lead] 도메인 주도 개발 시작하기 - 1. 도메인 모델 시작하기 본문

Study/Java, Spring

[EFUB 4기 BE Lead] 도메인 주도 개발 시작하기 - 1. 도메인 모델 시작하기

Hyun_! 2024. 3. 10. 20:58

EFUB 4기 BackEnd Lead_ 도메인 주도 개발 스터디

  • 스터디 커리큘럼: 최범균, "도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지"
  • 1주차 과제: Chapter 1. 도메인 모델 시작하기, Chapter 2. 아키텍처 개요

Chapter 1. 도메인 모델 시작하기

  • Keywords: 도메인, 도메인 모델, 엔티티와 밸류, 도메인 용어

1.1 도메인이란?

도메인: 소프트웨어로 해결하고자 하는 문제 영역. (ex. 온라인 서점 - 온라인 서점 소프트웨어를 구현해야 함)

- 한 도메인은 여러 하위 도메인으로 구성된다. (ex. 주문, 카탈로그, 회원, 혜택, 결제, 배송, 리뷰, 정산 등)

- 하위 도메인 구성은 상황에 따라 달라진다. (ex. 기업 고객 대상 업체, 일반 고객 대상 업체)

- 한 하위 도메인은 다른 하위 도메인과 연동하여 완전한 기능을 제공한다. (ex. 고객이 물건을 구매하면 주문, 결제, 해송, 혜택 하위 도메인의 기능이 엮이게 됨)

- 특정 도메인을 위한 소프트웨어라고 해서 도메인이 제공해야 할 모든 기능을 직접 구현하는 것은 아니다. (ex. 결제 시스템 직접 구현하기보다 결제 대행업체를 이용해 처리하는 경우가 많음)

 

1.2 도메인 전문가와 개발자 간 지식 공유

도메인 전문가: 각 도메인 영역의 전문가. 해당 도메인의 지식과 경험을 바탕으로 기능 개발을 요구한다. (ex. 홍보, 회계) 

- 개발자는 그들의 요구사항을 분석하고 설계하여 코딩하는데, 코딩에 앞서 요구사항을 올바르게 이해하는 것이 중요하다. 요구사항을 제대로 이해해야 쓸모있고 유용한 시스템을 만들 수 있다.

- 요구사항을 올바르게 이해하기 위해서는 개발자와 전문가가 직접 대화하는 것이 가장 좋다.

- 이해관계자와 개발자도 도메인 지식을 갖춰야 한다. 제품 개발과 관련된 도메인 전문가, 관계자, 개발자가 같은 지식을 공유하고 직접 소통할수록 도메인 전문가가 원하는 제품을 만들 가능성이 높아진다.

"Garbage in, Garbage out" - 잘못된 값이 들어가면 잘못된 결과가 나온다. 잘못된 요구사항이 들어가면 잘못된 제품이 나온다. 전문가가 요구한 내용이 항상 올바른 것은 아니며, 때로는 본인들이 실제로 원하는 것을 정확하게 표현하지 못할 때도 있다. 그래서 개발자는 요구사항을 이해할 때 왜 이런 기능을 요구하는지 또는 실제로 원하는 게 무엇인지 생각하고, 전문가와 대화를 통해 진짜로 원하는 것을 찾아야 한다.

 

1.3 도메인 모델

도메인 모델: 특정 도메인을 개념적으로 표현한 것. 도메인 자체를 이해하기 위한 개념 모델이다.

도메인 모델을 사용하면 여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 된다.

*예시: 주문 도메인(온라인 쇼핑몰)을 객체 모델로 표현한 다이어그램

- 주문 모델 : 상품. 배송지. 가격. 총 지불 금액. 결제 수단. 배송 주소. 주문 취소 등.
주문에 주문번호와 총금액이 있고, 배송정보를 변경 가능함. 주문을 취소할 수도 있음

 

도메인 모델을 모델링하는 방법:

(1) 객체 모델 다이어그램 - 객체 지향 언어로 모델에 가깝게 구현 가능

(2) 상태 다이어그램 - 주문의 상태 전이를 모델링 가능

(3) 그래프 - 계산 규칙이 중요한 경우

 

- 도메인 모델은 구현 모델이 아니다. 구현 기술에 맞는 구현 모델이 따로 필요하다. 

- 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안 된다. 각 하위 도메인마다 별도로 모델을 만들어야 한다.

 

1.4 도메인 모델 패턴

일반적인 애플리케이션의 아키텍처는 4개 영역으로 구성된다.

(위에서 아래 방향으로) 표현 -> 응용 -> 도메인 -> 인프라스트럭처 (->DB)

 

아키텍처 구성 (각 영역과 역할)

영역 설명
사용자 인터페이스(UI) 또는 표현 사용자의 요청을 처리하고 사용자에게 정보를 보여준다. 여기서 사용자는 소프트웨어를 사용하는 사람뿐만 아니라 외부 시스템일 수도 있다.
응용(Application) 사용자가 요청한 기능을 실행한다. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능을 실행한다.
도메인(Domain) 시스템이 제공할 도메인 규칙을 구현한다.
인프라스트럭쳐(Infrastructure) 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동을 처리한다.

 

이제 살펴볼 도메인 모델 = 도메인 모델 패턴: 아키텍처 상의 도메인 계층을 객체 지향 기법으로 구현하는 패턴.

- 도메인 계층은 도메인의 핵심 규칙을 구현한다.

(ex. 주문 도메인의 경우 '출고 전에 배송지를 변경할 수 있다'는 규칙과 '주문 취소는 배송 전에만 할 수 있다'는 규칙을 구현한 코드가 도메인 계층에 위치하게 된다.)

- 이런 도메인 규칙을 객체 지향 기법으로 구현하는 패턴이 도메인 모델 패턴이다.

- 핵심 규칙을 구현한 코드는 도메인 모델에만 위치하기 때문에 규칙이 바뀌거나 규칙을 확장해야 할 때 다른 코드에 영향을 덜 주고 변경 내역을 모델에 반영할 수 있게 된다.

(ex. Order 클래스와 OrderState 이넘을 사용해 배송지 변경이 가능한지를 Order 클래스에서 판단하도록 함)

(도메인 모델이라는 용어는 도메인 자체를 표현하는 개념적인 모델을 의미하지만, 도메인 계층을 구현하는 객체 모델을 언급할 때에도 도메인 모델이란 용어를 사용한다.)

 

1.5 도메인 모델 도출

개발을 하려면 요구사항과 관련자와의 대화를 통해 도메인을 이해하고, 도메인 모델 초안을 만들어야 비로소 코드를 작성할 수 있다. 구현을 시작하려면 도메인에 대한 초기 모델이 필요하다.

도메인을 모델링할 때 기본이 되는 작업은 모델을 구성하는 핵심 구성요소, 규칙, 기능을 찾는 것이다.

이 과정은 요구사항에서 출발한다.

*예시: 주문 도메인과 관련된 요구사항을 보자.

요구사항 목록:
- 최소 한 종류 이상의 상품을 주문해야 한다.
- 한 상품을 한 개 이상 주문할 수 있다.
- 총 주문 금액은 각 상품의 구매 가격 합을 모두 더한 금액이다. (각 상품의 구매 가격 합 = 상품 가격 * 구매 개수)
- 주문할 때 배송지 정보를 반드시 지정해야 한다. (배송지 정보: 받는 사람 이름, 전화번호, 주소)
- 출고를 하면 배송지를 변경할 수 없고, 주문을 취소할 수 없다.
- 고객이 결제를 완료하지 전에는 상품을 준비하지 않는다.

+ 필요에 따라 생성자, getter

각 주문 항목을 표현하는 OrderLine 클래스
- 필드: 주문할 상품, 가격, 구매 개수, 각 구매 항목의 구매 가격 - Product product, int price, int quantity, int amounts
- 메소드: calculateAmounts(), getAmounts()

주문 도메인을 구현하는 Order 클래스
- 필드: List<OrderLine> orderLines, Money totalAmounts, ShippingInfo shippingInfo
- 필드 추가에 따른 메소드: setOrderLines(), verifyAtLeastOneOrMoreOrderLines(), calculateTotalAmounts(), setShippingInfo()
- 요구사항 요구 기능들 메소드: (1) 출고 상태로 변경, (2) 배송지 정보 변경, (3) 주문 취소 (4) 결제 완료 - changeShipped(), changeShippingInfo(), cancel(), completePayment()

배송지 정보가 담긴 ShippingInfo 클래스
- 필드: String receiverName,String receiverPhoneNumber, String shippingAddress1, String shippingAddress2, String shippingZipcode
- 메소드: 생성자, getter

배송 상태를 나타내는 이넘 클래스 OrderState
- PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED, CANCELED

Order 클래스에 추가
- OrderState state 필드 추가
- verifyNotYetShipped()로 출고여부 확인 메소드 추가

 

1.6 엔티티와 밸류

도출한 모델은 크게 엔티티와 밸류로 구분할 수 있다. 엔티티와 밸류를 제대로 구분해야 한다.

 

1.6.1 엔티티

엔티티(Entity): 식별자를 가진다. 식별자는 엔티티 객체마다 고유하다. (ex. 주문 도메인에서 주문 엔티티의 '주문번호')

- 주문 도메인 모델에서 Order가 엔티티이며 주문번호를 식별자로 갖게 된다.

- 엔티티의 식별자는 바뀌지 않고 고유하기 때문에 두 엔티티 객체의 식별자가 같으면 두 엔티티가 같다고 판단할 수 있다. - 엔티티를 구현한 클래스는 식별자를 이용해서 equals() 메서드와 hashCode() 메서드를 구현할 수 있다.

 

1.6.2 엔티티의 식별자 생성

엔티티의 식별자를 생성하는 시점은 도메인의 특징과 사용하는 기술에 따라 달라진다.

*식별자 생성 방식: (1) 특정 규칙에 따라 생성 (2) UUID나 Nano ID와 같은 고유 식별자 생성기 사용 (3) 값을 직접 입력 (4) 일련번호 사용 (시퀀스나 DB의 자동 증가 칼럼 사용)

 

(1) 특정 규칙에 따라 생성: 주문번호, 운송장번호, 카드번호와 같은 식별자

- 흔히 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것. (주의점: 같은 시간 동시에 식별자를 생성해도 같은 식별자가 만들어지면 안 됨)

(2) UUID(universally unique identifier)를 이용해 식별자 생성: java.util.UUID 클래스를 이용해 생성하고 toString()

(3) 값을 직접 입력: 회원의 아이디, 이메일 등. 식별자를 중복해서 입력하지 않도록 사전에 방지하는 것이 중요.

(4) 일련번호를 식별자로 사용: 일련번호 방식은 주로 데이터베이스가 제공하는 자동 증가 기능을 사용한다.

- MySQL의 경우 자동 증가 칼럼을 이용해서 일련번호 식별자를 생성한다.

- 자동 증가 칼럼은 db 테이블에 데이터를 삽입해야 비로소 값을 알 수 있기 때문에 테이블에 데이터를 추가하기 전에는 식별자를 알거나 참조할 수 없다. 이것은 엔티티 객체를 생성할 때 식별자를 전달할 수 없음을 의미한다.

- 자동 증가 칼럼을 제외한 다른 방식은 식별자를 먼저 만들고 엔티티 객체를 생성할 때 식별자를 전달할 수도 있다.

- 리포지터리는 도메인 객체를 데이터베이스에 저장할 때 사용하는 구성요소이다. 자동 증가 칼럼을 사용할 경우 리포지터리는 db가 생성한 식별자를 구해서 엔티티 객체에 반영한다.

 

1.6.3 밸류 타입

앞에서 작성한 ShippingInfo 클래스를 보자. 필드에서 받는 사람과 주소에 대한 데이터를 가지고 있다.

- receiverName, receiverPhoneNumber: 두 필드는 '받는 사람'이라는 하나의 개념을 표현하고 있다.

- shippingAddress1과 2, shippingZipcode: '주소'라는 하나의 개념을 표현한다.

 

밸류(Value) 타입은 이렇게 개념적으로 완전한 하나를 표현할 때 사용한다. ('받는 사람', '주소'와 같이) 

- 받는 사람을 표현하는 Receiver 밸류 타입을 작성 (필드: name, phoneNumber)

  (public class Receiver {...} 클래스 작성 방식은 엔티티와 차이 없는 것 같다)

- 주소 데이터를 담는 Address 밸류 타입을 작성 (필드: address1, address2, zipcode)

- 이 두 밸류 타입을 이용해 ShippingInfo 클래스를 다시 구현하니 간결해졌다. (필드: Receiver receiver, Address address)

 

밸류 타입의 장점

(1) 코드 가독성 향상. 의미 명확하게 표현 가능.

- 꼭 필드로 두 개 이상의 데이터를 가져야 하는 것은 아님.

- OrderLine에서 price와 amounts가 모두 돈을 나타내는 필드이므로, Money 타입을 만들어 사용 가능. (필드: int value)

(2) 밸류 타입을 위한 기능 추가할 수 있음.

- 밸류 객체의 데이터를 변경할 때는, 기존 데이터를 변경하기보다는 변경한 데이터를 갖는 새로운 밸류 객체를 생성하는 방식을 선호한다.

- Money 타입에 add, multiply 등 '돈 계산' 기능 추가. 메소드를 Money 자료형으로 하여 new Money를 반환하도록 했다.

 

데이터 변경 기능을 제공하지 않는 타입을 불변(immutable)이라고 한다.

- 밸류 타입을 불변으로 구현하는데, 가장 중요한 이유는 코드 안전이다.

- 불변 객체는 참조 투명성스레드에 안전한 특징을 가진다.

- Money가 불변 객체가 아니라면, 참조 투명성과 관련된 문제가 발생할 수 있다. 따라서 필드로 객체를 받을 때 파라미터의 데이터를 복사한 새로운 객체를 생성해야 한다. (ex. OrderLine이 price 필드로 참조하는 Money 타입 price 변수의 값이 외부에서 setValue()로 변경된다면 의도치 않게 가격이 바뀔 수 있음. // OrderLine의 생성자에서 this.price = new Money(price.getValue()); 해야 함)

- Money가 불변이면 이런 코드를 작성할 필요 없이 파라미터로 전달받은 price를 안전하게 사용할 수 있다. 

 

두 밸류 객체를 비교할 때는 모든 속성이 같은지 비교한다.

 

1.6.4 엔티티 식별자와 밸류 타입

엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다.

이런 식별자는 단순한 문자열이 아니라 도메인에서 특별한 의미를 지니는 경우가 많기 때문에, 식별자를 위한 밸류 타입을 사용해 의미가 잘 드러나도록 할 수 있다.

- (ex. 신용카드 번호도 16개의 숫자로 구성된 문자열이며, 많은 온라인 서비스에서 회원을 구분할 때 사용하는 이메일 주소도 문자열이다. 예시에서 Money 타입을 사용함으로써 도메인의 '돈'을 의미하는 것처럼.)

- (ex. 주문번호를 표현하기 위해 Order의 식별자 타입으로 String 대신 OrderNo 밸류 타입을 사용하면, 타입을 통해 해당 필드가 주문번호라는 것을 알 수 있다. // OrderNo 대신에 String 타입을 사용한다면 'id'라는 이름만으로는 해당 필드가 주문번호인지를 알 수 없다. 필드의 의미가 드러나도록 하려면 id라는 필드 이름 대신 orderNo라는 필드 이름을 사용해야 한다. // 반면에 식별자를 위해 OrderNo 타입을 만들면, 타입 자체로 주문번호라는 것을 알 수 있으므로 필드 이름이 id여도 실제 이름을 찾는 것이 어렵지 않다.)

 

1.6.5 도메인 모델에 set 메서드 넣지 않기

도메인 모델에 get, set 메서드를 무조건 추가하는 것은 좋지 않다. 특히 set 메서드는 도메인의 핵심 개념과 의도를 코드에서 사라지게 할 수 있다.

 

set 메서드를 지양해야 하는 이유

  • 정확히 어떤 기능을 하는 메소드인지 알기 어려움 : changeShippingInfo 메서드가 배송지 정보를 새로 변경한다는 의미를 가졌다면 setShippingInfo 메서드는 단순히 배송지 값을 새로 설정한다는 것을 의미한다. ('배송지 변경'이라는 기능이 코드에서 사라짐.) completePayment는 결제를 완료했다는 의미를 갖는 반면 setOrderState()는 단순히 주문 상태 값을 설정한다는 것을 의미한다. ('결제 완료'라는 기능이 코드에서 사라짐)
  • 코드로 어디까지 구현할지 애매함 : completePayment는 결제 완료 처리 코드를 구현하니까 결제 완료와 관련된 도메인 지식을 코드로 구현하는 것이 자연스럽다. 하지만 setOrderState()는 단순히 상태 값만 변경할지 아니면 상태값에 따라 다른 처리를 위한 코드를 함께 구현할지 애매하다. set 메서드는 필드값만 변경하고 끝나기 때문에 상태 변경과 관련된 도메인 지식이 코드에서 사라지게 된다.
  • 객체가 불완전할 수 있는 문제 : 빈 객체를 먼저 생성한 후 set 메서드로 필드 값을 지정한다면 도메인 객체가 온전하지 않은 상태가 될 수 있다. 주문자 설정을 누락하고 주문 완료 처리를 하게 되어버릴 수 있고, 주문이 정상인지 확인하는 코드를 어디 위치시킬지도 애매하다.

도메인 객체가 불완전한 상태로 사용되는 것을 막는 방법: 생성 시점에 생성자를 통해 필요한 데이터를 모두 받도록 한다.
- 생성자를 호출하는 시점에 필요한 데이터가 올바른지 검사할 수 있다.

- set 메소드를 사용했지만, private라 외부에서 데이터 변경이 불가. (불변 밸류 타입이라면 set 메서드 구현 x)

set 메서드를 구현해야 할 특별한 이유가 없다면 불변 타입의 장점을 잘 살릴 수 있도록, 밸류 타입은 불변으로 구현한다.

 

1.7 도메인 용어와 유비쿼터스 언어

- enum OrderState: 도메인 용어를 사용해서 구현하면 직관적이게 된다. (PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED)

- 유비쿼터스 언어: 전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 이를 대화, 문서, 도메인 모델, 코드, 테스트 등 모든 곳에서 같은 용어를 사용한다. 이렇게 하면 소통 과정에서 발생하는 용어의 모호함을 줄일 수 있고 개발자는 도메인과 코드 사이에서 불필요한 해석 과정을 줄일 수 있다.

- 이름을 잘 짓자.


2장 정리

https://hereishyun.tistory.com/110

 

[EFUB 4기 BE Lead] 도메인 주도 개발 시작하기 - 2. 아키텍처 개요

EFUB 4기 BackEnd Lead_ 도메인 주도 개발 스터디 스터디 커리큘럼: 최범균, "도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지" 1주차 과제: Chapter 1. 도메인 모델 시작하기, Chapter 2. 아키텍

hereishyun.tistory.com