아키텍처 규칙
목차
1. 개요
본 문서는 DDD(Domain-Driven Design), CQRS(Command Query Responsibility Segregation), Clean Architecture 패턴을 NestJS + Nx 모노레포 환경에서 적용하기 위한 개발 가이드입니다.
모든 개발자는 이 문서를 기준점으로 활용하고, 이 규칙을 준수하여 일관된 코드 구조를 유지해야 합니다.
2. 레이어 정의 및 책임
2.1 레이어 구조
┌─────────────────────────────────────┐
│ Presentation Layer │ apps
├─────────────────────────────────────┤-------------
│ Application Layer │
├─────────────────────────────────────┤
│ Domain Layer │ libs/feature
├─────────────────────────────────────┤
│ Infrastructure Layer │
├─────────────────────────────────────┤--------------
│ Shared Layer │ libs/shared
└─────────────────────────────────────┘
2.2 Presentation Layer
위치: apps/
- HTTP 요청/응답 처리 (REST Controller)
- 입력 유효성 검증 (class-validator)
- Application Layer로 Command/Query 위임
- Request/Response DTO 변환
2.3 Application Layer
위치: libs/feature/*/application/
- Use Case 오케스트레이션
- Command/Query Handler 구현
- Application Service 정의
- 트랜잭션 경계 관리
- Domain Event 발행 조율
2.4 Domain Layer
위치: libs/feature/*/domain/
- 핵심 비즈니스 로직 (순수한 도메인 규칙)
- Aggregate Root, Entity, Value Object 정의
- Domain Service, Domain Event 정의
- Repository Interface 정의 (구현체 X)
2.5 Infrastructure Layer
위치: libs/feature/*/infrastructure/
- Repository 구현체 (TypeORM, Prisma 등)
- External Service 연동 (API, SDK)
- Cache 연동 (Redis)
- Mapper 정의
2.6 Shared Layer
위치: libs/shared/contract-*/
- Command/Query 정의
- enum 정의
- domain-results 정의
3. 코드 위치 가이드
3.1 디렉토리 구조
apps/
└── api/ # Presentation Layer
└── src/
├── controllers/ # REST Controllers
└── dto/ # Request/Response DTOs
libs/
feature/
└── {domain-name}/ # Bounded Context
| ├── application/ # Application Layer
| │ ├── commands/ # Command Handler
| │ ├── queries/ # Query Handler
| │ ├── events/ # Event Handlers (구독)
| │ └── services/ # Application Services
| ├── domain/ # Domain Layer
| │ ├── aggregates/ # Aggregate Roots
| │ ├── entities/ # Entities
| │ ├── value-objects/ # Value Objects
| │ ├── repositories/ # Repository Interfaces
| │ └── services/ # Domain Services
| └── infrastructure/ # Infrastructure Layer
| ├── repositories/ # Repository 구현체
| ├── entities/ # ORM Entities
| └── mappers/ # Entity <-> Domain 매퍼
shared/
└── contracts-{domain-name}/
├── commands/ # Command
├── queries/ # Query
├── interfaces/ # Result(Dto)
├── enums/ # Enum
└── events/ # Domain Events (정의)
4. 네이밍 컨벤션
4.1 파일명 규칙
| 유형 | layer | 패턴 | 예시 |
|---|---|---|---|
| Aggregate Root | domain | {name}.aggregate.ts | user.aggregate.ts |
| Entity | domain | {name}.entity.ts | user-cycle.entity.ts |
| Value Object | domain | {name}.vo.ts | email.vo.ts |
| Repository Interface | domain | {name}.repository.ts | user.repository.ts |
| Command | application (libs/shared) | {action}-{entity}.command.ts | create-user.command.ts |
| Command Handler | application | {action}-{entity}.handler.ts | create-user.handler.ts |
| Query | application (libs/shared) | get-{entity}.query.ts | get-user.query.ts |
| Domain Event | application | {entity}-{action}.event.ts | user-created.event.ts |
| Repository Impl | infrastructure | {name}.repository.ts | user.repository.ts |
| Mapper | infrastructure | {name}.mapper.ts | user.mapper.ts |
4.2 클래스명 규칙
| 유형 | 패턴 | 예시 |
|---|---|---|
| Aggregate Root | {Name} | User |
| Value Object | {Name} | Email, UserId |
| Command | {Action}{Entity}Command | CreateUserCommand |
| Handler | {Action}{Entity}Handler | CreateUserHandler |
| Domain Event | {Entity}{Action}Event | UserCreatedEvent |
| Repository Interface | I{Name}Repository | IUserRepository |
5. 허용/금지 규칙
5.1 의존성 방향 (Dependency Rule)
의존성은 항상 안쪽 방향(Domain Layer 방향)으로만 (저수준 -> 고수준) 허용됩니다.
Presentation → Application → Domain ← Infrastructure
| 레이어 | 참조 가능 ✅ | 참조 금지 ❌ |
|---|---|---|
| Presentation | Application, Domain (DTO용) | Infrastructure |
| Application | Domain | Infrastructure, Presentation |
| Domain | 없음 (자기 자신만) | Application, Infrastructure |
| Infrastructure | Domain (Interface 구현) | Application, Presentation |
5.2 Import 규칙
✅ 허용되는 Import
// Application → Domain
import { Order } from '../domain';
// Domain에서 Interface 정의
export interface IOrderRepository { ... }
// Infrastructure에서 Interface 구현
export class OrderRepository implements IOrderRepository { ... }
❌ 금지되는 Import
// Domain → Infrastructure (절대 금지!)
import { OrderRepository } from '../infrastructure';
5.3 결정 규칙
Command Handler 구조 결정
-
배경: Command Handler에서 Repository를 직접 호출 할지, Application Service를 경유할지 결정
-
검토:
-
option A:
-
option B:
-
-
결정: 로직의 복잡성 및 재사용 여부에 따라 결정한다. (option A)
-
이유
- 단순한 경우 불필요한 레이어 제거로 코드 복잡도 감소
- 재사용 가능한 로직은 Service를 통해 트랜잭션 관리와 조율 로직 캡슐화
에러 처리 전략
-
배경: 각 레이어에서 try-catch를 어디에서 사용할지, 예외를 어떻게 처리할지 결정이 필요.
-
검토:
- option A: 모든 레이어에 try-catch
- option B: 레이어별 역할 분담
-
결정: 레이어별 역할 분담 (option B)
- Domain Layer: 도메인 예외 정의 & 도메인 예외 throw
- Infrastructure Layer: 도메인 예외로 변환 후 throw
- Presentation Layer: Global Filter 처리 (HttpStatus Error)
-
이유:
- 불필요한 try-catch 제거로 코드 간결화
- 예외 흐름이 명확 (아래에서 위로 전파)
- 도메인 예외로 통일하여 상위 레이어가 인프라 세부사항을 알 필요 없음.
합성 Feature의 외부 도메인 의존 방식
-
배경: 여러 도메인 데이터와 로직을 합성해서 처리해야할 경우 어떻게 로직을 참조할지 결정이 필요.
-
검토:
- option A: Presentation Layer에서 여러 Feature 조합
- option B: 합성 feature 도메인을 생성하고 Infrastructure에서 외부 도메인 Repository 참조
- option C: 합성 feature 도메인을 생성하고 Shared를 통한 외부 도메인 Application Service 참조
-
결정: 합성 도메인은 Shared에 정의된 Service Interface를 통해 외부 도메인의 Application Service를 참조한다. (option C)
- 의존 흐름
┌─────────────────────────────────────┐
│ Composite Feature │
│ (user-management) │
└──────────────┬──────────────────────┘
│ 의존 (인터페이스만)
▼
┌────────────────────────────────────────┐
│ libs/shared/contract-user/interfaces/ │
│ └── IUserService │
│ libs/shared/contract-site/interfaces/ │
│ └── SiteServicePort │
└──────────────┬──────────────---────────┘
▲ 구현
│
┌──────────────┴──────────────────────────┐
│ libs/feature/user/application/services │
│ └── UserService │
│ │
│ libs/feature/site/application/ │
│ └── SiteService │
└─────────────────────────────────────────┘ -
이유:
- 외부 도메인의 Application Service 로직을 재사용하여 중복 방지
- 인터페이스를 통한 의존으로 Feature 간 느슨한 결합 유지
- 단일 도메인 Feature의 내부 구현이 캡슐화 됨
6. CQRS 패턴 가이드
정리: CQRS Implementation Patterns
7. Domain Event 가이드
8. Value Object 패턴
8.1 Value Object란?
Value Object(VO) 는 개체의 정체성(identity)이 중요한 것이 아니라 값 자체가 중요한 도메인 개념입니다.
Value Object vs Entity 비교
| 구분 | Value Object | Entity |
|---|---|---|
| 정체성 | 값으로 식별 | ID로 식별 |
| 불변성 | 불변(Immutable) | 가변(Mutable) |
| 동등성 | 모든 속성이 같으면 같음 | ID가 같으면 같음 |
| 생명주기 | 독립적이지 않음 | 독립적 |
| 예시 | Money(100, KRW), Email | User, Order |
8.2 Value Object 구현 규칙
- 불변성 보장: 생성 후 변경 불가 (setter 금지)
- 생성 팩토리: 정적 factory 메서드로 생성
- 유효성 검증: 생성 시점에 유효성 검증 수행
- 동등성 비교: 모든 속성이 같으면 같은 객체로 취급
- 자체 비즈니스 로직: VO가 자신의 규칙을 알고 있음
9. Aggregate 패턴
9.1 Aggregate란?
Aggregate는 관련된 객체들의 집합으로, 단일 단위로 취급되어야 하는 도메인 개념입니다.
- Aggregate Root: Aggregate 외부에서 접근하는 유일한 진입점
- 일관성 경계: Aggregate 내의 모든 비즈니스 규칙을 보장
- 트랜잭션 경계: 보통 하나의 Aggregate = 하나의 트랜잭션
Aggregate의 특징
┌─────────────────────────────────────┐
│ Order (Aggregate Root) │ ← 외부에서 접근
├─────────────────────────────────────┤
│ - orderId: OrderId (VO) │
│ - customerId: CustomerId (VO) │
│ - items: OrderItem[] (Entity) │ ← 내부에서만 접근
│ - totalAmount: Money (VO) │
│ - status: OrderStatus (VO) │
│ - createdAt: Date │
├─────────────────────────────────────┤
│ 메서드: │
│ + create(...) │
│ + addItem(...) │
│ + removeItem(...) │
│ + confirm() │
│ + cancel() │
└─────────────────────────────────────┘
9.2 Aggregate Root 구현 규칙
- Root를 통한 접근만: 외부에서는 Aggregate Root를 통해서만 내부 객체에 접근
- 경계 내 일관성: Aggregate 내의 모든 불변식(Invariant) 보장
- 트랜잭션 단위: 보통 하나의 Aggregate Root = 하나의 트랜잭션
- 이벤트 발행: 비즈니스 로직 실행 후 Domain Event 발행
의존성 관계도
Application Layer (Handler)
│
├─→ 사용 (주입받음)
│
↓
Domain Layer
├── Repository Interface (IUserCycleRepository)
└── Aggregate (UserCycle)
↑ (구현/의존)
│
Infrastructure Layer
├── Repository Impl (IUserCycleRepository 구현)
├── Mapper (ORM ↔ Domain 변환)
└── ORM Entities (Prisma 기반)
핵심 원칙:
- Domain: Repository Interface만 정의 (구현 X, Mapper 모름)
- Infrastructure: Repository 구현체 + Mapper 함께 배치 (같은 디렉토리 또는 근처)
- Application: Domain의 Repository Interface만 주입받음 (Mapper와 ORM은 완전히 숨김)
이렇게 설계하면 Domain Layer는 완전히 순수하고, Infrastructure 계층의 변경(Mapper 수정, ORM 변경)이 Domain Layer에 영향을 주지 않습니다.
특징:
✓ 읽기 목적: Plan 정보를 확인하지만 **수정하지 않음**
✓ 느슨한 결합: SleepLog는 Plan ID만 알고 있음
✗ 수정은 금지: Plan.update() 절대 금지!
패턴 선택 가이드
| 상황 | 추천 패턴 | 이유 | DTA Wide 예시 |
|---|---|---|---|
| 다른 BC의 Aggregate 수정 | 1️⃣ Domain Event | 느슨한 결합, 확장성 | UserCycle 완료 → User 기록 |
| 다른 BC의 Aggregate 읽기 | 2️⃣ Repository (ID) | 단순, 즉시 일관성 | SleepLog 생성 → Plan 활성화 확인 |
| 같은 BC의 다중 Aggregate | 3️⃣ Domain Service | 강한 일관성, TX | SleepLog + DailySleepMetrics 완료 |
| 최종 일관성 허용 | 1️⃣ Domain Event | 비동기, 확장성 | Sleep 완료 → User 통계 갱신 |
| 실시간 동기화 | 2️⃣ Repository | 즉시 반영 | Plan 상태 확인 후 SleepLog 기록 |
| 복잡한 도메인 규칙 | 3️⃣ Domain Service | 규칙 명시 | UserCycle 상태 + SleepLog 메트릭 검증 |
요약: Aggregate 간 통신의 3가지 원칙
| 원칙 | 설명 |
|---|---|
| 🔸 ID로 참조 | Aggregate 객체가 아니라 ID만 저장 |
| 🔸 Event로 통지 | 다른 BC는 Event로 상태 변경 통지 |
| 🔸 Repository로 조회 | 읽기만 필요하면 Repository 직접 사용 |
이 3가지를 지키면 Aggregate는 느슨하게 결합되면서도 도메인 규칙은 명확하게 유지됩니다.
📌 참고: 이 문서는 팀의 아키텍처 결정에 따라 지속적으로 업데이트됩니다.
비고: 아키텍처 규칙 준수 개선 필요 (claude-code 리뷰)
- 1.1 의존성 역전 (user, auth, time-machine, questionnarie ...)
- 1.2 layer 불분명 (questionnarie)
- 1.3 time machine mapper 위치 이동 (application -> infrastructure)