코딩은 마라톤

[도메인 주도 개발 시작하기] 책 선정 이유와 Ch1. 도메인 모델 시작하기 본문

Backend

[도메인 주도 개발 시작하기] 책 선정 이유와 Ch1. 도메인 모델 시작하기

anxi 2024. 9. 10. 14:58

책 선정

도메인 주도 개발 시작하기_최범균

개발 방법론은 여러 개있는 걸로 알고 있다.

예를 들면, 테스트 주도 개발(TDD), 도메인 주도 개발(DDD), 리드미 주도 개발(?) 등등

TDD나 DDD를 공부해서 동아리 프로젝트때 도입시키고 싶은 욕구? 욕망? 이 컸다.

 

TDD와 DDD 중에 DDD를 선택한 이유는 TDD보다 쉽지 않을까..? 라는 생각이 가장 컸다.

TDD는 아무래도 테스트 코드 작성하는 과정이 좀 복잡할 거라고 생각했다. (물론 DDD 공부 후에 할거지만,,)

그리고 1달 뒤에 시작하는 프로젝트에서 도입하려면 TDD보다는 DDD가 더 낫지 않을까 생각했다. (프로젝트에 도입할 수 있을까,,,)

또 다른 이유로는 전 회사에서 DDD로 프로젝트를 진행하셨던 거 같은데 이미 프로젝트가 완성된 상황에서 입사한 상태였어서 패키지 구조나 밸류 타입 등 뭘 의미하는지 잘 몰랐다. 도메인 로직은 뭐고,, 서비스 계층에서 기능을 만드는 것이 아닌 도메인의 기능을 가져다 와서 쓰는 것 자체가 좀 신기했다. 근데 아마 이게 DDD로 인한 산출물이라 생각했어서 TDD보다는 DDD에 혹했다. (그래도 회사에서 잠깐 접했으니,,)

 

그래서 책을 찾던 도중, 최범균님의 "도메인 주도 개발 시작하기" 책이 눈에 띄었고, 스프링5 프로그래밍 입문을 읽지는 않았지만 이 책도 읽을 생각이라 먼저 DDD를 통해 최범균님의 인사이트를 접해보자! 라는 생각으로 책을 선정했다.


Ch1. 도메인 모델 시작하기

도메인이란?

온라인 서점 : 소프트웨어 대상

온라인으로 책을 판매하는데 필요한 상품 조회, 구매, 결제, 배송 추적 등의 기능

즉, 소프트웨어로 해결하고자 하는 문제 영역을 도메인이라 한다.

 

코딩에 앞서 요구사항을 올바르게 이해하는 것이 중요하다.

→ 개발자와 전문가가 직접 대화하는 것이 가장 간단한 방법

→ 개발자도 도메인 지식을 갖춰야 한다

  • 도메인은 다수의 하위 도메인으로 구성된다.

 도메인모델

특정 도메인을 개념적으로 표현한 것(이해하기 위한 개념 모델)

객체 기반 주문 도메인 모델
상태 다이어그램을 이용한 주문 상태 모델링

도메인모델의 표현 방식은 도메인을 이해만 할 수 있다면 어떤 방식이든 중요하지 않다.

도메인 모델 패턴

위에서 말하는 도메인 모델(도메인 이해에 필요한 개념 모델)이 아닌 마틴 파울러가 말한 “아키텍처 상의 도메인 계층을 객체 지향 기법으로 구현하는 패턴” 을 의미한다.

 

일반적인 애플리케이션 아키텍처

아키텍처 구성

  • 도메인 계층은 도메인의 핵심 규칙을 구현한다.
    • 주문 도메인의 경우 ‘출고 전에 배송지를 변경할 수 있다’, ‘주문 취소는 배송 전에만 할 수 있다’ 라는 규칙을 구현한 코드가 도메인 계층에 위치
    • 즉, Order 클래스에서 배송지 변경, 주문 취소 등의 로직이 존재해야한다.

도메인 모델 도출

도메인 모델링할 때 기본이 되는 작업은 모델을 구성하는 `핵심 구성요소`, `규칙`, `기능`을 찾는 것이다. 이 과정은 요구사항에서 출발한다.

 

요구사항을 가지고 도메인 모델을 점진적으로 만들어 나간다

도출 과정 : 요구사항 분석 → 도메인에 맞는 핵심 구성요소, 규칙, 기능, 제약조건 등 추출 → 요추출한 정보를 가지고 코드 구현


엔티티와 밸류

위에서 도출한 모델은 엔티티와 밸류로 구분할 수 있다.

엔티티와 밸류

엔티티와 밸류를 제대로 구분해야 도메인을 올바르게 설계하고 구현할 수 있다.

엔티티

  • 엔티티의 가장 큰 특징은 식별자를 가진다는 것이다.
    • 엔티티 객체마다 고유해서 서로 다른 식별자를 갖는다.
    • Order는 엔티티로써 주문번호를 속성(식별자)으로 갖는다.

 

  • 엔티티의 식별자 생성 (4가지 방식)
    • 특정 규칙에 따라 생성
      • 흔히 사용하는 규칙은 현재 시간과 다른 값을 함께 조합하는 것이다.
    • UUID, Nano ID와 같은 고유 식별자 생성기 사용
    • 값을 직접 입력
      • 회원의 아이디나 이메일과 같은 식별자는 값을 직접 입력한다.
    • 일련번호 사용 (Auto Increment)
      • 데이터베이스에 저장 후 엔티티 객체에 식별자가 반영된다.

밸류 타입

public class ShippingInfo {
	
	// 받는 사람
	private String receiverName;
	private String receiverPhoneNumber;
	
	// 주소
	private String shippingAddress1;
	private String shippingAddress2;
	private String shippingZipcode;
 }

 

  • receiverName, receiverPhoneNumber는 다른 데이터를 담고 있지만 개념적으로 받는 사람을 의미한다.
    • 즉 두 필드는 실제로 하나의 개념을 표현한다.

즉, 밸류 타입은 개념적으로 완전한 하나를 표현할 때 사용한다.

 

Receiver는 ‘받는 사람’ 이라는 도메인 개념을 표현한다.

Address는 ‘주소’ 라는 도메인 개념을 표현한다.

따라서 위의 코드를 아래와 같이 변경할 수 있다.

 

public class Receiver {
	private String name;
	private String phoneNumber;
	...
}

public class Address {
	private String address1;
	private String address2;
	private String zipcode;
	...
}

public class ShippingInfo {
	
	private Receiver receiver;
	private Address address;
	...
}

 

  • 밸류 타입의 또 다른 장점은 밸류 타입을 위한 기능을 추가할 수 있다.
    • Money 타입은 돈 계산을 위한 기능을 추가할 수 있다.

 

  • 밸류 객체의 데이터를 변경할 때 밸류 객체의 기존 데이터를 변경하기 보다 변경한 데이터를 생성자를 통해 새롭게 객체를 생성하는 방식을 선호한다.
public class Money {
	private int value;

    // 기존 데이터 변경
    public Money add(Money money) {
	  return this.value + money.value;

    // 새로운 밸류 객체 생성
    public Money add(Money money) {
	  return new Money(this.value + money.value);
    ..
}

 

위처럼 기존 데이터를 변경하지 않고 새로운 밸류 객체를 생성하게 되면, 즉 데이터 변경 기능을 제공하지 않으면 불변으로 구현할 수 있다. → 안정성 보장


엔티티 식별자와 밸류 타입

  • 식별자를 밸류 타입으로 설정하자
    • 엔티티 식별자의 실제 데이터는 String과 같은 문자열로 구성된 경우가 많다.
    • 하지만 식별자의 경우 단순한 문자열이 아닌 도메인에서 특별한 의미를 지니는 경우가 많기 때문에 식별자를 위한 밸류 타입을 사용해서 의미가 잘 드러나도록 할 수 있다.
    • 예를 들어, 주문 번호를 표현하기 위해 Order의 식별자를 String이 아닌 OrderNo 밸류 타입을 사용하면 타입을 통해 주문 번호임을 알 수 있다.
public class Order {
	
	// String일 경우
	private String id; 
	// 또는 private String orderNo;
	
	// 밸류 타입을 사용할 경우
	private OrderNo id;
	...
}
  • 도메인 모델에 set 메서드 넣지 않기
    • get/set 메서드를 무조건 추가하는 건 좋지 않은 버릇이다.
    • set 메서드는 도메인의 핵심 개념이나 의도를 코드에서 사라지게 한다.
public class Order {
	
	// setter 사용할 경우
	public void setShippingInfo(ShippingInfo new Shipping) { .. }
	public void setOrderState(OrderState state) { .. }
	
	// 사용하지 않을 경우
	public void changeShippingInfo(ShippingInfo new Shipping) { .. }
	public void completePayment(OrderState state) { .. }
}
  • setter를 사용하면 도메인 지식을 코드로 구현하는데 부자연스럽다. 반대로 말하면 setter를 사용하지 않으면 도메인 지식을 코드로 구현하는 것이 자연스럽다.
  • 또한 도메인 객체 생성 시점에서 setter를 사용해 전달 시 불완전한 상태로 사용될 수 있어 생성 시점에 필요한 것을 전달해주는 것이 좋다. (생성자를 통한 데이터 추가)

set 메서드는 지양하자. 만약 사용할 거면 접근 범위를 private로 하자 클래스 내부에서 데이터를 변경할 목적으로만 사용하자

불변 밸류 타입을 사용하면 밸류 타입에는 set 메서드를 구현하지 말자