[DDD] 도메인 주도 개발 시작하기 - 도메인 모델 시작하기, 아키텍처 개요

01_도메인 모델 시작하기


📍 도메인

소프트웨어로 해결하고자 하는 문제 영역

한 도메인은 다시 하위 도메인으로 나눌 수 있음

ex. 카탈로그 하위 도메인은 고객에게 구매할 수 있는 상품 목록 제공

 

Garbage in, Garbage out

잘못된 요구사항이 들어가면 잘못된 제품이 나온다

 

 

 

📍 도메인 모델

특정 도메인을 개념적으로 표현한 것

여러 관계자들이 동일한 모습으로 도메인을 이해하고 도메인 지식을 공유하는 데 도움이 됨

도메인에 따라 용어 의미가 결정되므로 여러 하위 도메인을 하나의 다이어그램에 모델링하면 안됨

객체 기반 주문 도메인 모델

 

 

도메인 모델 패턴

  • 애플리케이션 아키텍처
영역 설명
사용자 인터페이스 또는 표현(UI or Presentation) 사용자의 요청을 처리하고 사용자에게 정보 제공
응용(Application) 사용자가 요청한 기능 실행. 업무 로직을 직접 구현하지 않으며 도메인 계층을 조합해서 기능 실행
도메인(Domain) 시스템이 제공할 도메인 규칙 구현
인프라스트럭처(Infrastructure) 데이터베이스나 메시징 시스템과 같은 외부 시스템과의 연동 처리

 

 

 

📍 엔티티와 밸류

엔티티

각 엔티티는 고유한 식별자를 가짐

ex. 주문 도메인에서 각 주문은 주문번호라는 식별자 가짐

 

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

 

밸류 타입

개념적으로 완전한 하나를 표현할 때 사용

코드의 가독성 향상

// Receiver 밸류 타입
public class Receiver {
		
		private String name;
		private String phoneNumber;
		
		public Receiver (String name, String phoneNumber) {
				this.name = name;
				this.phoneNumber = phoneNumber;
		}
		
		public String getName() {
				return name;
		}
		
		public String getPhoneNumber() {
				return phoneNumber;
		}
}

 

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

→ 파라미터가 변경될 때 발생하는 문제를 방지

public class Money {
		private int value;
		...
		// 기능 추가
		public Money add(Money money) {
				return new Money(this.value + money.value);
		}
		
		public Money multiply(int multiplier) {
				return new Money(value * multiplier);
		}
}

 

도메인에서 식별자는 특별한 의미를 지니는 경우가 많기 때문에 식별자를 위한 밸류 타입 사용하기도 함

public class Order {
		// OrderNo 타입 자체로 id가 주문번호임을 알 수 있다.
		private OrderNo id;
		...
		public OrderNo getId() {
				return id;
		}
}

 

set 메서드

도메인 모델에 set 메서드는 넣지 않는 것이 일반적

도메인의 핵심 개념이나 의도를 코드에서 사라지게 함

도메인 객체를 생성할 때 온전하지 않은 상태가 될 수 있음

→ 생성자를 통해 필요한 데이터를 모두 받아야 함

Order order = new Order(orderer, lines, shippingInfo, OrderState.PREPARING);

 

 

 

📍 도메인 용어

코드를 작성할 때 도메인에서 사용하는 용어를 코드에 반영하는 것이 좋음

→ 코드 가독성 향상

→ 도메인 규칙을 코드로 작성하게 되므로 버그 감소

public OrderState {
	STEP1, STEP2, STEP3, STEP4, STEP5 // 코드 해석 필요
}

public OrderState {
	PAYMENT_WAITING, PREPARING, SHIPPED, DELIVERING, DELIVERY_COMPLETED;
}

 

유비쿼터스 언어

전문가, 관계자, 개발자가 도메인과 관련된 공통의 언어를 만들고 모든 곳에서 같은 용어 사용

 

 

 

 

02_아키텍처 개요


📍 아키텍처

표현 → 응용 → 도메인 → 인프라스트럭처는 아키텍처를 설계할 때 출현하는 전형적인 네 가지 영역

상위 계층에서 하위 계층으로의 의존만 존재

 

표현(UI) 영역

사용자의 요청을 받아 응용 영역에 전달하고 응용 영역의 처리 결과를 다시 사용자에게 보여주는 역할

표현 영역을 위한 기술에는 스프링 MVC 프레임워크가 해당

웹 애플리케이션의 표현 영역은 HTTP 요청을 응용 영역이 필요로 하는 형식으로 변환해서 응용 영역에 전달하고 응용 영역의 응답을 HTTP 응답으로 변환하여 전송

 

응용 영역

시스템이 사용자에게 제공해야 할 기능 구현

기능을 구현하기 위해 도메인 영역의 도메인 모델 사용

실제 도메인 로직 구현은 도메인 모델에 위임

 

도메인 영역

도메인 모델, 핵심 로직 구현

 

인프라스트럭처 영역

구현 기술을 다룸

ex. RDBMS 연동 처리, HTTP 클라이언트를 이용해서 REST API 호출 처리

 

 

 

📍 DIP(Dependency Inversion Principle)

인프라스트럭처에 의존하면 테스트 어려움과 기능 확장의 어려움 문제 발생

→ DIP는 저수준 모듈이 고수준 모듈에 의존하도록 함

→ 추상화한 인터페이스 사용

→ DIP를 적용하면 응용 영역과 도메인 영역에 영향을 최소화하면서 구현체 변경, 추가 가능

// 사용할 저수준 객체 생성
RuleDiscounter ruleDiscounter = new DroolsRuleDiscounter();

// 생성자 방식으로 주입
CalculateDiscountService disService = new CalculateDiscountService(ruleDiscounter);

 

  • 주의사항

DIP를 적용한 결과 구조만 보고 저수준 모듈에서 인터페이스 추출하는 문제 발생

DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출

인프라스트럭처 영역은 구현 기술을 다루는 저수준 모듈이고 응용 영역과 도메인 영역은 고수준 모듈

 

 

 

📍 도메인 영역의 주요 구성요소

요소 설명
ENTITY • 고유의 식별자를 갖는 객체로 자신의 라이프 사이클 가짐
• 도메인의 고유한 개념 표현
• 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능 함께 제공
VALUE • 고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현
• 엔티티의 속성으로 사용할 뿐만 아니라 다른 밸류 타입의 속성으로 사용됨
AGGREGATE • 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것
REPOSITORY • 도메인 모델의 영속성 처리
DOMAIN SERVICE • 특정 엔티티에 속하지 않은 도메인 로직 제공

 

  • 도메인 모델의 엔티티

데이터와 함께 도메인 기능을 함께 제공

도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경하는 것을 막음

두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현

 

  • DB 모델의 엔티티

밸류 타입 표현하기 어려움

개별 데이터를 저장하거나 별도 테이블로 분리해서 저장

 

애그리거트

관련 객체를 하나로 묶은 군집

군집에 속한 객체를 관리하는 루트 엔티티를 가짐

루트 엔티티는 애그리거트에 속해 있는 엔티티와 밸류 객체를 이용해서 애그리거트가 구현해야 할 기능 제공

 

리포지터리

물리적인 저장소에 도메인 객체를 보관하기 위한 도메인 모델

애그리거트 단위로 도메인 객체를 저장하고 조회하는 기능 정의

사용 주체인 응용 서비스가 필요로 하는 메서드 제공

  • 애그리거트를 저장하는 메서드
  • 애그리거트 루트 식별자로 애그리거트를 조회하는 메서드
public interface SomeRepository {
	void save(Some some);
	Some findById(SomeId id);
}

 

 

 

📍 인프라스트럭처

도메인 객체의 영속성 처리, 트랜잭션, SMTP 클라이언트, REST 클라이언트 등 다른 영역에서 필요로 하는 프레임워크, 구현 기술, 보조 기능 지원

도메인 영역과 응용 영역에서 정의한 인터페이스를 인프라스트럭처 영역에서 구현하는 것이 DIP 관점에서 좋음

그러나 스프링을 사용할 경우 응용 서비스는 스프링이 제공하는 @Transactional 를 사용하는 것이 편리

 

 

 

📍 모듈

아키텍처의 각 영역은 별도 패키지에 위치

도메인 모듈은 도메인에 속한 애그리거트를 기준으로 다시 패키지 구성

애그리거트, 모델, 리포지터리는 같은 패키지에 위치