닷넷관련/ASP.NET CORE 🍔
DDD(Domain-Driven Design) 기반 설계와 함께, ASP.NET Core 8 MVC 및 Dapper, AutoMapper, CQRS 사용
재우니
2025. 1. 7. 11:49
아래는 “To-Do 관리 시스템” 예시를 바탕으로 작성한 기능정의서(Feature Specification) 예시입니다.
본 문서는 프로젝트 개요 → 기능 상세 → 시스템 설계/구조 → 기술 스택 순으로 정리했습니다. 필요에 따라 추가·수정·삭제하여 실제 현업 환경에 맞게 활용하시면 되겠습니다.
기능정의서(Feature Specification)
1. 개요
- 프로젝트 이름: MyApp - “To-Do 관리 시스템”
- 주요 목적
- 사용자가 등록한 할 일(To-Do) 항목을 체계적으로 관리
- 여러 개의 필드(최대 50+ 개)도 처리 가능하도록 확장성 확보
- DDD(Domain-Driven Design) + Clean Architecture 개념을 적용해, 유지보수와 확장성을 극대화
- 핵심 사용 기술:
- ASP.NET Core 8 MVC
- CQRS(Command와 Query를 분리)
- Dapper(데이터베이스 접근)
- AutoMapper(객체 매핑)
- MediatR(Command/Query Handler 간 통신)
2. 기능 상세 (주요 Use Case)
2.1 할 일 생성(Create)
- 설명: 사용자가 새 할 일을 등록할 수 있음.
- 주요 시나리오
1) 사용자가 “할 일 제목”, “상세 설명”, “기한(Due Date)” 등을 입력
2) 생성 버튼 클릭 시, CreateToDoCommand를 MediatR를 통해 Handler에 전달
3) DB에 Insert 후, 새로 발급된Id
를 반환하거나, Index 페이지 등으로 리다이렉트
2.2 할 일 조회(Read)
- 설명: 등록된 할 일 목록 또는 특정 할 일 세부 정보를 조회
- 주요 시나리오
1) 사용자가 목록 페이지 진입
2) GetAllToDosQuery(또는 “단일 항목 조회 시 GetToDoByIdQuery”)를 통해 DB에서 데이터를 가져옴
3) View(Razor)에서 각 할 일을 목록 형태로 표시
2.3 할 일 수정(Update)
- 설명: 기존에 등록된 할 일의 “제목, 설명, 기한 등”을 변경
- 주요 시나리오
1) 사용자가 특정 할 일 수정 페이지로 이동
2) UpdateToDoCommand(별도 정의)로 변경된 정보 전송
3) 도메인 로직 내UpdateDetails()
메서드 통해 유효성 검증 후 DB Update
2.4 할 일 삭제(Delete)
- 설명: 더 이상 필요 없는 할 일을 삭제
- 주요 시나리오
1) 목록에서 “삭제” 버튼 클릭
2) DeleteToDoCommand(또는 단순 메서드)로 DB에서 해당 아이템 삭제
3) 삭제 후 목록 화면으로 리다이렉트
2.5 (선택) 할 일 완료 처리(MarkComplete)
- 설명: 할 일을 완료 상태로 전환
- 주요 시나리오
- 도메인 엔티티의
MarkComplete()
메서드 호출 - DB에
IsCompleted = true
업데이트
- 도메인 엔티티의
3. 시스템 설계/구조
3.1 폴더 구조 (DDD + Clean Architecture)
MyApp
├── Core/ // 도메인(핵심 비즈니스 로직) 계층
│ ├── Models/ // 도메인 엔터티, Value Objects
│ ├── Aggregates/ // (필요시) 애그리게이트 루트
│ ├── Contracts/ // 도메인 인터페이스(Repository 등)
│ └── Events/ // 도메인 이벤트
│
├── Infrastructure/ // 기술 구현 계층
│ ├── Persistence/ // DapperContext 등 DB 설정
│ ├── Repositories/ // Repository 구현체
│ └── ExternalServices/ // 외부(Email, 3rd-party) 서비스 구현
│
├── UseCases/ // CQRS, Application 로직 계층
│ ├── Commands/ // Command + CommandHandler
│ ├── Queries/ // Query + QueryHandler
│ ├── Dtos/ // DTO, Request/Response 객체
│ ├── Contracts/ // (필요시) Application(Service) 인터페이스
│ └── Mapping/ // AutoMapper 등 매핑 설정
│
├── WebUI/ // 프레젠테이션 계층 (ASP.NET Core MVC)
│ ├── Controllers/ // MVC 컨트롤러
│ ├── Views/ // Razor Views
│ ├── wwwroot/ // 정적 파일
│ └── Shared/ // Layout, Partial View 등
│
├── Tests/ // 테스트 프로젝트들
│ ├── CoreTests/ // 도메인 로직 테스트
│ ├── UseCasesTests/ // Application(CQRS) 테스트
│ └── IntegrationTests/ // DB 연동 등 통합 테스트
│
└── MyApp.sln
구조적 특징
- Core: 순수 비즈니스 로직 (Entity, Domain Services, Repository Interface 등)
- Infrastructure: 실제 DB 접근(Dapper), 외부 서비스(Email 등) 구현체 (Repository 구현)
- UseCases: Command/Query Handler, DTO, 매핑 등 “앱 기능 로직”을 담당
- WebUI: MVC Controller, View, 정적 자원 관리
- Tests: 각 계층별 테스트 분리
3.2 데이터베이스 구조(테이블 예시)
- ToDoItems
Id
(PK, int)Title
(nvarchar)Description
(nvarchar)DueDate
(datetime)IsCompleted
(bit)- ... (필요에 따라 50+ 개 필드 추가 가능)
3.3 주요 흐름도(간단 예시)
[사용자]
↓ (Create 페이지 이동)
[WebUI: Controller] --CreateToDoDto-->
↓
[UseCases: CreateToDoCommandHandler]
↓ (AutoMapper를 통해 CreateToDoCommand -> ToDoItem 변환)
[Core: ToDoItem] (도메인 엔티티 생성/검증)
↓
[Infrastructure: ToDoRepository] (DapperContext -> DB Insert)
↓
[DB] (ToDoItems 테이블)
4. 기술 스택 및 환경
- ASP.NET Core 8
- 최신 .NET 버전 사용, Minimal API 혹은 MVC(Razor) 적용 가능
- Dapper
- Lightweight ORM, SQL 직접 작성
- 복잡한 쿼리에 유연히 대응
- AutoMapper
- Entity ↔ DTO 간 매핑 자동화
- Command ↔ Entity 매핑 시 편의성 향상
- MediatR
- CQRS(Command/Query) 패턴 구현
- 핸들러(Handler) 간 의존성을 느슨하게 유지
- SQL Server (또는 원하는 DB)
DefaultConnection
을 통해 연결- DapperContext에 Connection String 제공
5. 테스트 전략
- 단위 테스트(Unit Test)
- CoreTests: ToDoItem 등 도메인 모델의 비즈니스 규칙 테스트
- UseCasesTests: CommandHandler/QueryHandler가 정상 동작하는지 Mock Repository 활용
- 통합 테스트(Integration Test)
- IntegrationTests: 실제 DB/인프라 연결 확인
- ToDoRepository를 통해 Insert → Select → Update → Delete가 제대로 작동하는지 검증
- UI 테스트 (선택)
- Selenium 등 E2E 테스트 툴 사용 가능
- 브라우저 상에서 “생성/조회/수정/삭제” 프로세스 시나리오 테스트
6. 기대효과 및 확장성
- 유연한 확장
- 50개 이상의 필드를 지닌 테이블에도 대비, AutoMapper로 DTO 매핑 관리
- CQRS로 로직 분할 → 새로운 UseCase 추가 시 손쉬운 확장 가능
- 비즈니스 로직 보호
- 도메인(Core) 레이어를 다른 계층에서 직접 침범하지 않도록 구조화
- 유지보수 용이
- 기능별(Command/Query 단위)로 분리되어 있어, 수정 범위가 명확
- Infrastructure(Repository) 분리로 DB 교체나 개선 시에도 최소 영향
전체 폴더/파일 구조
MyApp
├── Core/ // (과거 Domain)
│ ├── Models/ // 도메인 엔터티, Value Object 등
│ │ └── ToDoItem.cs
│ ├── Aggregates/ // (필요시) 애그리게이트 루트
│ ├── Contracts/ // 도메인 계층에서 필요한 인터페이스 (ex. Repository)
│ │ └── IToDoRepository.cs
│ └── Events/ // 도메인 이벤트
│
├── Infrastructure/ // (과거 Infrastructure)
│ ├── Persistence/ // DapperContext 등 DB 연결/세팅
│ │ └── DapperContext.cs
│ ├── Repositories/ // 구체적인 리포지토리 구현체
│ │ └── ToDoRepository.cs
│ └── ExternalServices/ // 외부 서비스(Email, 3rd-party API 연동) 모음
│ └── EmailService.cs // 예시
│
├── UseCases/ // (과거 Application)
│ ├── Commands/ // CQRS - Command 및 Handler
│ │ ├── CreateToDoCommand.cs
│ │ └── CreateToDoCommandHandler.cs
│ ├── Queries/ // CQRS - Query 및 Handler
│ │ ├── GetAllToDosQuery.cs
│ │ └── GetAllToDosQueryHandler.cs
│ ├── Dtos/ // DTO(입출력 데이터 전달 용도)
│ │ ├── ToDoItemDto.cs
│ │ └── CreateToDoDto.cs
│ ├── Contracts/ // (필요시) UseCase 계층 내 인터페이스
│ └── Mapping/ // AutoMapper 등 매핑 설정
│ └── MappingProfile.cs
│
├── WebUI/ // (과거 Presentation)
│ ├── Controllers/ // ASP.NET MVC 컨트롤러
│ │ └── ToDosController.cs
│ ├── Views/ // Razor Views
│ │ └── ToDos/
│ │ ├── Index.cshtml
│ │ └── Create.cshtml
│ ├── wwwroot/ // 정적 파일(CSS, JS 등)
│ │ ├── css/
│ │ └── js/
│ │ └── validation.js
│ └── Shared/ // Layout, Partial View 등
│ └── _Layout.cshtml
│
├── Tests/ // 테스트 프로젝트들
│ ├── CoreTests/ // (과거 DomainTests)
│ ├── UseCasesTests/ // (과거 ApplicationTests)
│ └── IntegrationTests/ // 통합 테스트
│
└── MyApp.sln // 솔루션 파일
1. Core 계층
1.1 ToDoItem.cs
(Models)
using System;
namespace MyApp.Core.Models
{
/// <summary>
/// 실제 비즈니스 규칙 및 유효성 검증을 포함하는 ToDo 엔터티.
/// </summary>
public class ToDoItem
{
public int Id { get; private set; }
public string Title { get; private set; }
public string Description { get; private set; } // 50개 이상의 필드를 포함할 수 있다고 가정
public DateTime? DueDate { get; private set; }
public bool IsCompleted { get; private set; }
/// <summary>
/// 신규 ToDoItem 생성 시 필요한 최소 정보(Title, Description, DueDate).
/// </summary>
public ToDoItem(string title, string description, DateTime? dueDate)
{
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentNullException(nameof(title));
if (string.IsNullOrWhiteSpace(description))
throw new ArgumentNullException(nameof(description));
Title = title;
Description = description;
DueDate = dueDate;
IsCompleted = false;
}
/// <summary>
/// 할 일을 완료 상태로 변경한다.
/// </summary>
public void MarkComplete()
{
IsCompleted = true;
}
/// <summary>
/// 할 일 정보를 업데이트한다.
/// </summary>
public void UpdateDetails(string title, string description, DateTime? dueDate)
{
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentNullException(nameof(title));
if (string.IsNullOrWhiteSpace(description))
throw new ArgumentNullException(nameof(description));
Title = title;
Description = description;
DueDate = dueDate;
}
}
}
1.2 IToDoRepository.cs
(Contracts)
using System.Collections.Generic;
using System.Threading.Tasks;
using MyApp.Core.Models;
namespace MyApp.Core.Contracts
{
/// <summary>
/// 도메인(핵심 비즈니스) 계층에서 필요한 저장소(Repository) 인터페이스.
/// ToDo 데이터를 어떻게 저장/조회/삭제하는지는 구현체(Infrastructure)가 담당.
/// </summary>
public interface IToDoRepository
{
Task<List<ToDoItem>> GetAllAsync();
Task<ToDoItem?> GetByIdAsync(int id);
Task<int> AddAsync(ToDoItem toDoItem);
Task UpdateAsync(ToDoItem toDoItem);
Task DeleteAsync(int id);
}
}
2. Infrastructure 계층
2.1 DapperContext.cs
(Persistence)
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
namespace MyApp.Infrastructure.Persistence
{
/// <summary>
/// Dapper를 이용하여 DB Connection을 생성하는 역할.
/// IConfiguration을 주입받아 ConnectionString 등을 관리.
/// </summary>
public class DapperContext
{
private readonly IConfiguration _configuration;
public DapperContext(IConfiguration configuration)
{
_configuration = configuration;
}
/// <summary>
/// DB Connection 객체를 생성해서 반환.
/// </summary>
public IDbConnection CreateConnection()
{
return new SqlConnection(_configuration.GetConnectionString("DefaultConnection"));
}
}
}
2.2 ToDoRepository.cs
(Repositories)
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using MyApp.Core.Contracts;
using MyApp.Core.Models;
using MyApp.Infrastructure.Persistence;
namespace MyApp.Infrastructure.Repositories
{
/// <summary>
/// IToDoRepository 인터페이스의 구현체.
/// DapperContext를 통해 DB와 상호작용하며, ToDoItem 엔터티를 저장/조회/삭제한다.
/// </summary>
public class ToDoRepository : IToDoRepository
{
private readonly DapperContext _context;
public ToDoRepository(DapperContext context)
{
_context = context;
}
public async Task<List<ToDoItem>> GetAllAsync()
{
const string query = "SELECT * FROM ToDoItems";
using var connection = _context.CreateConnection();
var result = await connection.QueryAsync<ToDoItem>(query);
return result.ToList();
}
public async Task<ToDoItem?> GetByIdAsync(int id)
{
const string query = "SELECT * FROM ToDoItems WHERE Id = @Id";
using var connection = _context.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<ToDoItem>(query, new { Id = id });
}
public async Task<int> AddAsync(ToDoItem toDoItem)
{
const string query = @"
INSERT INTO ToDoItems (Title, Description, DueDate, IsCompleted)
VALUES (@Title, @Description, @DueDate, @IsCompleted);
SELECT CAST(SCOPE_IDENTITY() as int);
";
using var connection = _context.CreateConnection();
var newId = await connection.ExecuteScalarAsync<int>(query, new
{
toDoItem.Title,
toDoItem.Description,
toDoItem.DueDate,
toDoItem.IsCompleted
});
return newId;
}
public async Task UpdateAsync(ToDoItem toDoItem)
{
const string query = @"
UPDATE ToDoItems
SET Title = @Title,
Description = @Description,
DueDate = @DueDate,
IsCompleted = @IsCompleted
WHERE Id = @Id;
";
using var connection = _context.CreateConnection();
await connection.ExecuteAsync(query, toDoItem);
}
public async Task DeleteAsync(int id)
{
const string query = "DELETE FROM ToDoItems WHERE Id = @Id";
using var connection = _context.CreateConnection();
await connection.ExecuteAsync(query, new { Id = id });
}
}
}
3. UseCases 계층
3.1 DTOs
3.1.1 ToDoItemDto.cs
using System;
namespace MyApp.UseCases.Dtos
{
/// <summary>
/// ToDo 데이터를 전달받거나 반환할 때 사용하는 전송 객체(예: 조회 시).
/// </summary>
public class ToDoItemDto
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime? DueDate { get; set; }
}
}
3.1.2 CreateToDoDto.cs
using System;
namespace MyApp.UseCases.Dtos
{
/// <summary>
/// ToDo 생성 시, 사용자로부터 입력받을 데이터 전송 객체.
/// (Controller에서 [HttpPost]로 받는 폼 데이터 등에 대응)
/// </summary>
public class CreateToDoDto
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime? DueDate { get; set; }
}
}
3.2 Command & Handler
3.2.1 CreateToDoCommand.cs
using System;
using MediatR;
namespace MyApp.UseCases.Commands
{
/// <summary>
/// ToDo를 새로 생성하기 위한 Command.
/// MediatR의 IRequest<T>를 구현하여 Handler에게 전달됨.
/// </summary>
public class CreateToDoCommand : IRequest<int>
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime? DueDate { get; set; }
}
}
3.2.2 CreateToDoCommandHandler.cs
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using MediatR;
using MyApp.Core.Contracts;
using MyApp.Core.Models;
namespace MyApp.UseCases.Commands
{
/// <summary>
/// CreateToDoCommand를 처리하는 Handler.
/// ToDoItem 엔터티로 매핑 후, 리포지토리에 저장 로직을 호출.
/// </summary>
public class CreateToDoCommandHandler : IRequestHandler<CreateToDoCommand, int>
{
private readonly IToDoRepository _repository;
private readonly IMapper _mapper;
public CreateToDoCommandHandler(IToDoRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<int> Handle(CreateToDoCommand request, CancellationToken cancellationToken)
{
// Command -> Entity 변환(AutoMapper)
var toDoItem = _mapper.Map<ToDoItem>(request);
// DB에 저장
var newId = await _repository.AddAsync(toDoItem);
return newId;
}
}
}
3.3 Query & Handler
3.3.1 GetAllToDosQuery.cs
using System.Collections.Generic;
using MediatR;
using MyApp.UseCases.Dtos;
namespace MyApp.UseCases.Queries
{
/// <summary>
/// 모든 ToDo 목록을 가져오기 위한 Query.
/// MediatR를 통해 Handler에 전달.
/// </summary>
public class GetAllToDosQuery : IRequest<List<ToDoItemDto>>
{
}
/// <summary>
/// 특정 ID를 가진 ToDo를 조회하기 위한 Query.
/// </summary>
public class GetToDoByIdQuery : IRequest<ToDoItemDto>
{
public int Id { get; }
public GetToDoByIdQuery(int id)
{
Id = id;
}
}
}
3.3.2 GetAllToDosQueryHandler.cs
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using MediatR;
using MyApp.Core.Contracts;
using MyApp.UseCases.Dtos;
namespace MyApp.UseCases.Queries
{
/// <summary>
/// GetAllToDosQuery를 처리하는 Handler.
/// 모든 ToDoItem을 조회 후, ToDoItemDto 리스트로 매핑하여 반환.
/// </summary>
public class GetAllToDosQueryHandler : IRequestHandler<GetAllToDosQuery, List<ToDoItemDto>>
{
private readonly IToDoRepository _repository;
private readonly IMapper _mapper;
public GetAllToDosQueryHandler(IToDoRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<List<ToDoItemDto>> Handle(GetAllToDosQuery request, CancellationToken cancellationToken)
{
var toDoItems = await _repository.GetAllAsync();
return toDoItems.Select(item => _mapper.Map<ToDoItemDto>(item)).ToList();
}
}
}
3.3.3 UseCases/Queries 폴더 내 새로운 Query Handler 추가
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using MediatR;
using MyApp.Core.Contracts;
using MyApp.UseCases.Dtos;
namespace MyApp.UseCases.Queries
{
/// <summary>
/// GetToDoByIdQuery를 처리하는 Handler.
/// Repository를 통해 해당 ID의 ToDoItem을 조회하고, DTO로 매핑 후 반환한다.
/// </summary>
public class GetToDoByIdQueryHandler : IRequestHandler<GetToDoByIdQuery, ToDoItemDto>
{
private readonly IToDoRepository _repository;
private readonly IMapper _mapper;
public GetToDoByIdQueryHandler(IToDoRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
public async Task<ToDoItemDto> Handle(GetToDoByIdQuery request, CancellationToken cancellationToken)
{
// Repository 이용해 DB에서 아이템 조회
var toDoItem = await _repository.GetByIdAsync(request.Id);
// 조회 결과가 없으면 null 반환(또는 예외 처리)
if (toDoItem == null)
return null;
// Entity -> DTO 매핑
return _mapper.Map<ToDoItemDto>(toDoItem);
}
}
}
3.4 AutoMapper 설정 (Mapping)
3.4.1 MappingProfile.cs
using AutoMapper;
using MyApp.Core.Models;
using MyApp.UseCases.Commands;
using MyApp.UseCases.Dtos;
namespace MyApp.UseCases.Mapping
{
/// <summary>
/// 객체 간 매핑 규칙 설정.
/// Command -> Entity, DTO -> Entity 등.
/// </summary>
public class MappingProfile : Profile
{
public MappingProfile()
{
// CreateToDoCommand -> ToDoItem
CreateMap<CreateToDoCommand, ToDoItem>();
// ToDoItemDto <-> ToDoItem (양방향)
CreateMap<ToDoItemDto, ToDoItem>().ReverseMap();
// 필요 시, CreateToDoDto -> ToDoItem 매핑 등 추가 가능
CreateMap<CreateToDoDto, ToDoItem>();
}
}
}
4. WebUI 계층
4.1 ToDosController.cs
using System.Threading.Tasks;
using AutoMapper;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using MyApp.UseCases.Commands;
using MyApp.UseCases.Dtos;
namespace MyApp.WebUI.Controllers
{
/// <summary>
/// MVC Controller: ToDo 항목에 대한 Create/Read/Update/Delete 등 요청을 처리.
/// </summary>
public class ToDosController : Controller
{
private readonly IMediator _mediator;
private readonly IMapper _mapper;
public ToDosController(IMediator mediator, IMapper mapper)
{
_mediator = mediator;
_mapper = mapper;
}
[HttpGet]
public IActionResult Create()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Create(CreateToDoDto dto)
{
if (!ModelState.IsValid)
return View(dto);
// DTO -> Command 변환
var command = new CreateToDoCommand
{
Title = dto.Title,
Description = dto.Description,
DueDate = dto.DueDate
};
// Handler를 통해 새 ToDo 생성
await _mediator.Send(command);
// 생성 후 목록 페이지로 리다이렉트
return RedirectToAction("Index");
}
/// <summary>
/// 예시: 목록 화면(Index) - Query를 통해 모든 ToDo 가져오기
/// </summary>
[HttpGet]
public IActionResult Index()
{
// 단순 페이지 이동(실제 데이터 호출은 서버단에서 별도 처리 or Razor에서 직접 호출 가능)
// 혹은 Mediator Query 호출 후 View에 모델을 넘길 수도 있음
return View();
}
/// <summary>
/// 특정 ID의 ToDo 상세 페이지를 보여주는 메서드 (예시).
/// </summary>
[HttpGet]
public async Task<IActionResult> Detail(int id)
{
// GetToDoByIdQuery를 발행하여 Handler 호출
var toDoItem = await _mediator.Send(new GetToDoByIdQuery(id));
// 결과가 없으면 404 처리(또는 다른 로직)
if (toDoItem == null)
return NotFound();
// Razor View에 모델 전달
return View(toDoItem);
}
}
}
참고:
여기서는 Index 액션 메서드 내에서 실제 GetAllToDosQuery 호출 로직을 생략했습니다.
실제로는 다음과 같이 할 수도 있습니다.[HttpGet] public async Task<IActionResult> Index() { var query = new GetAllToDosQuery(); var toDoList = await _mediator.Send(query); return View(toDoList); }
그리고Index.cshtml
에서 모델을 받아서 렌더링할 수 있습니다.
5. Tests 계층
Tests
├── CoreTests/
│ └── ToDoItemTests.cs // 예: 도메인 모델(Unit Test)
├── UseCasesTests/
│ ├── CreateToDoCommandHandlerTests.cs
│ └── GetAllToDosQueryHandlerTests.cs
└── IntegrationTests/
└── ToDoIntegrationTests.cs // 실제 DB 연동 등 통합 테스트
CoreTests
에서는ToDoItem
같은 도메인 모델의 순수 비즈니스 로직 유효성 검증을 테스트합니다.UseCasesTests
에서는 Command/Query Handler를 테스트하면서, Mock Repository 등을 활용해 시나리오 검증을 합니다.IntegrationTests
에서는 실제 DB(혹은 테스트 DB)에 붙여 보거나, 여러 계층을 한 번에 테스트합니다.
6. 요약 및 마무리
- 위 코드는 DDD 구조와 Clean Architecture 아이디어를 혼합하여,
1) Core (과거 Domain)
2) Infrastructure (DB, 외부 서비스 등 기술 구현)
3) UseCases (CQRS, Application 로직)
4) WebUI (Presentation)
로 분리하였습니다. - 각 폴더와 클래스 파일마다 용도와 책임을 주석으로 적절히 기술해,
새로운 팀원이 와도 “어느 폴더에 무엇이 들어있고, 왜 거기에 있는지”를 쉽게 이해할 수 있도록 했습니다. - 프로젝트 상황에 따라 네임스페이스, 폴더명 등을 팀 컨벤션에 맞게 조정하면 됩니다.
- CI/CD나 실제 배포 과정에서 여러 프로젝트(프로젝트 분리)로 구성할 수도 있으나, 단일 프로젝트 내에서도 이처럼 관심사 분리가 가능합니다.
가장 중요한 점은, “비즈니스 로직(Core)을 잘 보호하고, 다른 계층(UseCases, Infrastructure, WebUI)과 의존성 흐름이 명확히 구분되는지”를 항상 염두에 두는 것입니다.
이렇게 폴더 구조와 네임스페이스를 재구성하면, 유지보수와 확장성 면에서 큰 이점을 누릴 수 있습니다.