ASP.NET에서 DDD 확장: 30개 이상의 집계를 넘어서는 도전
ASP.NET과 Entity Framework Core를 사용하는 프로젝트가 30개 이상의 집계를 초과하면, '한 쿼리당 한 집계'라는 원칙이 치명적인 성능 문제를 야기합니다. 각 집계는 중첩된 객체를 모두 로딩해야 하며, 이로 인해 수많은 JOIN을 포함한 SQL 쿼리가 생성됩니다. 특히 ERP 시스템이나 마켓플레이스와 같은 환경에서는 객체 그래프가 5~6단계까지 깊어져 문제가 더욱 심화됩니다.
일반적인 주문 집계 쿼리는 다음과 같습니다:
var orders = await context.Orders
.Include(o => o.Customer)
.ThenInclude(c => c.Address)
.Include(o => o.Customer)
.ThenInclude(c => c.Contacts)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.ThenInclude(p => p.Category)
.Include(o => o.Items)
.ThenInclude(i => i.Discount)
.ToListAsync();
결과적으로 생성되는 SQL은 수십 개의 LEFT JOIN을 포함하며, 실행 속도가 크게 저하됩니다. 주문 헤더의 단일 필드 수정만으로도 전체 OrderItems가 집계에 포함되어 로딩되며, 성능 부담이 지수적으로 증가합니다.
사양의 급속한 증가와 그 한계
사양(Specification) 패턴은 쿼리 논리를 캡슐화하지만, 30개 이상의 집계를 가진 프로젝트에서는 사양 클래스 파일이 수백 개로 늘어납니다. And/Or를 통한 조합은 EF Core의 IQueryable 의존성으로 인해 비효율적인 SQL을 생성합니다.
주요 과제:
- ORM에 종속됨: Dapper나 gRPC와 함께 사용하려면 다시 작성해야 합니다.
- 복잡한 조합: 역할, 상태, 시간대 등 실제 시나리오가 복잡해지면서 관리가 불가능해집니다.
- 추상화의 부담: 추상화 설계에 소요되는 시간이 실제 비즈니스 로직 구현보다 더 큽니다.
집계 간 트랜잭션과 인프라 부담
ASP.NET Core의 스코프 기반 DbContext는 소규모 프로젝트에서는 단위 작업(Unit of Work)에 매우 효과적입니다. 하지만 대규모 시스템에서는 비즈니스 워크플로우가 5개 이상의 집계를 동시에 업데이트해야 하는 경우가 많습니다: 주문 생성, 결제 처리, 재고 예약, 콜센터 작업 생성 등.
사용 가능한 해결책은 DDD 원칙과 충돌합니다:
- 단일 데이터베이스 트랜잭션 — 신의 서비스를 만들고 집계 경계를 위반합니다.
- 분산 사가 또는 아웃박스 패턴 — 복잡성을 최대 10배까지 증가시킵니다.
결과적으로 코드베이스의 70%는 동기화를 위한 인프라 연결 코드이며, 비즈니스 로직이 아닙니다.
코드 탐색: 수천 개의 파일과 느린 반복 개발
DDD 구조(API/Application/Domain/Infrastructure + Features)는 30개 이상의 집계를 가질 경우 1,000개 이상의 파일을 생성합니다. 단일 엔티티 필드 변경은 다음 10가지 요소에 영향을 미칩니다:
- 도메인 엔티티
- 플루언트 API 구성
- 마이그레이션
- 명령어
- 검증기 (FluentValidation)
- 핸들러
- DTO
- 매핑기 (AutoMapper)
- 사양
- 테스트
이로 인해 개발 속도가 극도로 저하되고, 신입 직원의 정착이 어려워지고 장기 유지보수가 어렵습니다.
무엇이 가장 중요한가?
- 성능: Include/ThenInclude 연산은 비효율적인 SQL을 생성하여 API 응답 시간이 지수적으로 증가합니다.
- 사양: 확장되지 않으며 EF Core에 종속되며, 조합 시 오류 발생 가능성이 높습니다.
- 트랜잭션: 집계 간 연산은 DDD 이상과 현실적 비즈니스 요구 사이의 타협을 강요합니다.
- 코드베이스 크기: 1,000개 이상의 파일은 반복 개발 속도를 50~70% 감소시킵니다.
- 하이브리드 접근법: 핵심부에는 DDD를 적용하고, 주변 기능은 CQRS 및 수직 슬라이스를 활용하세요.
마이그레이션을 위한 제안
30개 이상의 집계를 가진 프로젝트는 하이브리드 아키텍처를 채택하는 것이 좋습니다:
- CQRS: 명령어와 쿼리를 분리하고, Include 없이 읽기 모델을 사용하세요.
- 수직 슬라이스: 기능별로 코드를 그룹화하여 계층 구조의 복잡성을 줄이세요.
- 기능 기반 디자인: 오니언처럼 엄격한 구조 없이도 모듈성을 달성할 수 있습니다.
이 접근 방식은 복잡한 도메인에서도 DDD 원칙을 유지하면서, 나머지 모든 부분을 단순화합니다.
— Editorial Team
아직 댓글이 없습니다.