개발이군고구마

[전전긍긍 설정세팅] 7. DDD와 클린 아키텍처 (#디렉토리 #공통규약) 본문

SERVER/Architecture

[전전긍긍 설정세팅] 7. DDD와 클린 아키텍처 (#디렉토리 #공통규약)

김구황 2025. 5. 29. 22:34
728x90

지금까지 과연 나는 개발을 하면서, 기술의 도입 이유와 아키텍처 선정 방식에 대해 고민한 적이 있었나. 

클래스를 의례 디렉토리에 생성한 후 쾡한 눈으로 의존성 주입을 반복하지 않았나? 

 

최근 아키텍처 학습을 하나 둘 진행하며 디렉토리 구조도 역시 아키텍처의 반영이라는 사실을 뒤늦게 깨달았다. 

무엇보다  아티클 하나를 읽고 지금까지의 나의 학습 패턴에 대해 깊은 반성을 하게 되었는데, 

 

문제를 해결하기 위해 선택한 기술과 아키텍처,
그리고 그 선택의 근거
 

 

회사 성격상 오너쉽을 가지고 일하기가 어려운 상황도 있지만, 그래도

정작 가장 중요한 질문에 대해 생각해보지도 않은채 경력 2-3년을 낭비해왔다. (아까운 시간이여...)

난 그냥 막 아무데나 클래스 생성하면 되는 줄 알았지 뭐..........

 

 

이참에 아키텍처 설계의 근간이 되는 방법론들을 하나하나 짚어보며,

이 방법론을 선택한 이유와 그렇게 해서 나온 결과물들을 설명하고자 한다. 


1. 개념 

1) DDD

비즈니스 도메인을 기반으로 생각을 하는 설계
복잡한 비즈니스 도메인의 본질을 이해하고, 그 이해를 소프트웨어 모델에 직접 반영

 

* Model (시뮬레이션의 현실 반영 결과) 를 중심으로 개발을 하자 에서 나온 방법론

(model - db와 연관되자 -> domain으로 이름 변경) 

 

🌐 구조 : 도메인 > 컨텍스트 >(어그리게이트 > 루트 어그리게이트 + 객체) / 서비스 / 리파지토리 

▶ (바운디드 컨텍스트) 구분되는 경계를 갖는 컨텍스트 용어를 기준으로 구분 (예, 대여회원, 주문회원) 

[Bounded Context: 주문(Order) 컨텍스트]
    ├── Entity: Order, Customer, Product
    ├── Value Object: Address, Money
    ├── Aggregate: OrderAggregate
    ├── Domain Service: OrderPolicy, DiscountCalculator
    ├── Repository: OrderRepository
    └── 도메인 이벤트: OrderPlaced, OrderCancelled

 


❓ context와 bounded context의 의미 구분 

Domain (도메인)
│
├── Bounded Context (바운디드 컨텍스트)  ← 여러 개 존재할 수 있음
│       │
│       └── Aggregate (애그리거트)   ← 여러 개 존재할 수 있음
│
└── Bounded Context ...
Domain Bounded Context 비즈니스 전체에서 서로 의미가 다른 하위 경계로 분리
Bounded Context Aggregate 각 경계 내에서 트랜잭션 일관성이 필요한 객체 묶음
Aggregate Entity/Value 루트 엔티티와 관련된 하위 객체들

 

  • Domain: 회사 전체 (예: “온라인 서점” 회사)
  • Bounded Context: 각 부서(“주문팀”, “결제팀”, “배송팀”)
  • Aggregate: 부서 내에서 단위로 움직이는 일 처리 묶음(“주문 1건 처리”, “결제 1건”, “배송 1건”)
  • Context: 용어와 규칙이 쓰이는 환경(각 부서별 상황, 또는 어떤 이야기를 하는지에 따라 용어 의미가 달라짐)

 

❓ domain 과 aggregate> aggregate 구성 요소들 구분

  • aggregate는 한 번에 하나의 트랜잭션으로 변경되어야 하는 객체들의 집합
    • "주문" aggregate라면 (root entity)
    • 주문 자체(order)와 주문 내역(order items), 배송 정보(shipping info) 등이 하나로 묶여 aggregate를 이룸 (해당 객체들은 VO로 정의해돌 수 있겠다) 
  • 도메인은 우리가 해결하고자 하는 비즈니스의 문제 영역 전체
    • 도메인 안에는 여러 하위 개념(예: 주문, 결제, 배송 등)
   Domain(도메인)  Aggregate(애그리거트)
의미 비즈니스의 문제 영역 전체 도메인 내에서 일관성 보장되는 객체의 묶음
범위 넓음 (비즈니스 전체) 좁음 (한 트랜잭션 단위)
목적 비즈니스 전체를 이해하고 모델링 데이터의 일관성과 트랜잭션 경계 정의
예시 온라인 서점, 배달 서비스, 결제 시스템 주문, 장바구니, 계좌 등

 

 

2) 클린 아키텍처

관심사의 분리(Separation of Concerns)를 통해 유지보수성이 높고, 테스트하기 쉬우며, 기술 변화에 유연하게 대응할 수 있는 시스템

그 심장(도메인) 을 안전하게 보호하고 다른 장기들과 효과적으로 연결하는 뼈대와 혈관 시스템

즉, "어떻게 계층을 나눌 것인가"에 대한 구조

 

 

 


2. 적용

1. DDD를 기반으로 논리적 '패키지'를  나눈다

- MSA를 사용 X , Monolithic 방법 사용 

- 패키지 모듈 단위에서만 명목상 context를 분리시켜놓는 방법

- 모놀리식 구조이기 때문에  global 영역이나 infra 영역 등의 패키지에서 코드를 공유함   

 

🤔 정말로 필요할까?

MSA 를 도입하기엔 현재 한 컨텍스트 당 가지고 가능 기능이 많지 않다. 

사이드 프로젝트 특성상 PG 를 연결하지도 않고, 배송/물류 시스템 연결도 하지 않는다. 

공통 로직은 AWS 관련 (S3, RDS) 정도인데 각 콘텍스트별로 서버를 하나씩 만드는 것은 금전적인 제약도 있다. 

 

🤔 다른 방법은 없을까?  

콘텍스트로 구분하지 않고는 각각의 모듈을 나누는 방법은 쉽지 않아보인다. 

우선 디렉토리(패키지 구조)는 도메인 중심 > 논리적 컨텍스트 로 잡고 가는 것이 가장 일반적이고 쉬운 방법이라 생각한다. 

 

 

2. 뼈대(프레임)는 클린 아키텍처를 기반으로 쌓아올린다. 즉, 계층적 설계로 비즈니스 로직을 보호한다. 

- bounded context 안 구분 속성들을 연결하기 위해서 클린 아키텍처의 사상을 가지고 옴 

 

 

3. 헥사고날 아키텍처의 일부를 도입하여 adapter port 활용하여 외부 의존성을 모듈화 시킨다. 

- 즉, inport/outport 를 두고 controller나 interface (외부모듈) 를 adapter 역할로 사용 

- 서비스 레이어는 컨텍스트 마다 별개로 가지고 가기 때문에 inport를 따로 인터페이스화 하지 않고 web adapter (controller) 를 사용

- outport 는 공유하고 있는 컨텍스트가 있기 때문에 인터페이스화 하였음 

🤔 정말로 필요할까? 

서비스 단에 파일 업로드 기능과 같이 외부와 연동하는 부분에서 클래스가 너무 의존하게 되면 테스트가 어렵다. 

outport 인터페이스로 만들게 되면 서비스 테스트 코드 작성시에 mock으로 주입받고, verify 정도로 단위테스트가 가능하다. 

DDD 도메인 모델을 효과적으로 구현할 수 있는 구조를 제공 

 

🤔 다른 방법은 없을까?  

어플리케이션 - 프리젠테이션 - 도메인 등의 각 계층의 결합이 너무나 강력하여 테스트 하기가 쉽지 않다. 

TDD 로 하나씩 테스트를 하게 될때에도 코드를 실제 작성해주지 않으면 결국 테스트가 어렵게 된다. 

인터페이스화 하여 비즈니스 로직을 다른 부분들과 느슨하게 결합하는 것이 더 좋겠다는 판단. 

 

 

 

4. command 와 query연산은 패키지에서 따로 구분하지 않는다. 

조회 쿼리에서 복잡성이 높을 만큼 현재 큰 규모의 프로젝트가 아니다. 

또한 DB는 한 개를 가지고 있고, 조회용 캐시 서버를 두고 있지도 않은 상태이기 때문에 오히려 나누는 것이 더 복잡성이 증가할 것이라는 판단.

하나의 컨트롤러에서 command query를 함께 쓴다. 

또한 조회에서는 컨트롤러 (저수준) ↔ outport (저수준) 을 바로 조회하는 것도 허용한다. 

 

 

👋🏻 여기서 잠깐 문법 학습

  CQRS

- command 연산은 헥사고날 아키텍처 즉, 도메인에게 트랜잭션을 주고 싶을 때 사용할 수 있지만 

- query 연산은 데이터를 읽어오는 infra 에 따라 그에 맞는 구조를 채택할 수 있다. 

(조회하고자 하는 쿼리의 복잡성이 크고, 여기서는 빠르게 데이터를 읽어오는 것이 중요하기 때문) 

- application - query / command로 나누어서 controller, dto 등을 따로 두는 방법 

  • 읽기/쓰기 책임 분리
    • 읽기 로직(Read)을 최적화된 스키마(예: 잘 정규화되지 않은 뷰, 캐시, NoSQL 등)로 처리하고,
    • 쓰기 로직(Write)은 도메인 규칙을 강하게 적용한 객체 모델로 처리하므로 성능 및 도메인 복잡도 관리가 수월해진다.
  • 성능(Performance) & 확장성(Scalability)
    • 읽기 트래픽이 폭주할 때, 읽기 전용 DB나 캐시를 수평 확장하기 쉽다.
    • 쓰기 트래픽이 많은 경우, 쓰기 모델만 따로 스케일하거나 최적화할 수 있다.
  • 유지보수성(Maintainability) 향상
    • 커맨드 핸들러(CommandHandler)는 순수하게 “도메인 로직 + 변경”만 담당하고,
    • 쿼리 핸들러(QueryHandler)는 순수하게 “조회”만 담당하므로 코드가 더 명확해진다.
  • 도메인 이벤트 활용
    • 쓰기 모델에서 발생한 주요 이벤트(예: 주문 생성, 결제 완료 등)를 별도의 이벤트로 발행하고, 이를 구독해서 읽기 모델을 업데이트하면 시스템 상태 추적이 쉬워진다.

 

 

 

🤔 정말로 필요할까? 

query와 command를 완전 별개로 두어서 배포하는 것은 api가 많지 않은 상황에서 굳이 필요없겠다는 생각. 

하지만 확장성 측면에서는..? 서비스가 커지면 결국 분리하는 게 맞을 수도 있다. 

 

 

🤔 다른 방법은 없을까?  

대안1. 모놀리식 방법에서 패키지안 모듈 분리라도 할 수 있는 방법이 있음. 

즉, 

src/
  └─ controller/
     ├─ command/    ← 쓰기 로직 전용 패키지
     └─ query/      ← 읽기 로직 전용 패키지

 

대안2. 공통으로 쓰는 command query 가 많다면 공통 컴포넌트 어플리케이션을 따로 두고 implement 하는 방법 

 


3. 최종

클린아키텍처를 기준으로 디렉토리를 분리하지 않았음. 

컨트롤러/repository 는 기능을 따라 패키지로 분리하되, 저수준 계층을 연결하는 객체들은 'outport' 용으로 따로 뽑아둠 

 

1) domain < DDD 의 영향 (Bounded context의 논리적 패키징 구분)

2) global < 모놀리식의 잔재 (context가 공통으로 쓰는 부분)

3) infra < clean architecture 지향 (저수준 즉, DB 연결이나 외부 시스템 연동 부분에 관한 로직) 

domain - 시스템 전체 
	ㄴcommon 
		모듈별로 공통으로 사용하는 파일들
		BaseEntity 
		
		ㄴoutport
			모듈들이 공통으로 외부의 서비스를 이용하게 도와주는 인터페이스 
	ㄴmember - 모듈
		ㄴdomain - domain 레이어임 
			entity 객체 (root aggregate)
			vo (aggregates)
		ㄴcontroller - inport 
		ㄴexception 
			예외 메세지 정리
			단위테스트 필요
		ㄴoutport
			외부 모듈과 연관되는 인터페이스 
		ㄴrepository
			데이터 전달 메서드 
		ㄴservice 
			비즈니스 로직 
			단위테스트 필요
		ㄴutils
			모듈별 필요한 validate 클래스 등이 정의 됨 

global - 전체 모듈이 함께 쓰는 설정
	ㄴapi
		api 응답 클래스 
	ㄴerror
		ExceptionHandler
		ㄴdto
			error response
		ㄴexception
			custom exception 정의 
			ExceptioMessage
	ㄴutils	
		공통으로 사용하는 utils (예, 변환등)

infra - 테스트가 필요 없는 데이터 클래스 
	ㄴdatabase
		ㄴconfig
			각종 설정 jpa config , swagger config, queryDsl 
	ㄴstorage
		S3 관련 구현 클래스

 

 

 

 


https://velog.io/@daehoon12/DDD-Domain-Model-Aggregate-Bounded-Context (DDD)

https://velog.io/@s2moon98/Event-Storming%EC%9D%84-%EC%8B%9C%EB%8F%84%ED%95%B4%EB%B3%B4%EC%9E%90 (DDD EventStorming)

https://www.msaschool.io/operation/design/design-three/ (MSA)

https://custom-li.tistory.com/207 (Event Storming)

https://choo.oopy.io/d1668834-2724-4400-ab7e-7099516c9c89 (clean architecture / hexagonal)