


도메인 주도(DDD) .NET 모듈을 계층과 공개 API를 깨지 않고 만들기
도메인 주도 설계로 만든 모듈식 .NET 앱을 AI가 작성하거나 수정할 때 사용한다. 기존 방식을 따르고, 계층 간 의존을 한 방향으로 유지하며, 애그리게이트·리포지토리·DTO를 올바른 계층에 두고, 지원하는 모든 데이터베이스에서 기존 사용자에게 영향이 없는지 변경을 테스트한다.
이 AI 규칙을 사용하려면
## 기존 방식을 따르고 계층 간 의존을 한 방향으로 유지하기
코드를 만들기 전에 리포지토리에서 비슷한 구현을 검색하세요. 요청된 변경이 명시적으로 대체하지 않는 한 기존 프로젝트 구조, 명명, 종속성, 테스트, 확장 패턴을 따르세요. diff를 대상에 집중시키고 부수적인 포맷 변경, 리팩터링, 종속성 업그레이드를 피하세요.
단방향 종속성을 갖는 명시적인 계층을 사용하세요.
- **Domain.Shared:** 모든 계층과 외부 클라이언트에서 안전하게 사용할 수 있는 상수, enum, 현지화 리소스, 이벤트 계약, 공유 타입.
- **Domain:** 엔터티, 애그리게이트 루트, 리포지토리 계약, 도메인 서비스, 비즈니스 규칙.
- **Application.Contracts:** 애플리케이션 서비스 인터페이스와 데이터 전송 객체.
- **Application:** 애플리케이션 서비스 구현과 사용 사례 오케스트레이션.
- **Persistence integrations:** 도메인에 종속되는 프로바이더별 컨텍스트, 매핑, 리포지토리 구현.
- **HTTP API:** 구현이 아니라 애플리케이션 계약에 종속되는 전송 어댑터.
- **Remote client:** 애플리케이션 계약에 종속되는 자동 생성 또는 수동 작성 프록시.
- **Web UI:** HTTP API를 사용하는 프레젠테이션 코드.
웹, 전송, 인증 컨텍스트, ORM 관련 사항을 도메인에 포함하지 마세요. 애플리케이션 로직을 웹 계층에서 독립적으로 유지하세요. 구현이 짧아지더라도 금지된 참조는 아키텍처 결함으로 취급하세요.
공개 API와 패키지 경계를 보존하세요. 명시적으로 요청되고, 정당화되고, 마이그레이션이 제공되고, 호환성 테스트로 다뤄지지 않은 한 호환성을 깨는 변경을 도입하지 마세요.
## 애그리게이트를 처음부터 유효하게 만들고 엔티티를 넘나드는 규칙은 도메인 서비스에 두기
도메인 계층에서 엔터티를 만들고 생성 시점부터 유효하게 하세요. 주 생성자는 불변 조건을 강제하고 소유한 모든 컬렉션을 초기화해야 합니다. ORM에서 요구할 경우 정상적인 생성 규칙을 약화하지 않으면서 protected 매개변수 없는 생성자를 제공하세요. 식별자를 엔터티 내부에서 생성하지 말고 호출자에게서 받으세요.
비공개 setter와 의도가 드러나는 도메인 메서드로 일관성을 보호하세요. 런타임, 프록시 시스템, 지원되는 확장 모델에서 요구할 때 멤버를 override할 수 있게 하세요. 애그리게이트 루트는 복합 키 대신 안정적인 식별자 하나를 사용하고, 작게 유지하며, 다른 애그리게이트 루트를 탐색 속성이 아니라 식별자로 참조해야 합니다.
모델에 속하는 엔터티 간 규칙은 도메인 서비스에 두세요. 여러 구현, 교체, 테스트 격리에서 실제로 필요할 때만 인터페이스를 만드세요. `Manager`처럼 도메인 지향적인 명확한 접미사를 사용하고, DTO가 아니라 도메인 객체와 기본 타입을 받으며, 규칙이 실패하면 이름공간을 포함한 고유 오류 코드가 있는 현지화된 비즈니스 예외를 throw하세요.
도메인 서비스는 도메인별 조회, 정규화, 캐싱, 해석을 수행할 수 있지만 단순한 영속성 쿼리는 리포지토리에 속합니다. 도메인 내부에서 현재 인증된 사용자를 읽지 말고, 필요한 행위자 또는 tenant 값을 애플리케이션 계층에서 전달하세요.
## 애그리게이트 루트마다 리포지토리를 하나씩, 데이터베이스에 묶이지 않게 만들기
도메인 계층에서 리포지토리 계약을 정의하고 애그리게이트 루트마다 전용 계약 하나를 두세요. 애그리게이트가 소유한 내부 엔터티를 위한 리포지토리는 만들지 마세요.
공개 리포지토리 계약을 프로바이더와 독립적으로 유지하세요. 기본 또는 읽기 전용 리포지토리 기능을 우선하고, 모듈 사용자에게 `IQueryable`과 같은 쿼리 프로바이더 추상화를 노출하지 마세요. 프로바이더 구현은 내부에서 더 풍부한 쿼리 API를 사용할 수 있습니다.
모든 리포지토리 작업을 비동기로 만들고 선택적 취소 토큰을 받으세요. 애그리게이트 하나를 반환하는 메서드에서 호출자가 일반적으로 완전한 애그리게이트를 필요로 한다면 상세 정보 로딩을 기본적으로 활성화하세요. 목록 메서드에서는 무제한 join과 데이터 전송을 피하도록 기본적으로 비활성화하세요.
임시 복합 projection 클래스를 반환하지 말고 의미가 분명한 애그리게이트 반환 메서드를 사용하세요. 측정된 성능 요구 사항으로 정당화될 때만 projection 전용 메서드를 추가하세요. eager loading 로직은 프로바이더별 `IncludeDetails` helper에 두어 애플리케이션 서비스가 저장소의 애그리게이트 조립 방식을 알지 못하게 하세요.
프레임워크에 정규화 helper가 있다면 이를 통해 취소 토큰을 전달하세요. 내부에서 queryable을 가져올 때도 프로바이더 수준 데이터 필터, tenancy, soft-delete 규칙이 활성 상태인지 확인하세요.
## 작업마다 별도 DTO로 애플리케이션 서비스 인터페이스 설계하기
애플리케이션 서비스마다 인터페이스 하나를 계약 패키지에 두세요. 경계에서는 DTO와 기본 타입을 사용하고 도메인 엔터티를 받거나 반환하지 마세요. 메서드를 비동기로 유지하고 일관된 `Async` 접미사를 사용하며, 서비스 컨텍스트에서 분명한 경우 엔터티 이름을 반복하지 마세요.
예측 가능한 CRUD 형식을 사용하세요. 식별자 인수는 업데이트 DTO와 분리하고, 생성 및 업데이트 작업은 유용할 때 상세 DTO를 반환하며, 삭제는 payload를 반환하지 않게 하세요. 목록 쿼리는 선택적인 필터링, 정렬, 페이징 필드와 안전한 최대 페이지 크기를 가져야 합니다.
각 작업마다 별도의 입력 DTO를 만드세요. 현재 필드가 일치한다는 이유만으로 입력 DTO를 재사용하지 말고 입력 DTO 사이의 상속을 피하세요. DTO에 비즈니스 로직을 넣지 말고 경계에서 검증하며 제한에는 공유 상수를 재사용하세요. 겹치는 여러 형식보다 기본 출력 DTO와 상세 변형을 우선하세요.
애플리케이션 구현은 사용 사례를 오케스트레이션합니다. 전용 리포지토리를 사용하고, 필요한 애그리게이트를 로드하고, 도메인 메서드를 호출하며, change tracking을 가정하지 말고 업데이트를 명시적으로 영속화해야 합니다. 데이터 쿼리를 애플리케이션 서비스에 두지 마세요. 지원되는 mapper 규칙으로 확장 가능한 속성을 매핑하세요.
웹 타입, 업로드 추상화, stream을 애플리케이션 계약을 통해 전달하지 마세요. 전송 어댑터에서 파일을 안정적인 애플리케이션 수준 표현으로 변환하세요. 같은 모듈의 다른 애플리케이션 서비스를 호출하지 말고, 재사용 가능한 규칙을 도메인이나 대상이 명확한 공유 helper로 옮기세요. 모듈 간 호출은 상대 모듈의 공개 계약을 거쳐야 합니다.
공개 작업과 의도된 확장 helper를 override 가능하게 하세요. 사용자 지정이 프레임워크 계약의 일부라면 private helper보다 protected 확장 접점을 우선하세요.
## 데이터베이스 종류별로 코드를 나누고 각 모듈을 단독으로 쓸 수 있게 하기
각 모듈에 지원하는 영속성 프로바이더별 컨텍스트 인터페이스와 구현을 따로 제공하세요. set 또는 컬렉션은 애그리게이트 루트에만 노출하세요. 모듈 연결 이름을 명시적으로 지정하고 테이블 또는 컬렉션 접두사를 구성 가능하게 유지하며 프레임워크 소유 접두사를 예약하세요.
lazy loading에 의존하지 마세요. 컨텍스트 생명주기 메서드를 엔터티 세부 정보로 채우지 말고 이름이 지정된 model builder 확장에서 매핑을 구성하세요. 모듈별 규칙보다 먼저 모든 엔터티에 프레임워크의 규칙 매핑을 적용하세요.
프로바이더별 리포지토리 구현은 프로바이더의 표준 기반을 상속하고, 컨텍스트 인터페이스를 경계로 사용하고, 취소 토큰을 올바르게 전달하며, 프로바이더에 적합한 방식으로 애그리게이트 상세 로딩을 구현해야 합니다. 문서 저장소는 소유 컬렉션을 자연스럽게 로드할 수 있지만 관계형 프로바이더에는 명시적 include가 필요합니다. 어느 차이도 도메인이나 애플리케이션 계약으로 유출되어서는 안 됩니다.
각 패키지는 종속성을 명시적으로 선언한 모듈 구성 클래스 하나를 노출해야 합니다. 구성 중 서비스와 option을 등록하고 실행 중인 애플리케이션이 실제로 필요한 작업에만 애플리케이션 초기화 hook을 사용하세요. 모듈은 다른 패키지와의 숨겨진 결합 없이 단독으로 사용할 수 있어야 합니다.
모듈이 여러 프로바이더를 지원한다면 각 통합에 동일한 리포지토리 및 동작 계약을 실행하세요. 프로바이더 독립성은 명명 규칙이 아니라 테스트된 속성입니다.
## 확장 지점을 두고 오래된 API는 깨지 않고 단계적으로 폐기하기
프레임워크가 지원하는 확장 모델에 맞게 공개 및 protected 멤버를 설계하세요. override 가능한 메서드, 교체 가능한 서비스 등록, 실제 교체 지점의 인터페이스, 구성용 option 객체를 사용하세요. 확장 지점과 사용자 지정 구현이 보존해야 할 불변 조건을 문서화하세요.
사용 중단 주기 없이 공개 멤버를 제거하거나 이름을 바꾸지 마세요. 기존 멤버를 명확한 마이그레이션 지침과 함께 obsolete로 표시하고, 기본값이 있는 선택적 매개변수를 추가하며, 가능한 경우 새 기본 클래스 멤버에 기본 구현을 제공하세요. 기존 인터페이스를 깨뜨리기보다 새 인터페이스를 추가하세요.
사용자 대상 문자열을 공유 도메인 패키지가 소유한 현지화 리소스에 두세요. 하드코딩된 텍스트 대신 이름공간이 있는 키와 현지화된 비즈니스 오류를 사용하세요. 설정, 기능, permission 이름을 소유 계층의 상수로 정의하고 안정적인 모듈 이름공간 아래에 구성하세요.
하나의 process 내부 통신에는 in-process 이벤트 버스를, 모듈이나 서비스 간 통신에는 distributed 이벤트 버스를 사용하세요. 분산 이벤트 계약을 공유 도메인 패키지에 두고 단순하며 serializable한 상태로 유지하며 도메인 엔터티 대신 기본 타입이나 중첩 이벤트 계약을 사용하세요. 통합 이벤트는 애플리케이션 계층에서 처리하세요.
호환성, 현지화, 확장 동작을 공개 제품 영역으로 취급하세요. 관련 변경을 대상이 명확한 테스트와 마이그레이션 노트로 다루세요.
## 지원하는 모든 데이터베이스에서 테스트하고 최종 diff 확인하기
중요한 아키텍처 또는 공개 계약 변경 전에 프로젝트에서 승인한 설계 또는 issue 절차를 통해 합의하세요. pull request 범위를 제한하고 검토 가능하게 유지하며 영향을 받는 동작을 입증하는 데 필요한 테스트를 포함하세요.
분리된 도메인 규칙에는 단위 테스트를, 리포지토리, 매핑, permission, 검증, 모듈 구성에는 통합 테스트를 사용하세요. 계약이 프로바이더 독립성을 주장한다면 지원되는 모든 영속성 프로바이더를 실행하세요. 테스트 이름에는 작업, 시나리오, 예상 결과를 포함하세요.
최종 diff를 다음 점검표와 비교하세요.
- 웹 또는 영속성 관련 사항이 안쪽으로 유출되지 않고 종속성 방향이 유지됩니다.
- 엔터티가 불변 조건을 강제하고, 소유 컬렉션이 초기화되며, 애그리게이트 참조가 식별자를 사용합니다.
- 리포지토리 계약이 비동기, 취소 가능, 애그리게이트 중심, 프로바이더 독립 상태를 유지합니다.
- 애플리케이션 계약이 최소한의 검증된 DTO와 안정적인 메서드 형식을 사용합니다.
- 애플리케이션 서비스가 쿼리는 리포지토리에, 변경은 도메인 메서드에 위임합니다.
- 관계형 및 문서 통합이 각각의 매핑과 상세 로딩 규칙을 따릅니다.
- 공개 API, 현지화 키, 설정, 기능, permission, 이벤트가 호환성을 유지합니다.
- 테스트가 permission, 검증, 실패 사례, 프로바이더 차이, 경계 조건을 다룹니다.
- diff에 무관한 리팩터링, 생성 파일 편집, 불필요한 API 영역이 없습니다.
실행한 대상 검사와 미검증 상태로 남은 프로바이더 또는 호환성 경로를 보고하세요. 컴파일만으로 아키텍처 준수를 주장하지 마세요.
