본문 바로가기
이슈와해결

도메인 주도 개발 방법론(DDD)을 적용하여 3티어 아키텍처를 변경해보자

by Renechoi 2023. 6. 26.

0. 배경

 

스프링 부트로 개발을하면서 어떻게 하면 결합도는 낮추고 응집도는 높이는 코드를 짤 수 있을까 하는 고민을 한다. 프로젝트를 하면서 MVC 패턴을 이용하는 것은 익숙했다. 하지만 보다 좋은 설계에 대한 갈증 같은 것이 항상 있었다. 엔티티 간의 연관관계가 복잡해지고 참조 Depth가 깊어질 때마다 내가 하는 이 방식이 맞는건가 하는 생각이 자주 들곤 했다. 서비스가 비대해지는 것도 문제였지만 특히 서비스와 서비스가 서로 참조관계를 맺는 것도 불편했다. 

 

 

 

또 서비스라고 부르기에는 좀 애매한데 그렇다고 서비스가 아니라고 하기는 좀 뭐한 기능 클래스들이 있었다. 예를 들어 다음과 같은 PaginationService이다. 이에 대해서도 적절한 네이밍을 설정하고 레이어를 맞춰주는 것이 힘들었다. 

 

 

이에 대한 고민 중에 DDD 개발 방법론을 알게됐고 관련된 서적과 강의를 들으며 레이어를 좀 더 효율적으로 구성할 수 있다는 것을 알게 됐다. 

 

좋은 구현이란 무엇일까? 

 

다음의 사항을 만족해야 할 것이다. 

- 잘 읽히기 

- 변경에는 닫혀 있고 확장에는 무한히 열려 있기

- 테스트 코드 작성이 쉽기 

 

결론적으로 높은 안정성과 확장성이다. 이를 실현함에 있어서 높은 의존성과 강한 결합도는 해롭다.

 

도메인 주도 설계 방식을 적용한 아키텍처는 순수 MVC보다 확실히 위의 좋은 구현 사항을 만족한다고 생각한다.

 

이를 실전에 적용하는 연습으로서 MVP로 개발한 간단한 프로젝트를 리팩토링해보았다. 

 

 

1. 프로젝트 소개 

 

제삼자물류 시스템에서 VOC가 발생하였을 때 이를 처리하기 위한 백엔드 로직을 구현한 어플리케이션이다. 도메인인 VOC를 중심으로 VOC 처리 로직 구현에 집중한 프로젝트였다. 

(깃허브 링크 https://github.com/renechoi/timf-voc-task)

 

주요 기능과 시나리오는 다음과 같다. 

 

주요 기능

- 운송사, 고객사 Entity 생성 및 연관관계 설정
- Voc 및 관련 Value Objects, Entity 생성 및 연관관계 설정
- Claim 조회 기능
- Voc 생성 및 조회 기능
- Voc 발생에 따른 귀책 당사자 Penalty 처리 기능
- 운송사 귀책시 월급 공제
- Voc 발생에 따른 Compensation 요구 처리 기능
- 고객사 요구시 임의의 payment 지급 기능
- 운송 기사 마이페이지를 통한 Voc 알림 기능
- SSE(Server-Sent Event)를 이용한 구독->알림 로직 구현

 

시나리오

- 임의의 사용자로부터 Claim이 접수된다(Claim은 더미 데이터로서 생성한다).
- 관리자는 접수된 Claim을 확인하고 Voc를 생성한다.
- Voc 생성시 귀책 당사자를 설정한다. 귀책 당사자는 운송사 혹은 고객사이다. 운송사의 경우 실질 귀책 당사자는 배송기사로 설정한다.
- 현재 프로젝트에서는 귀책 당사자는 전부 운송사이다.
- 귀책 당사자, 즉 배송기사에게는 Penalty 금액이 부과된다. Penalty는 배송기사의 월급에서 금액을 제외하는 것으로 처리된다.
- 고객사에게는 Compensation 금액이 지급된다.
- 생성된 Voc는 운송기사 마이페이지로 전송된다. 알림이 지원된다.
- 배송기사는 Voc를 확인하고 두 가지 액션 중 한가지를 선택해 응답해야 한다
  - 승낙: 해당 Voc에 업데이트 되며 Penalty 부과와 Compensation 지급후 DB 저장으로 마무리 된다.
  - 거절: 배송기사는 사유와 함께 거절할 수 있다. Penalty와 Compensation은 처리되지 않고 홀딩된다. 정책에 따른 별도 로직이 요구된다.

 

 

 

2. 기존 방식 

 

스프링 MVC에서 사용하는 레이어드 아키텍처(Layered Architecture)를 그대로 사용했다. 

 

https://www.javaguides.net/2020/07/three-tier-three-layer-architecture-in-spring-mvc-web-application.html

 

 

다음과 같은 3 티어 아키텍처이다. 각 계층은 표현 계층, 비즈니스 로직 계층, 데이터 접근 계층으로 나뉜다.

 

표현 계층 

- 컨트롤러(Controller)와 뷰(View)로 구성된다.
- 사용자의 요청을 받아 처리하고, 응답을 생성하여 클라이언트에 반환한다.

 

비즈니스 로직 계층 

- 사용자 요청에 대한 비즈니스 규칙을 적용하고, 데이터의 유효성 검사, 트랜잭션 관리 등을 담당한다.
- 도메인 객체의 상태 변경, 데이터 조작, 데이터 검색 등을 수행한다.
- 스프링 MVC에서는 주로 서비스(Service) 클래스가 해당 역할을 수행한다.

 

데이터 접근 계층

- 데이터베이스나 외부 시스템과의 상호 작용을 담당한다.
- 데이터의 영속성을 보장하고 데이터의 CRUD(Create, Read, Update, Delete) 작업을 수행한다.
- 주로 데이터베이스와의 연동, 쿼리 실행, 데이터 매핑 등을 처리한다.
- 스프링 MVC에서는 주로 리포지토리(Repository) 또는 DAO(Data Access Object)가 해당 역할을 수행한다.

 

 

이와 같은 계층 구분을 그대로 사용하여 다음과 같은 패키지를 구성했다. 

 

 

 

계층 구성도를 보면 다음과 같다. 

 

컨트롤러가 서비스를 의존하지만, 계층 구성의 중심이 되는 서비스는 분명 데이터 레벨에 독립적이다. Service는 Repository를 인터페이스로 가지며, Spring data Jpa를 통해 Repository 구현체가 Service로 주입된다. 

 

스프링 DI 원칙을 접할 때 Spring Data Jpa를 통해 고수준 모듈인 서비스 계층이 저수준 모듈인 데이터 계층에 대한 의존성을 해체하는 과정을 보면서 매우 신기하고 놀라웠었다. 기존의 Jdbc 템플릿이나 Mybatis는  직접 데이터베이스 연결 및 관련 객체를 생성하고, 필요한 의존성을 수동으로 처리해야 한다. 때문에 서비스 레이어든 어디든 철저하게 데이터베이스와 강결합을 맺는 코드를 작성하게 되었었다. 게다가 Spring Data Jpa의 쿼리 메서드 작성 방식까지 배우면서 ORM 매핑 방식과 스프링 DI 원칙의 강력함에 매일 같이 감탄하곤 했었다. 

 

public class TransportCompanyService {

   private final DeliveryDriverRepository deliveryDriverRepository;
   
   // ...
}
public interface DeliveryDriverRepository extends JpaRepository<DeliveryDriver, Long> {
   Optional<DeliveryDriver> findDeliveryDriverByDeliveryDriverToken(String token);
}

 

분명히 이와 같은 의존관계 설정은 DI 원칙을 적용한 설계라고 볼 수 있지만, 시간이 지날수록 의문이 들었다. 

 

DI의 목표는 무엇일까? 의존관계를 역전시켜 결합도를 낮추는 것의 목적은 결국 유연함과 확장성이다. 예를 들어 서비스 레벨과 데이터베이스 레벨의 의존성 역전이라고 한다면, 데이터 베이스 계층의 설계를 어떻게 바꾸더라도 서비스 레벨이 해당 변경 사항에 따라 영향을 받아서는 안된다. 즉, JPA를 Mybatis로 바꾸거나 QueryDsl로 바꿔도 서비스에서는 코드 변경 사항이 발생해서는 안 된다. 

 

 

1) 서비스와 레포지토리 간의 의존관계 문제 

 

서비스와 레포지토리 간의 의존관계를 인터페이스로 연결하여 의존성 결합도를 낮출 수는 있지만, 여전히 일부 문제가 발생할 수 있다.

예를 들어, MyBatis를 사용하는 경우 Optional을 리턴할 수 없는 제약이 있다. MyBatis는 일반적으로 조회 결과가 없을 때 null을 반환하거나 빈 컬렉션을 반환하는 방식을 취한다. 따라서 서비스에서 MyBatis를 사용하는 경우에는 Optional을 직접 반환할 수 없고, null 또는 빈 컬렉션을 반환하는 방식으로 처리해야 한다. 뿐만 아니라 MyBatis의 쿼리 매핑 방식은 SQL 쿼리를 직접 작성하는 것이기 때문에 데이터베이스 구조가 변경되거나 쿼리를 수정해야 할 때에는 서비스 레이어에서 해당 변경에 따른 코드 수정이 필요할 수 있다. 

 

어쨌거나 의존성이 주입되기는 하지만 데이터베이스 접근 레이어와 서비스 레이어가 직접적으로 닿아 있어 데이터베이스 관련 추가적인 작업이 필요할 경우 서비스 코드의 수정이 발생할 수 있다. 이점이 상당히 불편하게 다가왔다. 

 

 

2) 컨트롤러와 서비스 간의 의존관계 문제 

 

또 다른 문제는 컨트롤러와 서비스 간의 의존관계 문제이다. 현재 설계에서는 컨트롤러에서 서비스를 직접 의존한다.

 

@Controller
@RequestMapping("/voc")
@RequiredArgsConstructor
public class VocController {

   private final VocService vocService;
   
   // ...
}

 

3) 서비스와 서비스 간의 많은 참조 관계 문제 

 

또 다른 문제는 서비스와 서비스 사이에서 발생한다. 

 

예를 들어 다음과 같은 코드를 보자. 

 

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class VocService {

   private final ClientCompanyService clientCompanyService;
   private final TransportCompanyService transportCompanyService;
   private final ClaimService claimService;
   private final NotificationService notificationService;
   private final KafkaProducerService kafkaProducerService;

   private final VocRepository vocRepository;
   private final CompensationRepository compensationRepository;
   
   // ...
}

 

Voc 서비스는 ClientCompany 외 5개의 서비스를 참조하는데, VOC 등록, VOC 상태 처리, 알림 전송, 데이터베이스 조회 등의 작업을 수행한다. ClientCompany와 DeliveryDriver는 아래와 같이 Voc 엔티티 매핑에 사용되므로 필요하다. 

 

@Transactional
public void registerVoc(VocRequest vocRequest) {
   ClientCompany clientCompany = getClientCompany(vocRequest);
   DeliveryDriver deliveryDriver = getDeliveryDriver(vocRequest);

   vocRepository.save(Voc.createVoc(vocRequest, clientCompany, deliveryDriver));

   // ...
}
private DeliveryDriver getDeliveryDriver(VocRequest vocRequest) {
   return transportCompanyService.searchDeliveryDriverEntity(vocRequest.getDeliveryDriverId());
}

private ClientCompany getClientCompany(VocRequest vocRequest) {
   return clientCompanyService.searchClientCompanyEntity(vocRequest.getClientCompanyId());
}

 

 

4) 서비스의 부가 기능 처리 문제 

 

VocService의 RegisterVoc 메서드는 request에 대해 save로직을 수행한다. 그런데 저장이 완료된 후에 또 다른 기능이 필요하만 어떨까? 예를 들면 Voc를 등록하게 한 Claim에 대한 처리, Voc 관련 당사자에게 알려야 하는 알림이다.

 

@Transactional
public void registerVoc(VocRequest vocRequest) {
   
   vocRepository.save(Voc.createVoc(vocRequest, clientCompany, deliveryDriver));

   claimService.handleStatus(vocRequest, true);

   notificationService.notifyNewVoc();
   kafkaProducerService.notifyNewVoc();

}

 

 

이와 같은 문제들을 아키텍처 변경으로 자연스럽게 해결하는 과정을 살펴보자. 

 

 

3. 변경 

 

 

1) 레이어 계층 구조 변경 

 

 

다음과 같이 레이어 계층 구조를 변경했다. 

 

 

레이어별 특징과 역할은 다음과 같다. 

 

 

Layer Description 주요 객체
사용자 인터페이스 (Interfaces) 사용자의 요청을 해석해 서버로 전달하고, 서버로부터 받은 응답을 사용자에게 전달한다. Controller, Dto
응용 계층 (Application) 수행할 작업을 정의하고 적절한 도메인 객체에게 작업을 분배한다. 상호작용이 필요한 역할에 대해서 수행한다.  Facade
도메인 계층 (Domain) 비즈니스 로직에 대한 개념, 행위, 규칙 등을 규정하고 수행한다. 비즈니스 상태를 반영하는 상태를 제어하고 추상화된 표현으로 주요 로직을 정의한다. 세부 수행을 인프라 계층에 위임한다.  Entity, Command, Info, Service, Reader, Persister, Factory, 등등
인프라 계층 (Infrastructure) 상위 계층을 지원하는 기술적 구현을 담당한다. 도메인 영속화 등의 데이터 베이스를 사용하는 세부 기능을 수행한다. Low level 구현체(SimpleReader, Repository, 등등)

 

전반적인 데이터 흐름 자체는 크게 다르지 않지만 계층을 보다 세분화 하고 범위를 명확히 한데에 의미가 있다. 

 

주요 특징 사항을 살펴보면 다음과 같다. 

 

 

Application 계층 생성

- transaction으로 묶여야 하는 도메인 로직과 기타 부수적인 기능을 수행하는 계층으로 정의한다. 

- 일반적인 도메인 주도 설계에서 이야기하는 역할, 즉 수행할 작업을 정의하고 작업을 조정하는 역할은 사실 도메인의 역할과 비슷하다.

- 따라서 실용적인 측면에서 서비스 간의 조합을 하나의 요구사항에서 처리해야 할 때 필요한 작업을 수행하는 정도로 정의한다. 

- 예를 들어, Voc가 등록된 뒤 추가적으로 필요한 Claim에 대한 처리, Voc 발생에 대한 알림 처리 등을 수행한다. 

- 인터페이스 aggregation의 의미로 사용되는 Facade로 클래스를 네이밍한다.

- transaction의 사용 여부는 비즈니스 정책에 따라 다를 수 있다. 예를 들어, Voc 등록 직후 알림 발송에 실패하더라도 모든 과정이 롤백될 필요가 있을까? 따라서 aggregate의 정합성을 유지하는 한도 내에서 transaction은 정책에 따라 다르게 설정할 수 있는 유연성을 가져간다. 

 

@RequiredArgsConstructor
@Service
public class VocFacade {
   private final SimpleVocService simpleVocService;
   private final SimpleClaimService simpleClaimService;
   private final NotificationService notificationService;
   private final KafkaProducerService kafkaProducerService;

   public void registerVoc(VocRegisterRequest request) {
      simpleVocService.registerVoc(request);
      simpleClaimService.updateStatusTrue(request);

      notificationService.notifyNewVoc();
      kafkaProducerService.notifyNewVoc();
   }
   
   // ...
}

 

→ 서비스의 부가 기능 처리 문제 해결 

 

 

 

VO 객체들은 특정 범위를 벗어나지 않는다.

- Dto는 Interfaces 레이어를 벗어나지 않는다.

- Intefaces 밑의 서버 침투를 위해 Command 객체를 생성한다. 

- Entity는 Domain 레이어를 벗어나지 않는다. 

- Domain 레이어 바깥으로 전달되는 객체는 Info 객체이다. 

 

 

 

도메인 계층의 클래스의 네이밍을 세분화한다

- 하나의 도메인에 너무 많은 Service가 있으면 인지 과부하가 온다. 

- 주요 도메인의 흐름을 관리하는 핵심 Service를 두고, 이를 위한 Support 수준의 클래스들은 기능에 따라 Service 이외의 네이밍으로 한다. 

- 예를 들어 Reader, Persister, Factory 등등이다. 

- 해당 클래스들은 도메인에서 추상화 레벨로 존재하고, 인프라 계층에서 구현체로서 존재하도록 한다. 관련 내용은 밑의 추상화 부분에서 더 자세히 다룬다. 

 

다음은 transport company의 관련 집합들에 대한 서비스 클래스 세분화 예시이다. 

 

 

 

도메인 주도 개발에서 사용하는 aggregate란 개념은 관련된 객체들의 집합을 의미한다. 하나의 aggregate는 하나 혹은 다수의 루트 엔티티와 다수의 값 객체(Value Object) 및 다른 엔티티로 구성된다. 현재 프로젝트에서 Voc는 aggregate를 형성한다.

 

아래는 Voc aggregate를 표현하는 클래스 다이어그램이다. 

 

 

기존 코드에서는 aggregate내의 vo를 처리하는 역할을 어떻게 분배할지 애매했었다. Vo 객체들은 root를 통해서만 조작되어야 하는데, 그러자니 관련된 모든 기능들을 VocService에서 처리하게 되기 때문이다. 그결과 한 서비스에서 의미적으로도 일관되지 않은 메서드들이 생겨나게 되고 서비스가 두꺼워질 뿐만 아니라 개념적 통일성을 헤친다. 

 

그런데 아래와 같은 서비스 support 클래스들을 별도로 분리하면 root 계층이 관리하는 vo들에 대한 계층 관계가 형성된다. 또한 factory라는 네이밍의 aggregate를 관리하는 클래스는 의미상 명확한 책임과 역할을 클래스명에서부터 드러낸다.

 

 

 

 

 

 

 

 

서비스 간에 참조 관계를 맺지 않는다. 

- 주요 Service만을 Service로 정의함으로써 이와 같은 분리가 가능해진다. 

- Service 간에는 암묵적인 상하 관계가 생기는 경우가 많은데, 이런 구조를 허용하면 상위 레벨의 Service가 하위 레벨의 Service를 다수 참조하게 되고, 이는 복잡함을 야기하며 어려운 코드를 만든다. 

- Servcie 내 로직의 추상화 수준을 높인 상태에서 필요한 특정 클래스만을 참조하되, 해당 클래스들 역시 추상화 레벨을 높게 가져간다면 복잡한 참조 관계를 피할 수 있다. 

- 위에서 다룬 Application층 도입과, 서비스와 기능 서비스들을 분리함으로써 가능하다.

→ 서비스 간 많은 참조 문제 해결

 

 

인프라 계층은 의존성 주입(DI)을 통해 도메인 계층을 지원한다. 

- 도메인 계층의 높은 추상화 수준을 유지하기 위해서 DI를 통해 세부 구현체들은 인프라 계층에서부터 주입된다.

- 기존의 DI 방식과 본질적인 내용은 동일하지만 중심되는 Service는 DB와 직접적인 접촉을 피한다. 

→ 서비스와 레포지토리 간의 의존 관계 문제 해결 

 

 

인프라 계층 간에는 참조 관계를 허용한다. 

- 인프라 계층의 구현체는 도메인 계층에서 선언된 인터페이스를 구현하는 경우가 대부분이다.

- 따라서 서비스에 비해 의존도가 크지 않다.

 

 

세부 구현체들은 @Componet를 통해 빈으로 등록한다. 

- Spring에서 @Service와 @Component는 실질적으로 큰 차이가 없다.

- 그러나 명시성을 위해 @Component로 등록한다. 이는 앞서 하나의 주된 Service를 설정하는 것과 같은 맥락이다. 

 

 

 

 

 

 

2) 추상화 

 

아키텍처 변경에서 또 한가지 중요한 포인트는 도메인 레이어의 추상화 레벨을 높은 수준으로 설정하였다는 점이다. 

 

기존의 도메인 레벨에서는 데이터베이스 접근 로직에 대해서만 추상화하였었다. 변경된 스타일에서는 도메인에서 정의하는 대부분을 추상화하여 정의한다. 

 

 

 

사실 3 티어 아키텍처에서도 Service를 인터페이스로 작성할 수 있지만 그 의미가 크게 다가오지 않았었다. 왜냐하면 한 도메인에 대해서 Service는 대체로 하나만 존재하기 때문에 실질적인 의존관계 역전의 효용이 없다고 보았기 때문이다. 지금 생각해보면 사실은 그렇더라도 인터페이스로 정의했어야 하는 게 맞는 것 같다. 

 

 

1) 계약의 의미 

 

인터페이스를 사용하는 이유 중 하나로 계약의 의미가 있다. 인터페이스는 객체 간의 계약을 정의한다. 인터페이스는 인터페이스를 구현하는 객체가 정의된 기능들을 반드시 구현하도록 하위 객체의 내부 설계를 강제한다. 따라서 xxxService라고 이름 붙은 모든 것들은 인터페이스에 정의된 xxx메서드 의 내용을 반드시 구현해야 한다. 이는 프로그램의 규모가 커질수록 일관된 통일성을 유지할 수 있다는 점에서 유용하다. 

 

변경한 아키텍처 구조는 계약으로서의 의미를 좀 더 잘 드러낸다. 하나의 주요 서비스를 정의하기 때문에 해당 서비스는 추상화된 레벨로서 핵심 기능들을 정의한다. 이때 세부 구현에 대해서는 자신 메서드에 대해서 뿐 아니라 필요한 메서드에 대해 같은 위상의 세부 구현 객체들(Reader, Persister 등)에게 위임하며, 구체적인 기능 구현에 대해서는 인프라 레이어로 밀어넣는다. 이는 도메인 레이어가 계약으로서의 추상성을 두드러지게 한다. 

 

 

 

위의 코드에서 보면 먼저 인터페이스가 구현체들이 구현해야 하는 메서드들을 선언한다. 해당 인터페이스를 구현하는 SimpleVocService는 해당하는 메서드를 Overriding하여 구현하지만, 실제로 세부 구현에 대한 내용은 없다. 구체적인 구현은 다음과 같이 인프라 레이어에서 이루어진다. 

 

 

 

레포지토리에 대한 직접적인 참조는 이와 같이 인프라레벨에 구현된 세부 구현체들이 형성한다. 

 

도메인 레이어에 존재하는 서비스 클래스가 이처럼 행동의 규약을 확장한 차원에서만 메서드를 정의한다는 것을 추상화 레벨을 높인다고 볼 수 있다. 이는 계약의 의미로써 존재하는 메서드가 일관된 목적을 갖게 존재하도록 강제할 뿐 아니라, 가독성을 높여 인지 과부하를 방지하고 누구나 읽어도 쉽고 빠르게 이해할 수 있도록 한다. 이는 좋은 코드 구현 사항의 "잘 읽히기"를 만족하는 것으로 성공적인 협업을 위해 중요한 부분이라고 생각한다. 

 

 

2) 의존성 분리와 다형성 

 

추상화 레벨을 높게 가져감으로써 얻을 수 있는 또 다른 이점은 의존성 분리이다. 앞에서 언급했듯이 3티어에서도 충분히 같은 방식의 인터페이스 선언과 활용이 가능할 것이지만, 현재 아키텍처로 변경하면서 그 의미가 크게 다가왔다. 

 

위에서 하나의 Service와 그 외의 support 클래스를 나눈 것을 보자. 명확한 책임의 분리로 필요에 따라 유연하게 다른 클래스로 대체될 가능성을 열어둔다. 

 

예를 들어 보상에 대한 로직만 변경되는 사항이 있다고 해보자. 기존 Mysql에 db와 연결하던 Compensation은 이제 PostgreSql로 마이그레이션되었다. 이전의 구조라면 Service를 인터페이스로 구현하고 그 구현체를 바꾼다고 하더라도, 해당하는 구현체 자체를 수정해야하는 것이 불가피하다. 그러나 이렇게 세부 기능을 나눠두면 CompensationReader만 교체하면 된다. 

 

 

 

또 하나의 예시는 기능 확장시에 용이할 수 있다는 점이다. 

 

 

예를 들어 Compensation을 Validation해야 하는 로직이 필요하다고 해보자. 이 경우 도메인 레이어를 변경할 필요 없이 단순하게 VocFactory의 코드를 변경해주면 된다. 

 

이 단계에서 추상화 레벨을 한 단계 더 가져가는 것으로 또 한번 인터페이스를 활용할 수 있다. 게다가 DI를 복수형태로 주입한다면 매우 유연한 코드를 작성할 수 있다. 

 

다음과 같은 인터페이스를 도메인 내의 aggregate 패키지에 선언한다. 

 

public interface VocSeriesValidator {
   void validate(Compensation compensation, VocCommand.VocRegisterRequest request);
}

 

이후 구현체들은 Infrastructure 내의 voc 밑의 validator에 구현한다. 

 

 

 

public class AmountValidator implements VocSeriesValidator {
   @Override
   public void validate(Compensation compensation, VocCommand.VocRegisterRequest request) {
      // todo: amount validation
   }
}

 

public class StatusValidator implements VocSeriesValidator {
   @Override
   public void validate(Compensation compensation, VocCommand.VocRegisterRequest request) {
      // todo: status validation
   }
}

 

 

Factory는 다음과 같이 validator들을 참조한다. 

private final List<VocSeriesValidator> vocSeriesValidators;

 

 

이 후에 할일은 이전에 만든 로직에서 검증 로직을 추가해주기만 하면된다. 

 

@Override
public void save(VocCommand.VocRegisterRequest request, ClientCompany clientCompany,
   DeliveryDriver deliveryDriver) {

   Compensation compensation = createCompensation(request);
   vocSeriesValidators.forEach(validator -> validator.validate(compensation, request));
   
   // ... 
}

 

이와 같이 분리된 의존성을 기반으로 쉽게 새로운 기능을 확장하면서도 도메인 레이어를 전혀 변경하지 않고도 쉽게 새로운 기능을 확장할 수 있다. 

 

 

 

 

 

4. 맺음말 

 

기존에 aggregate 패러다임만을 적용했던 MVP 프로젝트를 도메인 중심 설계에 집중하여 다계층의 레이어로 리팩토링하는 내용을 다루어보았다. 

 

결론적으로, DDD 개발 방법론을 적용한 아키텍처는 순수 MVC보다 높은 안정성과 확장성을 만족한다고 생각한다.

 

개발자로서 좋은 코드와 더 나은 설계는 끊임없는 도전의 영역이라고 생각한다. 본인 스스로의 지적인 만족도 동기가 될 것이다. 하지만 궁극적으로는 그렇게 해야 기술력이 향상되고 결과적으로 비즈니스의 성공으로 이어지기 때문일 것이다. 

 

 

 

 


레퍼런스

- 패스트캠퍼스 - 비즈니스 성공을 위한 Java/Spring 기반 서비스 개발과 MSA 구축 by 이희창

- 도메인 주도 설계로 시작하는 마이크로서비스 개발(한정헌, 유해식, 최은정, 이주영 | 위키북스)

- 도메인 주도 개발 시작하기(최범균, 한빛 미디어) 

 

반응형