Search
💼

DDD 기반의 아키텍처 학습

들어가기에 앞서

다른 포스팅에서도 얘기했던 것 처럼 서비스가 성장하면서 가장 많이 고민했던 부분 중 하나가 “복잡해지는 비즈니스 로직/도메인 규칙을 어떻게 잘 관리할것인가” 였다.
지금 재직중인 회사에서 보일러플레이트로 삼고 있는 아키텍처에 대한 강의를 접하게 되어 내용을 학습하게 되었는데 DDD 기반의 아키텍처가 상당히 잘 구성되어있어서 도움이 많이 되었다.
이와 관련하여 내용을 정리해보고자 한다.

1. 프로젝트 구조 및 설계

1.1. 좋은 구현이란?

비즈니스 가치를 명확히 충족시켜야 한다.
잘 읽혀야 한다.
별도의 문서 없이 코드 자체로 도메인 파악 필요
코드로 표현할 수 없는 외부 요소들은 별도의 기술 문서로 표현
테스트 코드 작성이 쉬워야 한다.
코드간의 의존성 높음 → 테스트 코드 작성 어려움
테스트 코드 작성이 쉬움 → 코드 품질 높음
변경에 유연해야 한다.

1.2. 도메인 주도 설계

Layer 별 특징과 역할
Layer
Description
Object
Interfaces
사용자에게 정보를 보여주고 사용자의 명령을 해석API, GQL Resolver
Controller, DTO, Mapper(Converter)
application
수행할 작업을 정의, 표현력 있는 도메인 객체가 문제를 해결이 계층은 얇게 유지 - 오직 작업을 조정하고 아래 위치한 계층에 포함된 도메인 객체의 협력자에게 작업을 위임
Facade
domain
• 업무 개념과 업무 상황에 대한 정보, 업무 규칙을 표현하는 일을 책임 • 업무 상황을 반영하는 상태를 제어하고 사용 • 상태 저장과 같은 관련된 기술적인 세부 사항은 infrastructure 에 위임 • 업무용 소프트웨어의 핵심
Entity, Service, Command, Criteria, Info, Reader, Store, Executor, Factory (interface)
infrastructure
상위 계층을 지원하는 일반화된 기술적 기능을 제공애플리케이션에 대한 메시지 전송, 도메인 영속화, UI에 위젯 그리기 등
low level 구현체 (ReaderImpl, StoreImpl, TypeORM, RedisConnector, KafkaProducer)
Layer간 참조 관계
레이어간의 참조관계는 단방향 의존성 유지
계층간의 호출에는 인터페이스를 통한 호출

2. DDD Layer 별 역할 및 구현 상세

2.1. Glossary

[Aggregate]
필요성
모델 내에서 복잡한 연관관계를 맺는 객체를 대상으로 일관된 규칙 보장 어려움
개별 객체만이 아닌 서로 밀접한 관계에 있는 객체 집합에도 변경에 대한 일관된 규칙 유지 필요
이를 위해 Aggregate 구성. Aggregate에 적용되는 불변식은 각 트랜잭션이 완료될 때 이행
특징
Aggregate는 데이터 변경의 단위로 다루는 연관 객체의 묶음
각 Aggregate에는 루트(Root)와 경계(Boundary)가 있음
경계는 Aggregate에 무엇이 포함되고 포함되지 않는지를 정의
루트는 단 하나만 존재. Aggregate에 포함된 특정 Entity 가리킴
각 루트 Entity는 전역 식별성을 가짐
Aggregate 경계 밖에서는 루트 Entity를 제외한 Aggregate 내부의 구성요소를 참조할 수 없음
Aggregate 는 생명주기의 전단계에서 불변식이 유지 되어야할 범위 표시
[Factory & Repository]
필요성
복잡한 객체를 생성하는 것은 도메인 계층의 책임이나, 그것이 모델을 표현하는 객체에 속하는 것은 아님
복잡한 객체와 Aggregate의 인스턴스 생성을 책임지는 별도의 객체를 선언하여 운영하는 것이 필요
특징
자신의 책임과 역할이 다른 객체를 생성하는 것 = Factory
Factory는 해당 Factory에서 만들어내는 객체와 매우 강하게 결합돼있음 → 자신이 생성하는 객체와 가장가까이 있어야함
Factory는 Aggregate에서 유지되어어야할 불변식 로직을 Factory 내에 두어 Aggregate 내에 들어있는 복잡한 요소를 줄일 수 있음
특정 객체나 Aggregate 를 생성하는 일이 복잡해지거나 내부 구조를 너무 많이 드러내는 경우 Factory 사용

2.2. Domain Layer

역할
업무 개념과 업무 상황에 대한 정보, 업무 규칙을 표현하는 일을 책임
업무 상황을 반영하는 상태 제어
상태 저장과 관련된 기술적인 세부사항은 인프라 스트럭처에 위임
업무용 소프트웨어의 핵심
표준 구현
1.
Domain Layer에서의 Service에서는 해당 도메인의 전체 흐름을 파악할 수 있게 구현되어야 함
2.
Domain Layer에서의 모든 클래스명이 xxxService로 선언될 필요는 없음
주요 도메인 흐름을 관리하는 Service는 하나로 유지, 이를 위한 support 역할을 하는 클래스는 Service 이외의 네이밍 사용 권장
하나의 책임을 가져가는 각각의 구현체는 그 책임 및 역할에 맞는 네이밍으로 선언
예) xxxReader / xxxStore / xxxExecutor / xxxFactory / xxxAggregator
다만, 해당 구현체는 Domain Layer에서 interface로 추상화하고, 실제 구현체는 Infrastructure layer에서 구현 (DIP 개념)
Domain Layer - Service / ServiceImpl
Infrastructure Layer - ReaderImpl / StoreImpl / ExecutorImpl
3.
Service 간에는 참조 관계를 가지지 않도록 함
Service 내의 로직은 추상화 수준을 높게 가져감
각 추상화의 실제 구현체를 잘게 쪼갬

2.3. Infrastructure Layer

역할
상위 계층을 지원하는 일반화된 기술적 기능 제공
표준 구현
Domain Layer에 선언되고 사용되는 추상화된 interface를 실제로 구현
runtime시에는 실제 로직을 동작 (DIP)
Domain의 추상화된 interface를 구현하는 레이어로 구현의 자유도 높음
Infrastructure layer 에서의 구현체간의 참조 관계 허용
domain layer에 선언된 interface를 구현하는게 대부분이므로 service에 비해 의존성을 많이 가지지 않음
로직 재활용을 위해 infrastructure 내 구현체를 의존관계로 활용 해도 됨
단, 순환 참조가 발생하지 않도록 적절한 상하관계 정의 필요

2.4. Application Layer

역할
수행할 작업 정의
도메인 객체가 문제를 해결하도록 지시
다른 애플리케이션 계층과의 상호작용 진행
비즈니스 규칙 포함 X. 작업을 조정하며 다음 하위 계층에서 도메인 객체의 협력을 위해 업무 위임 → Layer가 얇음
작업을 조정하기만 하며, 도메인 상태를 가지면 안됨
의문점
수행할 작업을 정의하고 작업을 조정하는게 결국 도메인 로직 아닌가?
다른 애플리케이션 계층과 상호작용 → import & 생성자 인자가 많아질 수 밖에 없지 않나??
표준 구현
역할
transaction으로 묶어야하는 도메인 로직
그 외 로직을 aggregation하는 역할로 한정
네이밍
xxxFacade로 명명
사전적 개념
복잡한 여러 개의 API를 하나의 인터페이스로 aggregation하는 역할
DDD의 개념
서비스 간의 조합으로 하나의 요구사항을 처리하는 클래스

2.5. Interface Layer

역할
사용자에게 정보를 보여주고 사용자의 명령을 해석하는 책임
표준 구현
Req / Res
API를 설계시 없어도 되는 Request parameter 제거
외부에 리턴하는 Response 도 최소한으로 유지
서비스간 통신 기술
http / gRPC, 비동기 메세징과 같은 서비스간 통신 기술은 interface layer에서만 가급적 사용

2.6. Summary

Interface Layer
Controller
Input / Output
Request Dto
Response Dto
Application Layer
Facade
Domain Layer
Service / ServiceImpl
Input / Output
Input
Command - CUD
Criteria - R
Output
Info - Entity Converted Object
Interface
Reader - R 로직
Store - CUD 로직
Executor
Processor
Generator
Factory
Infrastructure Layer
Implementation
ReaderImpl
StoreImpl
ExecutorImpl
ProcessorImpl
GeneratorImpl
Repository

3. 권장 구현 방식

3.1. 개발 디자인 문서

개발 디자인 문서를 선행적으로 작성 후 구현
개발 전 개발 디자인 문서 작성 후 공유
서비스 구현에 필요한 목표, 설계, 제약 사항등을 생각해보고 개발 진행 → 시행착오 최소화
개발 디자인 피어 리뷰 → 더 나은 디자인과 방향성 설정 가능
예시

3.2. 핵심 도메인 도출

핵심 도메인 도출이 테이블 설계보다 먼저 진행되어야한다.
테이블 - 도메인 객체 영속화를 위한 그릇에 불과
우리가 해야할 것
주요 요구사항과 제약조건을 고려
핵심 도메인 객체 도출
특정 기능 수행을 위한 도메인간에 주고 받아야할 메세지 먼저 정의

3.3. 그 외..

변수명, 메서드명에 많이 신경 쓰자
표준, 네이밍 규칙 설정 및 운영
API 명세 최적화
꼭 필요한 프로퍼티만 넣을 것
setter 최대한 지양
캡슐화된 도메인 객체 오염 주범
transaction 사용과 범위 설정에 대한 많은 고민 진행할 것
범위가 작으면 작을수록 좋다…
도메인 객체가 무조건 DB에 저장되지는 않음
try-catch는 필요한 경우가 아니라면 지양하자
exception을 catch 했을 때 추가적인 로직 구현이 필요한 경우에만 선언
try-catch가 필요한 케이스
꼭 필요한 상태만 선언하자
도메인 객체의 상태값은 식별자 만큼이나 중요한 프로퍼티
다만, 너무 세분화된 상태값은 코드 구현의 난이도와 복잡도를 높임

4. 도메인 개발 예시

4.1. Glossary

이해 관계자
유저 - 서비스를 통해 상품을 선택하여 주문하는 고객
파트너 - 해당 서비스에 입점하여 상품을 판매하는 업체
내부 운영자 - 해당 서비스를 운영하고 관리하는 담당자
도메인
파트너 - 파트너 등록과 운영을 처리
상품 - 상품과 상품의 옵션 정보를 등록/관리
주문 - 유저가 선택한 상품/주문 정보를 관리 및 결제
요구사항

4.2. DDD의 Aggregate

예시) 상품 도메인 Entity
Item
ItemOptionGroup
ItemOption
Aggregate Root
Item은 Item 도메인 전체의 Aggregate Root 역할
Aggregate Root 경계 밖에서는 Aggregate 내부 구성요소를 참조할 수 없음
Item을 획득하면 Aggregate 내부의 객체를 탐색해서 획득할 수 있게 됨
Item Aggregate 내부에서는 데이터가 변경될때마다 유지되어야하는 일관된 규칙이 지켜져야한다.
일관된 규칙은 Aggregate Root에 적용되는 모든 트랜잭션 내에서 지켜져야한다. → 규칙이 깨지면 트랜잭션 롤백이 발생
Factory
복잡한 객체와 Aggregate 인스턴스 생성에 책임
객체를 데이터베이스와 같은 저장소에 영속화 하는것의 역할은 포함하지 않음. 해당 역할은 Repository가 수행

4.3. Service 및 Implements 구현

도메인 로직과 Service의 역할
코드를 통해 해당 도메인의 전체 흐름 파악 필요
세세한 구현과 low level 기술은 implements (infrastructure)에 위임, 위임을 맡기는 구간은 interface로 정의해서 사용
Service 간에는 참조 관계 두지 않음
PartnerService<I>에서 제공해야할 요구사항
파트너 등록
파트너 정보 조회
파트너 활성화/비활성화
Command, Criteria, Info 객체
Command, Criteria
Service 메서드 처리와 조회를 위한 파라미터
Command: CUD
Criteria: R
Info
리턴 객체
Database에서 조회하여 가져온 Entity를 그대로 리턴 하지 않기 위한 객체
도메인 로직의 리턴값으로 Entity를 그대로 리턴하지 않음
도메인 로직의 리턴 값으로 Entity를 그대로 전달하면 domain layer 바깥에서 entity 내의 도메인 로직이 호출되거나 상태가 변경되는 명령어가 실행될 수 있음
→ 도메인 로직을 domain layer에 응집하고자 하는 의도와 맞지 않음
Info 객체는 필요에 따라 Entity의 일부 속성을 가공할 수 있음
파트너 Entity가 제공하는 기능과 세부적인 기술을 제공하는 Implements 와의 조합으로 PartnerService의 구현체 만듦
Partner 도메인의 조회와 저장을 담당하는 interface 각각 선언
PartnerReader - 조회
PartnerStore - 저장
ItemService<I>에서 제공해야할 요구사항
상품 등록
상품 정보 조회
상품 판매 상태 변경 (가능/불가능)
상품 등록
Item Aggregate 생성을 위한 Factory 구현
예) 복잡한 객체 생성 로직을 ItemOptionSeriesFactory 에서 구현 → 도메인 서비스에서 적절한 추상화를 통해 가독성 유지

4.4. Application 및 Interfaces 구현

Facade (Application layer) 구현
파트너 등록 시, 등록 성공 후 해당 파트너에게 이메일로 등록 성공 알림을 보내는 요구사항
파트너 등록 과정의 모든 도메인 로직을 하나의 transaction으로 묶어야 정합성 이슈 없음
이메일 발송의 경우 발송에 실패하더라도 파트너 등록에 정상적으로 이루어졌다면 큰 문제가 안됨 (요구사항에 대한 판단과 해석은 관련 팀과 협의하면서 진행)
응용 계층을 하나 더 둠으로써 도메인 계층에서 처리하기 애매한 요구사항 충족할 수 있는 여유가 생김
필요에 따라 여러 개의 Info를 조합하여 만드는 Result 객체 필요할 수 있음
이때 여러개의 Service를 호출하여 이를 조합하는 Result 생성 로직 생길 수 있음
Controller (Interfaces) 구현
사용자에게 정보를 보여주고 사용자에게 명령을 해석하는 책임
서비스 전체의 시스템간의 인터페이스 표준 정의, 그에 맞게 외부 호출과 응답 정의되도록 구현
외부 서비스의 요청과 응답은 내부 도메인 로직을 처리하는 layer와 명확히 분리되어야 한다
interface layer에서 사용할 dto를 정의하고 dto가 domain layer로 침투되지 않게 유의
이를 위한 convert 로직 필요
request dto → domain의 command/criteria
domain info → response dto
변환 로직을 dto 내에 구현하는 것은 응집도를 높일 수 있음
인터페이스 계층에서만 사용할 dto를 정의
static class 적극 활용
validation 정의
Facade를 호출하고 결과를 받아 API 응답에 맞게 변환

5. Reference