Refactoring To DDD

이완희·2023년 4월 16일
0

Refactoring To DDD

0. 개요

도메인 주도 설계(DDD)는 사용자를 위한 프로젝트 설계 방법 중 하나입니다.
도메인을 중심으로 설계를 하며 개발자는 반드시 도메인에 대한 이해가 필요하다.
설계 단계에서 부터 참여하여 비지니스와 꾸준히 소통하는 Agile방법론에 적합합니다.

1. 도메인이란?

도메인이란 프로그램이 쓰이는 대상분야이다. 쇼핑몰로 예시를 들자면 장바구니 담기, 상품 발송 등등이 도메인이 될 수 있다. 도메인 객체가 도메인 모델을 충분히 반영하고 있다면 도메인의 변화를 보다 코드로 옮기기 쉬워진다.

“운전기사가 차량을 운행하여 물건을 실어나른다” 라는 도메인을 모델링 하면 운전기사, 운행, 차량정보와 같은 도메인 객체가 생성된다.

DDD를 왜 써야 할까? 개발자에게 보다 프로젝트를 이해하기 쉽게하여 코드를 손쉽게 구성하기 위해서다. 특정 행위를 한다면 엔티티가 반드시 있어야 할테고 이를 기반으로 DB에 엔티티를 생성할 수 있다. DB설계가 되어있어야 개발이 가능한데 DB가 왜이렇게 설계되었는지 비지니스와 소통으로 알수있다. 즉, 소프트웨어의 유연성 확보와 비지니스 변화에 빠르게 대응할 수 있게 된다.

도메인 모델 패턴은 아래와 같다.

  • Presentation: 사용자의 요청 처리, 정보를 보여주는 계층. Controller레벨
  • Application: 사용자의 요청을 처리하는 계층. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합한다. Service 레벨
  • Domain: 시스템이 구현하는 도메인 규칙을 구현. Service레벨
  • Intrastructure: DB나 메세징같은 외부 시스템과의 연동처리. Repository레벨

도메인 모델은 Entity와 Value Object로 분류 할 수 있다.

EntityVODTO
정의식별자를 가지는
도메인을 표현한 객체
도메인을 표현한 값 표현용 객체Layer간 데이터 전송용 객체
상태 변경 여부상태를 변경할 수 없는 가변성상태를 변경할 수 없는 불변성.
값을 변경하려면 새 객체 생성 필요
가변 또는 불변 객체
동등 값 비교 기준두 객체의 식별자가 같다면
같은 객체로 간주
같은 값의 객체가 존재한다면
두 객체는 같은 객체로 간주
-
로직 포함 여부로직 포함 가능로직 포함 가능
제약 조건을 걸거나 가능한/가능하지
않은 행위 구현
로직 포함할 수 없음

즉, 식별자를 갖고있냐 아니냐에 따라 Entity, VO로 나눌 수 있으며 Entity는 DB를 생성하는 컬럼들의 집합이다.

Aggregate는 데이터의 변경을 다루는 Entity와 VO의 군집이다. 도메인이 커지며 Entity와 VO가 많아지며 모델이 복잡해지는데, 상위 수준에서 모델을 식별할 수 있어 도메인 모델에 대한 이해를 돕는다.

Aggregate Root는 전체를 관리할 주체이다. Aggregate 구성 항목의 일관성이 깨지지 않도록 한다.

즉, Aggregate 외부에서 Aggregate 내부 객체를 직접 변경하면 안되고 Root Entity를 통해서 직접 접근해야 한다.

  • 주문 Aggregate: 주문, 주문자, 주문 항목, 수령인, 배송지, 배송 추적 정보
  • 결제 Aggregate: 결제정보, 환불정보

이 예시에서 주문자(Aggregate 내부 객체)를 변경하려면 주문(Root Entiy)을 통해서 직접 변경해야 한다.

Aggregate Best Practice

Set Method 사용하지 않기

  • Set Method로 도메인 객체의 값을 변경할 경우, 어떤 기준으로 값을 변경하는지(도메인 규칙, 비지니스 로직)가 도메인 외부에 구현되어, ‘객체는 자기자신에 대해 책임진다’는 객체 지향 원칙에 어긋난다.
  • 객체를 최초 생성할 때는 반드시 생성자를 사용하고, 객체의 값이 변경되는 행위는 도메인 객체 내부의 메소드에 의해 이루어져야 한다.

Value Object는 불변으로 구현하기

  • Aggregate root에서 조회한 Value Object를 외부에서 변경할 수 없도록 구현하여 도메인 객체 오염 방지
  • 도메인 규칙과 로직 기반으로 구현된 Aggregate root를 통해서만 변경 가능하도록 구현

Entity & Value Object Best Practice

응집도 향상을 위해 Entity 식별자, 값을 Value Object 사용하기

  • Entity의 식별자로 VO를 사용하면, 해당 식별자가 특정 도메인의 식별자임을 쉽게 알아볼 수 있음

2. 도메인 서비스

언제 도메인 서비스를 사용해야 하는가?

  • 특정 엔티티에 속하지 않은 도메인 로직 제공
  • 도메인 객체로 구현하기 어색한 행동을 구현

Bad Practice

RouteChargeCalcService는 운행결과 승인 및 운임 조정율 변경, 운임을 계산하기 위한 서비스로써 도메인 서비스에 구현되어있다.

retrieveAdjustRatio는 RoutCharge 도메인 내에서도 구현이 충분히 가능하므로 도메인 내부에 구현되어야 한다.

searchTemplateDetail은 단순 조회임에도 불구하고 도메인 레벨에 생성되었다. 이는 애플리케이션 레벨에 구현되어야 한다.

3. 왜 로직을 도메인 서비스에 담아야 하는가?

Q. 왜 도메인 객체에 도메인 로직을 작성해야 할까?

A. 도메인에 대한 자기 서술적인 코드가 작성될 수 있음. 예를 들어 ‘승인’로직이 도메인 객체 내부에 존재할 경우, 해당 도메인 객체에 대한 ‘승인을 위한 제약조건’과 ‘승인이 되었을 때 변경되는 값’이 도메인 객체 내부에 포함되어 코드만으로 제약조건과 비지니스 로직을 파악할 수 있음.

따라서 승인에 대한 제약조건이 변경되거나 승인 시 변경될 값이 변경되었을 때도 해당 도메인 객체만 변경하면 되기 때문에 높은 응집도, 높은 유지보수성을 가질 수 있음.

Q. 도메인 서비스에 도메인 로직을 전부 작성하면 안되는 걸까?

A. 도메인 서비스를 남용할 경우, 도메인 객체는 단순히 데이터를 저장하고 조회하는 Getter / Setter 역할만 수행하고 별다른 정보를 제공할 수 없는 객체가 됨. 이렇게 아무런 정보가 없는 객체를 ‘Anemic Domain Model(빈약한 도메인 모델)’ 이라고 함.

Anemic Domain Model은 ‘데이터와 행위를 함께 모아 놓는다’는 객체 지향 설계의 기본 원칙을 거스른다. 데이터와 행위가 단절되어 로직이 흩어지고, 로직 변경에 대응하는 유연성이 저해되어 변화하지 않고 정체되는 서비스가 될 가능성이 높음.

→ 도메인 서비스에 로직등은 전부 담으려 하지말고 도메인 객체에 로직을 적절히 담아야 한다. 도메인 서비스에 작성할 코드와 도메인 객체에 작성할 코드를 잘 분리해서 작성하자.

4. 애플리케이션 서비스

  • Application Servie는 Presentation Layer와 Domain Layer를 연결
  • 구현하는 메소드는 Application Use Case구현과 일치하며, 필요한 도메인 로직을 호출하여 구현
  • 도메인을 호출(생성)하고, 도메인 객체에 구현된 메소드를 실행하고, 결과를 리턴하는 역할을 수행

Aplication Service에선 도메인 로직을 구현해서는 안된다. 도메인 로직 구현시 코드 중복, 로직 분산등 코드 품질 저하를 야기한다. 서비스 크기가 커진다면 2~3개의 기능으로 나눠서 구현하길 추천한다.

ex) MemberSaveApplication Service와 MemberApproveApplicationService에 중복되는 멤버 조회 메소드가 있다면 MemberServiceHelper에다 구현하자.

또한 Presentation Layer에서 의존하면 안된다. Application Service 다독 테스트가 어려워지고, Presentation Layer의 구현이 변경되면 Application Service도 함께 변경 되어야 한다.

Request, Response Validation도 해주자. 입력값 유효성 검증과 도메인 로직 검증이 이루어지면 코드가 늘어날지라도 완성도는 높아진다.

5. Specification Pattern

Specification Pattern이란 객체가 도메인 조건을 만족하는지 확인하는 역할만 수행한다.

도메인 값 변경, 도메인의 값 조회 및 리턴은 Specification의 역할이 아니다. 값 변경 또는 조회는 Application Service에서 수행하자.

Bad Practice

  • 객체 내에 타 Repository 주입하여 구현
  • Application Service에 도메인 조건이 노출

→ Specification 객체 내 Repository를 호출하자. 단 I/O가 많아질 경우를 대비하자.

→ first-class collection을 사용하자. 일급 컬렉션이란 클래스 내에 Collection을 제외한 다른 멤버 변수가 없는 객체이다. 이른 다른 글에서 더욱 다뤄보겠습니다.

Bad Practice

  • Repository에 조건 확인용 메소드 구현. Infrastructure Layer로 도메인 규칙이 노출, 분산 됨

→ Repository에서 findAll()하고 필터링을 건다. 다만 불필요한 데이터를 전체 조회하게 되는데 이게 맞을까…?

→ 그렇다면 Specification에 조건 메소드를 구현하고 명세를 전달하여 원하는 값만 가져오도록 하자!

profile
인생을 재밌게, 자유롭게

0개의 댓글