닷넷관련/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. 개요

  1. 프로젝트 이름: MyApp - “To-Do 관리 시스템”
  2. 주요 목적
    • 사용자가 등록한 할 일(To-Do) 항목을 체계적으로 관리
    • 여러 개의 필드(최대 50+ 개)도 처리 가능하도록 확장성 확보
    • DDD(Domain-Driven Design) + Clean Architecture 개념을 적용해, 유지보수와 확장성을 극대화
  3. 핵심 사용 기술:
    • 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. 기술 스택 및 환경

  1. ASP.NET Core 8
    • 최신 .NET 버전 사용, Minimal API 혹은 MVC(Razor) 적용 가능
  2. Dapper
    • Lightweight ORM, SQL 직접 작성
    • 복잡한 쿼리에 유연히 대응
  3. AutoMapper
    • Entity ↔ DTO 간 매핑 자동화
    • Command ↔ Entity 매핑 시 편의성 향상
  4. MediatR
    • CQRS(Command/Query) 패턴 구현
    • 핸들러(Handler) 간 의존성을 느슨하게 유지
  5. SQL Server (또는 원하는 DB)
    • DefaultConnection을 통해 연결
    • DapperContext에 Connection String 제공

5. 테스트 전략

  1. 단위 테스트(Unit Test)
    • CoreTests: ToDoItem 등 도메인 모델의 비즈니스 규칙 테스트
    • UseCasesTests: CommandHandler/QueryHandler가 정상 동작하는지 Mock Repository 활용
  2. 통합 테스트(Integration Test)
    • IntegrationTests: 실제 DB/인프라 연결 확인
    • ToDoRepository를 통해 Insert → Select → Update → Delete가 제대로 작동하는지 검증
  3. UI 테스트 (선택)
    • Selenium 등 E2E 테스트 툴 사용 가능
    • 브라우저 상에서 “생성/조회/수정/삭제” 프로세스 시나리오 테스트

6. 기대효과 및 확장성

  1. 유연한 확장
    • 50개 이상의 필드를 지닌 테이블에도 대비, AutoMapper로 DTO 매핑 관리
    • CQRS로 로직 분할 → 새로운 UseCase 추가 시 손쉬운 확장 가능
  2. 비즈니스 로직 보호
    • 도메인(Core) 레이어를 다른 계층에서 직접 침범하지 않도록 구조화
  3. 유지보수 용이
    • 기능별(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)과 의존성 흐름이 명확히 구분되는지”를 항상 염두에 두는 것입니다.
이렇게 폴더 구조와 네임스페이스를 재구성하면, 유지보수와 확장성 면에서 큰 이점을 누릴 수 있습니다.