티스토리 뷰

개요


# 참고

DDD START! - 최범균 지음

도메인 주도 개발 시작하기! -최범균 지음

 

# 개요

DDD에 대한 기술서적을 읽고 DDD가 무엇인지 이해하고 어떻게 코드에 적용시킬 수 있는지 고민해 보려고 한다.

 

# 2장의 로드맵

  • 아키텍처
  • DIP
  • 도메인 영역의 주요 구성요소
  • 인프라스트럭처
  • 모듈

2장. 도메인 모델 시작 

표현/응용/도메인/인프라스트럭처는 아키텍처를 설계할 때 나타나는 전형적인 영역이다. 

 

2-1. 네 개의 영역

도메인 모델은 특정 도메인을 개념적으로 표현한 것이다. 모델은 도메인의 모든 내용을 담고 있지는 않지만 이 모델을 보면 큰 틀을 파악할 수 있다.

 

# 응용 영역

응용영역은 시스템이 사용자에게 제공해야 할 기능을 구현한다 (Service계층 - "주문 등록", "주문 취소", "상품 상세 조회")

응용 서비스는 로직을 직접 수행하기보다는 도메인 모델에 로직 수행을 위임한다.

public class CancleOrderService {

	@Transactional
	public void cancleOrder(String orderId){
		Oreder oreder = findOrderById(orderId);
		
		if (oreder == null){
			throw new OrderNotFoundException(orderId);
		}
		
		// 주문 취소 로직을 직접 구현하지 않고 Order객체에 취소 처리를 위임하고 있다.
		oreder.cancel();
	}

}

위 코드에서 중요한 점은 주문 취소 로직을 직접 구현하지 않고 Order객체에 취소 처리를 위임하고 있다.

주문 도메인에서는 "배송지 변경", "결제 완료", "주문 총액 계산"과 같은 핵심 로직을 도메인 모델에서 구현한다.

 

 

# 인프라스트럭처 영역

인프라스트럭처 영역은 구현 기술에 대한 것을 다룬다. 보통 RDBMS 연동을 처리하고, 메세징 큐에 메시지를 전송하거나 수신하는 기능을 구현하고, Redis와 같은 데이터 연동을 처리한다. 이 영역은 SMTP를 이용한 메일 발송 기능을 구현하거나 HTTP클라이언트를 이용해 REST API를 호출하기도 한다. 인프라 영역은 논리적인 개념을 표현하기보다는 실제 구현을 다룬다.

 

도메인 영역, 응용 영역, 표현 영역은 구현 기술을 사용한 코드를 직접 만들지 않는다. 대신 인프라 영역에서  제공하는 기능을 사용해서 필요한 기능을 개발한다. 

 

 

2-2. 계층 구조 아키텍처

위 그림은 전형적인 계층 아키텍처 구조이다. 계층 구조는 그 특성상 상위 계층에서 하위 계층으로의 의존만 존재하고 하위 계층은 상위 계층에 의존하지 않는다. 좀 더 쉽게 말한다면 응용계층은 오직 도메인 계층만 의존하며, 도메인 계층은 응용 계층을 의존하지 않는다.

 

물론 유연함을 위해 응용 계층이 인프라 계층을 의존할 수도 있다. 하지만 이러한 구조의 문제점은 모든 계층이 상세한 구현 기술을 다루는 인프라 계층에 종속된다는 점이다.

 

// 인프라 영역
public class GoogleMailSMTP {
	public void googleMailSend(){
		// 메일 전송 로직
	}
}

// 인프라 영역
public class NaverMailSMTP {
    public void naverMailSend(){
		// 메일 전송 로직
	}
}

// 서비스 영역
public class SendMailService(){
	
	private GoogleMailSMTP infra;

	// 생성자
	public SendMailService() {
		infra = new GoogleMailSMTP();
	}
	
	// 메일 전송 서비스 로직
	public void sendMail(){
		
		// 인프라 메소드 사용
		infra.googleMailSend();
		// 즉, 인프라의 메소드가 변경이 되면, 서비스도 코드를 수정해 주어야 한다.
		// 구글에서 네이버로 SMTP서버를 변경하고 싶다면? 
		// 인프라 로직도 네이버로 변경해야 하고 서비스도 네이버 코드로 변경해야한다.
		infra.naverMailSend();
		
	}
	
}

 

서비스 이용자에게 메일을 전송하기 위해서는 SMTP를 사용해야 한다. SMTP는 보통 인프라 계층에 속한다. 나는 구글 SMTP 서버를 사용하기로 결정해서 로직을 구성하였지만, 구글 서버 이용료가 비싸져 네이버 SMTP 서버를 사용하기로 결정되었다면 어떻게 되는 것일까? 인프라 계층의 SMTP 로직도 변경해야 하지만, 서비스 계층도 코드를 변경해 주어야 한다. 즉, 응용 계층(Service)이 인프라 영역을 강하게 의존하게 된다.

 

이렇게 모든 계층이 인프라 영영역을 강하게 의존하면 다음과 같은 2가지의 문제가 발생한다.

1. 테스트하기가 어렵다.

테스트하기 위해서는 인프라 영역이 완벽하게 동작해야 한다.

2. 기능 확장의 어려움이 있다.

인프라 영역의 코드 변경이 서비스 계층에 영향을 주기 때문에 쉽게 확장을 하지 못한다.

 

그렇다면 이러한 문제를 어떻게 해결할까? 어떻게 해야 인프라 계층이 변해도 서비스 계층의 코드는 변하지 않도록 할 수 있을까? 정답은 바로 DIP이다. 

 

 

 

2.3 DIP

구글 메일 서버를 이용해서 서비스 이용자에게 메일을 전송하려면 다음과 같이 고객 정보를 구하고, 구한 고객 정보를 이용해 메일을 전송해야 한다. 

 

즉, 고수준의 모듈의 기능을 구현하려면 여러 하위의 저수준 기능들이 필요하다. 그런데 고수준 모듈이 저수준 모듈을 사용하면 앞서 설명한 계층 구조 아키텍처에서 언급했던 두 가지 문제가 발생한다.

1. 테스트의 어려움

2. 구현 변경의 어려움

 

DIP는 이 문제를 해결하기 위해 저수준 모듈이 고수준 모듈을 의존하도록 바꾼다. 그렇다면 저수준 모듈이 고수준 모듈을 의존하게 하려면 어떻게 해야 할까? 바로 추상화한 인터페이스를 사용하는 방법이다.

 

고수준이 저수준을 의존 (AS-IS)
고수준은 인터페이스를 의존, 저수준은 인터페이스를 구현 (TO-BE)

 

 

# DIP를 적용한 메일 전송 코드

public interface MailSenderInterface{
	void sendMail();
}

// 인프라 영역 - 구글
public class GoogleMailSMTP implements MailSenderInterface{

	@Override
	public void sendMail() {
		// 메일 전송

		//google, naver 메일 전송 코드
	}

}

// 인프라 영역 - 네이버
public class NaverMailSMTP implements MailSenderInterface{

	@Override
	public void sendMail() {
		// 메일 전송

		//google, naver 메일 전송 코드
	}

}

// 서비스 영역
public class SendMailService(){

	// (DIP)
	private MailSenderInterface mailSenderInterface;

	// 생성자  의존성 주입
	public SendMailService(MailSenderInterface mailSenderInterface) {
		this.mailSenderInterface = mailSenderInterface;
	}

	// 메일 전송 서비스 로직
	public void sendMail(){

		// 인터페이스 메소드 사용
		mailSenderInterface.sendMail();
		// 즉, 인프라의 메소드가 변경이 되면, 서비스도 코드를 수정이 필요없다.
		// 구글에서 네이버로 SMTP서버를 변경하고 싶다면?
		// 인프라 로직만 변경해도 전혀 서비스는 영향을 받지 않는다.
		mailSenderInterface.sendMail();

	}

}

즉, DIP를 적용하게 된다면, Infra의 코드가 변경되더라도 서비스 계층은 영향을 받지 않기 때문에 응용계층이 인프라계층을 의존하는 안 좋은 현상을 제거할 수 있다. 이전과 달리 위의 코드에서는 서비스 계층이 인터페이스를 의존하고 있고, 인프라 계층은 인터페이스를 구현하고 있는데, 만약 구글에서 네이버로 SMTP서버를 변경하더라도 서비스 계층에서 코드 변경은 전혀 일어나지 않는다. 인터페이스 역시 고수준 모듈에 속하게 된다. 즉, DIP란 저수준 모듈이 고수준 모듈을 의존하는 것을 의미한다.

 

		MailSenderInterface mailSender = new GoogleMailSMTP();
		SendMailService service = new SendMailService(mailSender);
		MailSenderInterface mailSender = new NaverMailSMTP();
		SendMailService service = new SendMailService(mailSender);

DIP를 이용한다면 다음과 같이 사용할 저수준 구현 객체만 변경해 주면 된다. 서비스 계층에서 사용되는 로직은 전혀 변경되지 않는다. 

 

2.3.1 DIP 주의사항

DIP를 잘못 생각하면 단순히 인터페이스와 구현 클래스를 분리하는 정도로 받아들일 수 있다. DIP의 핵심은 고수준 모듈이 저수준 모듈에 의존하지 않도록 하기 위함인데 DIP를 적용한 결과 구조만 보고 그림과 같이 저수준 모듈에서 인터페이스를 추출하는 경우가 있다.

잘못된 인터페이스 위치

위 그림은 잘못되 구조이다. 이 구조에서 도메인 영역은 구현 기술을 다루는 인프라 영역에 의존하고 있다. 여전히 고수준 모듈이 저수준 모듈에 의존하고 있는 것이다. DIP를 적용할 때 하위 기능을 추상화한 인터페이스는 고수준 모듈 관점에서 도출한다. Service 계층은 메일을 전송하기 위해 구글을 사용하든, 네이버를 사용하든 지는 중요하지 않다. 단순히 규칙에 따라 메일을 전송한다는 것이 중요할 뿐이다. 따라서 추상화한 인터페이스는 저수준 모듈이 아닌 고수준 모듈에 위치해야 한다. 

 

물론 반드시 DIP를 통해 인프라 계층의 의존성을 없애야 하는 것은 아니다. @Transaction은 스프링에서 제공하는 기능이지만 스프링에 대한 의존성을 없애기 위해 @Transaction을 직접 구현하는 것은 오히려 서비스를 복잡하게 만든다. DIP의 장점을 헤치지 않는 선에서 알맞게 사용되어야 한다. 


 

2-4. 도메인 영역 주요 구성요소

 

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

1. 엔티티

2. 밸류

3. 어그리게잇

4. 리포지토리

5. 도메인 서비스

 

# 엔티티

엔티티는 고유의 식별자를 갖는 객체로 자신의 라이프 사이클을 갖는다. 주문, 회원, 상품과 같이 도메인의 고유한 개념을 표현한다. 도메인 모델의 데이터를 포함하며 해당 데이터와 관련된 기능을 함께 제공한다.

 

# 밸류

고유의 식별자를 갖지 않는 객체로 주로 개념적으로 하나인 값을 표현할 때 사용된다. 배송지 주소를 표현하기 위한 주소나 구매금액을 위한 금액과 같은 타입이 밸류 타입이다. 엔티티 속성으로 상용할 뿐만 아니라 다른 밸류 타입의 속성으로도 사용할 수 있다.

 

# 어그리게잇

어그리게잇은 연관된 엔티티와 밸류 객체를 개념적으로 하나로 묶은 것이다. 예를 들어 주문과 관련된 Order 엔티티, OrderLine 밸류, Orderer 밸류 객체를 '주문' 어그리게잇으로 묶을 수 있다.

 

# 리포지터리

도메인 모델의 영속성을 처리한다. 예를 들어 DBMS 테이블에서 엔티티 객체를 로딩하거나 저장하는 기능을 제공한다.

 

# 도메인 서비스

특정 엔티티에 속하지 않은 도메인 로직을 제공한다. 예를 들어 '할인 금액 계산'은 상품, 쿠폰, 회원 등급, 구매 금액 등 다양한 조건을 이용해서 구현하게 되는데, 이렇게 도메인 로직이 여러 엔티티와 밸류를 필요로 하면 도메인 서비스에서 로직을 구현한다. 

 

Q. 응용 계층의 Service와 도메인 서비스는 무슨 차이 일까... 우리가 흔히 아는 Service 계층은 어디에 속할까?

 

 

2-4-1. 엔티티와 밸류

많은 사람들이 도메인 모델의 엔티티와 DB의 엔티티를 같은 걸로 생각하지만, 둘은 같은 것이 아니다. 이 두 모델의 가장 큰 차이점은 도메인 모델의 엔티티는 데이터와 함께 도메인 기능을 함께 제공한다는 점이다. 예를 들어 주문을 표현하는 엔티티는 주문과 관련된 데이터뿐만 아니라 배송지 주소변경을 위한 기능을 함께 제공한다.

 

// 도메인 엔티티
public class Order{

	// 도메인 엔티티 데이터
	private OrderNo number; // 밸류타입
	private Orderer orderer; // 밸류타입
	private Shipping shipping; // 밸류타입
	
	// 도메인 엔티티 기능
	public void changeShippingInfo(Shipping shipping){
		// 주문 정보 변경
	}
	
	...

}

// 밸류 타입 Orderer
public class Orderer {
	private String name;
	private String email;
}

도메인 모델의 엔티티는 단순히 데이터를 담고 있는 데이터 구조라고 하기보다는 데이터와 함께 기능을 제공하는 객체이다. 도메인 관점에서 기능을 구현하고 기능 구현을 캡슐화해서 데이터가 임의로 변경되는 것을 막는다. 

 

또 다른 차이점은 도메인 모델의 엔티티는 두 개 이상의 데이터가 개념적으로 하나인 경우 밸류 타입을 이용해서 표현할 수 있다는 것이다. 위 코드에서 OrderNo, Orderer, Shipping은 전부 밸류 타입이며 각각의 데이터를 포함하고 있다.

 

하지만 RDBMS에서는 밸류 타입을 제대로 표현하기 힘들기 때문에 밸류 타입은 별도의 테이블로 분리해서 저장해야 한다. 또한 밸류 타입은 불변으로 구현하는 것이 좋다. 불변으로 구현한다는 것은 엔티티의 밸류 타입 데이터를 변경할 때는 객체 자체를 완전히 교체한다는 것을 의미한다. 예를 들어 배송지 정보를 변경하는 코드는 기존 객체의 값을 변경하지 않고 다음과 같이 새로운 객체를 필드에 할당해야 한다.

 

// 도메인 엔티티
public class Order{

	// 도메인 엔티티 데이터
	private OrderNo number;
	private Orderer orderer;
	private Shipping shipping;

	// 도메인 엔티티 기능
	public void changeShippingInfo(Shipping newShipping){
		// 주문정보 변경 - 새로운 주문 정보 생성자 객체 할당
		setShipping(newShipping);
	}

	// setter - private 접근지정자 
	private void setShipping(Shipping shipping) {
		this.shipping = shipping;
	}

}

 

 

2-4-2. 어그리게잇

도메인이 커질수록 많은 엔티티와 밸류가 나타나게 되며 점점 복잡해진다. 도메인 모델이 복잡해지면 개발자는 전체가 아닌 개별 요소에 집중하는 실수가 발생할 수 있다. 우리가 지도를 볼 때 대축적을 보면 전체 위치를 파악하기 쉽듯, 어그리게잇이 대축적 지도와 비슷한 개념이다. 애그리게잇을 통해 도메인 모델의 전체 구조를 파악하는데 도움이 될 수 있다. 

 

애그리게잇은 관련 객체를 하나로 묶은 군집이다. 주문 도메인에는 "주문/배송지 정보/ 주문자/주문 목록/금액"의 하위 모델로 나눌 수 있다. 이 하위 개념을 표현하는 모델을 하나로 묶어서 주문이라는 상위 개념으로 표현할 수 있다. 따라서 어그리게잇을 통해 큰 틀에서 도메인 모델을 관리할 수 있다.

 

어그리게잇은 군집에 속한 객체를 관리하는 루트 엔티티를 갖는다. 루트 엔티티는 애그리게잇에 속해 있는 엔티티와 밸류 객체를 이용해서 어그리게잇을 구현해야 할 기능을 제공한다. 조금 더 설명한다면 어그리게잇 루트를 통해서 간접적으로 어그리게잇 내의 다른 엔티티나 밸류 객체에 접근한다. 이는 어그리게잇의 내부 구현을 숨겨서 어그리게잇 단위로 구현을 캡슐화 할 수 있도록 돕는다.

 

이는 주문 도메인에서 상품 도메인을 참조하기 위해서는 반드시 어그리게잇 루트를 통해서만 접근해야 한다는 것이다. 만약 상품 도메인에서 주문 도메인의 결제 데이터를 참조하기 위해 결제에 직접적으로 접근하는 것은 데이터의 일관성을 깨트릴 수 있기 때문에 피해야 할 접근법이다. 또한 어그리게잇을 어떻게 구성했느냐에 따라 구현이 복잡해지기도 하고, 트랜잭션 범위가 달라지기도 한다. 이는 추후 뒤에서 좀 더 자세히 설명.

 

 

2-4-3. 레포지토리

도메인 객체를 지속적으로 사용하려면 RDBMS, NoSQL 같은 물리적인 저장소에 도메인 객체를 보관해야 한다. 이를 위한 도메인 모델이 리포지토리(Repository)이다. 엔티티나 밸류는 요구사항에서 도출되는 도메인 모델이라면, Repository는 구현을 위한 도메인 모델이다. 

 

Repository는 어그리게잇 단위로 도메인 객체를 저장하고 조회하는 기능을 정의한다. 이상적인 Repository는 어그리게잇에 속한 모든 객체를 포함하고 있으므로 결과적으로 어그리게잇을 단위로 저장하고 조회한다. (나는 이때까지 밸류 단위로 Repository를 작성)

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
글 보관함