본문으로 건너뛰기

아키텍처 규칙


목차

  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)