본문으로 건너뛰기

아키텍처 규칙


목차​

  1. 개요
  2. 레이어 정의 및 책임
  3. 코드 위치 가이드
  4. 네이밍 컨벤션
  5. 허용/금지 규칙
  6. CQRS 패턴 가이드
  7. Domain Event 가이드
  8. Value Object 패턴
  9. Aggregate 패턴

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 Rootdomain{name}.aggregate.tsuser.aggregate.ts
Entitydomain{name}.entity.tsuser-cycle.entity.ts
Value Objectdomain{name}.vo.tsemail.vo.ts
Repository Interfacedomain{name}.repository.tsuser.repository.ts
Commandapplication (libs/shared){action}-{entity}.command.tscreate-user.command.ts
Command Handlerapplication{action}-{entity}.handler.tscreate-user.handler.ts
Queryapplication (libs/shared)get-{entity}.query.tsget-user.query.ts
Domain Eventapplication{entity}-{action}.event.tsuser-created.event.ts
Repository Implinfrastructure{name}.repository.tsuser.repository.ts
Mapperinfrastructure{name}.mapper.tsuser.mapper.ts

4.2 클래스명 규칙​

유형패턴예시
Aggregate Root{Name}User
Value Object{Name}Email, UserId
Command{Action}{Entity}CommandCreateUserCommand
Handler{Action}{Entity}HandlerCreateUserHandler
Domain Event{Entity}{Action}EventUserCreatedEvent
Repository InterfaceI{Name}RepositoryIUserRepository

5. 허용/금지 규칙​

5.1 의존성 방향 (Dependency Rule)​

의존성은 항상 안쪽 방향(Domain Layer 방향)으로만 (저수준 -> 고수준) 허용됩니다.

Presentation → Application → Domain ← Infrastructure
레이어참조 가능 ✅참조 금지 ❌
PresentationApplication, Domain (DTO용)Infrastructure
ApplicationDomainInfrastructure, Presentation
Domain없음 (자기 자신만)Application, Infrastructure
InfrastructureDomain (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 ObjectEntity
정체성값으로 식별ID로 식별
불변성불변(Immutable)가변(Mutable)
동등성모든 속성이 같으면 같음ID가 같으면 같음
생명주기독립적이지 않음독립적
예시Money(100, KRW), EmailUser, Order

8.2 Value Object 구현 규칙​

  1. 불변성 보장: 생성 후 변경 불가 (setter 금지)
  2. 생성 팩토리: 정적 factory 메서드로 생성
  3. 유효성 검증: 생성 시점에 유효성 검증 수행
  4. 동등성 비교: 모든 속성이 같으면 같은 객체로 취급
  5. 자체 비즈니스 로직: 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 구현 규칙​

  1. Root를 통한 접근만: 외부에서는 Aggregate Root를 통해서만 내부 객체에 접근
  2. 경계 내 일관성: Aggregate 내의 모든 불변식(Invariant) 보장
  3. 트랜잭션 단위: 보통 하나의 Aggregate Root = 하나의 트랜잭션
  4. 이벤트 발행: 비즈니스 로직 실행 후 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의 다중 Aggregate3️⃣ Domain Service강한 일관성, TXSleepLog + 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)