최소 API는 .NET 6에서 도입된 흥미로운 기능으로, API를 생성하는 방식을 혁신적으로 변화시키기 위해 설계되었습니다.

최소한의 코드와 제로 보일러플레이트로 강력한 API를 구축하는 것을 상상해 보세요. 더 이상 컨트롤러, 라우팅 또는 미들웨어와 씨름할 필요가 없습니다. 이것이 최소 API가 허용하는 것입니다. 이러한 API의 아이디어는 개발 프로세스를 간소화하여 매우 쉽고 효율적으로 만드는 것입니다.

이 기사에서는 .NET 8의 최소 API 세계에 대해 깊이 들어가고 완전히 기능적인 서점 API를 만드는 방법을 안내합니다. 모든 책을 가져오고, ID로 책을 검색하며, 새로운 책을 추가하고, 책을 삭제하는 방법을 배울 것입니다. 시작해 봅시다.

목차

필수 조건

시작하기 전에, 다음의 필수 조건이 귀하의 머신에 설치되어 있는지 확인하십시오:

또는 내장된 .NET 8 지원이 포함된 비주얼 스튜디오 2022를 사용할 수 있습니다. 그러나 이 기사에서는 비주얼 스튜디오 코드를 사용할 것입니다. 이 도구는 가벼우며 사용하기 쉽고 다중 플랫폼을 지원합니다.

우리는 Swagger UI를 사용하여 API를 테스트할 것입니다. Swagger UI는 브라우저에서 직접 API와 상호작용할 수 있게 해주는 강력한 도구입니다. 이 도구는 API 엔드포인트를 테스트하기 위한 사용자 친화적인 인터페이스를 제공하여 API를 테스트하고 디버그하는 과정을 쉽게 만들어줍니다.

새 프로젝트를 생성하면 필요한 패키지가 자동으로 설치되고 Swagger UI를 사용하도록 프로젝트가 구성됩니다. .NET 8은 기본적으로 Swagger UI를 포함하고 있으므로 비주얼 스튜디오에서 애플리케이션을 만들든 .NET으로 만들든 Swagger UI가 자동으로 구성됩니다.

애플리케이션을 실행하면 Swagger UI가 브라우저에서 자동으로 열리지만 VS Code를 사용하고 있으므로 터미널에서 포트 번호를 클릭해야 합니다.

이 프로젝트의 소스 코드는 GitHub에서 찾을 수 있습니다.

최소 API 소개

다수의 엔드포인트로 이루어진 코드베이스에서 작업 중이라고 상상해보세요. 이는 상당히 크고 복잡해집니다. 기존에는 ASP.NET Core에서 API를 구축하는 것이 컨트롤러, 라우팅, 미들웨어 및 상당량의 보일러플레이트 코드를 사용하는 것을 포함합니다. 그러나 ASP.NET Core에서 API를 구축하는 두 가지 방법이 있습니다: 전통적인 방법과 최소 방법.

전통적인 방법은 대부분의 개발자에게 익숙하며, 컨트롤러와 방대한 인프라 코드를 사용합니다. .NET 6에서 소개된 최소 방법은 최소한의 코드와 보일러플레이트가 없이 API를 생성할 수 있게 합니다. 이 접근 방식은 개발 프로세스를 간소화하여 인프라 코드를 처리하는 대신 비즈니스 로직 작성에 집중할 수 있게 합니다.

최소 API는 가벼우며 빠르며 소규모에서 중간 규모의 API를 구축하기에 이상적입니다. 프로토타입 작성, 마이크로서비스 구축 또는 복잡성이 크지 않은 간단한 API를 생성하는 데 이상적입니다. 이 안내서에서는 .NET 6의 최소 API 세계를 탐험하고 처음부터 완전히 기능하는 서점 API를 생성하는 방법을 배웁니다.

최소 API 생성 방법

기본 템플릿이 이미 최소 API인 경우 dotnet CLI를 사용하면 최소 API를 만드는 것이 간단합니다. 그러나 Visual Studio를 사용하는 경우 프로젝트 템플릿과 함께 제공되는 보일러플레이트 코드를 제거해야 합니다.

dotnet CLI를 사용하여 최소 API 프로젝트를 생성해 봅시다.


dotnet new webapi  -n BookStoreApi

dotnet new webapi 명령을 사용하면 BookStoreApi라는 새로운 최소 API 프로젝트가 생성됩니다. 이 프로젝트에는 시작하는 데 필요한 파일과 폴더가 포함되어 있습니다.

프로젝트 구조를 살펴봅시다:

  • Program.cs: 응용 프로그램의 진입점으로 호스트가 구성되는 곳입니다.

  • bookapi-minimal.sln: 프로젝트를 포함하는 솔루션 파일입니다.

  • bookapi-minimal.http: API를 테스트하기 위한 샘플 HTTP 요청을 포함하는 파일입니다.

  • bookapi-minimal.csproj: 프로젝트 구성을 포함하는 프로젝트 파일입니다.

  • appsettings.json: 응용 프로그램 설정을 저장하는 구성 파일입니다.

  • appsettings.Development.json : 개발 환경을 위한 구성 파일입니다.

프로그램.cs 파일을 열면 코드가 최소화되어 있는 것을 알 수 있습니다. Program.cs 파일에는 다음과 같은 코드가 포함되어 있습니다:


var builder = WebApplication.CreateBuilder(args);

// 컨테이너에 서비스 추가
// Swagger/OpenAPI 구성에 대한 자세한 내용은 https://aka.ms/aspnetcore/swashbuckle에서 확인할 수 있습니다.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// HTTP 요청 파이프라인 구성
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast =  Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();

app.Run();

record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

아직 코드를 완전히 이해하지 못했다면 걱정하지 마세요—다음 섹션에서 자세히 다룰 것입니다. 중요한 점은 최소 API는 매우 적은 코드를 필요로 하며, 이것이 그들의 주요 장점 중 하나입니다.

기본 코드는 간단한 날씨 예보 API를 설정하여 사용할 수 있도록 합니다. 날씨 예보 목록을 생성하고 GET 요청을 하면 해당 목록을 반환합니다. 또한 코드에는 API를 테스트하는 데 도움이 되는 Swagger UI가 포함되어 있습니다.

app.MapGet 메서드에 특별히 주의를 기울여야 합니다. 이 메서드는 경로를 핸들러 함수에 매핑합니다. 이 경우 /weatherforecast 경로를 날씨 예보 목록을 반환하는 함수에 매핑합니다. 다음 섹션에서 우리 자신의 엔드포인트를 만드는 데 비슷한 방법을 사용할 것입니다.

프로젝트 폴더 구조를 만들기 전에 Controller 기반 및 최소 API에서의 HTTP 메서드를 이해해 봅시다.

Controller 기반 및 최소 API에서의 HTTP 메서드

웹 API를 만드는 전통적인 방법인 Controller 기반 접근 방식에서는 컨트롤러 클래스를 만들고 각 HTTP 메서드에 대한 메서드를 정의해야 합니다. 예를 들어:

  • GET

    메서드를 만들려면 [HttpGet] 속성을 사용합니다.

  • POST

    메서드를 만들려면 [HttpPost] 속성을 사용합니다.

  • PUT

    메서드를 만들려면 [HttpPut] 속성을 사용합니다.

  • DELETE

    메서드를 만들려면 [HttpDelete] 속성을 사용합니다.

이것이 Controller 기반 접근 방식에서 엔드포인트가 만들어지는 방법입니다.

반면에, Minimal API는 app.MapGet, app.MapPost, app.MapPut, 그리고 app.MapDelete와 같은 메서드를 사용하여 엔드포인트를 만듭니다. 이것이 두 접근 방식 사이의 주요 차이점입니다: Controller 기반 API는 엔드포인트를 정의하기 위해 속성을 사용하는 반면, Minimal API는 메서드를 사용합니다.

이제 Controller 기반 및 Minimal API에서 HTTP 요청을 처리하는 방법을 이해했으니, 프로젝트 폴더 구조를 만들어봅시다.

프로젝트 폴더 구조를 만들기 전에 먼저 가지고 있는 것을 실행해 봅시다. 이전에 배웠듯이, Visual Studio 또는 .NET CLI로 프로젝트를 만들면 기본적으로 WeatherForecast 프로젝트가 함께 제공되며 이를 실행하여 UI에서 확인할 수 있습니다. 프로젝트 폴더를 만들기 전에 모든 것이 작동하는지 확인하기 위해 실행해 봅시다.

다음 명령을 실행하세요:


dotnet run

다음 출력이 나타날 것입니다:

info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5228
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Devolopemnt\Dotnet\bookapi-minimal

이는 응용 프로그램이 실행되고 http://localhost:5228에서 수신 대기 중임을 나타냅니다. 위에서 언급했듯이, dotnet CLI와 Visual Studio Code를 사용하고 있기 때문에 응용 프로그램은 자동으로 브라우저를 열어주지 않습니다. 우리가 직접 해야 합니다.

브라우저를 열고 http://localhost:5228/swagger/index.html로 이동하여 API의 기본 응답을 확인하세요.

다음과 같은 내용이 표시됩니다:

이제 우리가 해야 할 다음 단계는 프로젝트를 구조화하고 시작할 수 있도록 필요한 파일과 폴더를 만드는 방법을 찾는 것입니다.

최소 API 프로젝트 파일

프로젝트를 구성하기 위해 구조화된 폴더 계층을 만들 것입니다. 이는 코드를 깨끗하고 유지보수 가능하게 유지하는 데 도움이 됩니다. 사용할 폴더 구조는 다음과 같습니다:

  • AppContext: 데이터베이스 컨텍스트와 관련 구성을 포함합니다.

  • 구성: 데이터베이스의 Entity Framework Core 구성 및 시드 데이터를 보유합니다.

  • 계약: 응용 프로그램에서 사용되는 데이터 전송 객체(DTO)를 포함합니다.

  • 엔드포인트: 최소 API 엔드포인트를 정의하고 구성하는 곳입니다.

  • 예외: 프로젝트에서 사용되는 사용자 정의 예외 클래스를 포함합니다.

  • 확장: 프로젝트 전체에서 사용할 확장 메서드를 보유합니다.

  • 모델: 비즈니스 로직 모델을 포함합니다.

  • 서비스: 비즈니스 로직을 구현하는 서비스 클래스를 포함합니다.

  • 인터페이스: 서비스를 매핑하는 데 사용되는 인터페이스 정의를 보유합니다.

비주얼 스튜디오 코드에서 다음과 같은 폴더 구조를 만들 수 있습니다:

- AppContext
- Configurations
- Contracts
- Endpoints
- Exceptions
- Extensions
- Models
- Services
- Interfaces

설정을 완료한 후에는 프로젝트 폴더 구조가 다음과 같아야 합니다:

이제 프로젝트 구조를 설정했으니 코드 작성을 시작할 수 있습니다. 모델을 만들기 시작합시다.

모델 만드는 방법

이 섹션에서는 애플리케이션을 위한 모델을 만들 것입니다. 모델은 애플리케이션의 데이터를 나타내는 구성 요소로, 예제에서는 책을 위한 모델을 만들 것입니다.

시작하려면 프로젝트 디렉토리에 Models라는 이름의 폴더를 만드세요. 이 폴더 안에 BookModel.cs라는 파일을 만들고 다음 코드를 추가하세요:

// Models/BookModel.cs


namespace bookapi_minimal.Models
{
    public class BookModel
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public string Language { get; set; }
        public int TotalPages { get; set; }
    }
}

BookModel 클래스는 책의 제목, 작가, 설명, 카테고리, 언어, 총 페이지 수 등의 세부 정보를 나타내는 속성을 정의합니다. 각 속성은 책에 대한 특정 정보를 저장하는 데 설계되어 있어 애플리케이션 내에서 책 데이터를 쉽게 관리하고 조작할 수 있습니다.

모델을 만들었으니 이제 데이터베이스 컨텍스트를 만들어 봅시다.

데이터베이스 컨텍스트 만드는 방법

데이터베이스 컨텍스트는 데이터베이스와의 세션을 나타내는 클래스입니다. 데이터베이스와 상호작용하고 데이터베이스 작업을 실행하는 역할을 합니다. 우리 애플리케이션에서는 Entity Framework Core를 사용하여 데이터베이스와 상호작용할 것입니다.

필요한 패키지 설치

우리의 데이터베이스 컨텍스트를 만들기 전에 다음 패키지를 설치해야 합니다:

다음 명령어를 사용하여 이러한 패키지를 설치할 수 있습니다:

dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package FluentValidation.DependencyInjectionExtensions

패키지 설치 확인

설치된 패키지를 확인하려면 프로젝트의 루트 디렉토리에 있는 bookapi-minimal.csproj 파일을 엽니다. 설치된 패키지가 다음과 같이 나열되어 있는지 확인하십시오:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <RootNamespace>bookapi_minimal</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
   <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>

이로써 패키지가 성공적으로 설치되었음을 확인할 수 있습니다.

이제 데이터베이스 컨텍스트를 만들어 봅시다.

AppContext 폴더에 ApplicationContext.cs라는 새 파일을 만들고 다음 코드를 추가하십시오:

// AppContext/ApplicationContext.cs

using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;

namespace bookapi_minimal.AppContext
{

    public class ApplicationContext(DbContextOptions<ApplicationContext> options) : DbContext(options)
    {

        // 데이터베이스 컨텍스트의 기본 스키마
        private const string DefaultSchema = "bookapi";


       // 데이터베이스 내의 책 모음을 나타내는 DbSet
        public DbSet<BookModel> Books { get; set; }

        // 데이터베이스 컨텍스트를 구성하는 생성자

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.HasDefaultSchema(DefaultSchema);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationContext).Assembly);

        }

    }
}

위의 코드를 분해해 봅시다:

  • ApplicationContext라는 클래스를 정의하고 DbContext를 상속합니다. DbContext 클래스는 Entity Framework Core의 일부이며 데이터베이스와의 세션을 나타냅니다.

  • 생성자는 DbContextOptions<ApplicationContext>의 인스턴스를 허용합니다. 이 생성자는 데이터베이스 컨텍스트 옵션을 구성하는 데 사용됩니다.

  • Books라는 속성을 정의하고 DbSet<BookModel> 타입을 가집니다. 이 속성은 데이터베이스 내의 책 모음을 나타냅니다.

  • OnModelCreating 메소드를 재정의하여 데이터베이스 스키마를 구성하고 애플리케이션에서 정의된 구성을 적용합니다.

이제 데이터베이스 컨텍스트를 만들었으니, 확장 메소드를 만들고 의존성 주입 컨테이너에 데이터베이스 컨텍스트를 등록해봅시다.

확장 메소드 만들기

확장 메소드를 만들기 전에, ASP.NET Core의 문맥에서 확장 메소드가 무엇인지 이해해봅시다.

확장 메소드는 기존 타입을 수정하지 않고 새로운 기능을 추가하는 정적 메소드입니다. ASP.NET Core에서 확장 메소드는 의존성 주입 컨테이너에 서비스를 등록하는 데 사용되는 IServiceCollection 인터페이스의 기능을 확장하는 데 자주 사용됩니다.

서비스는 데이터베이스 액세스, 로깅, 구성과 같은 애플리케이션에 기능을 제공하는 구성 요소입니다. IServiceCollection 인터페이스에 대한 확장 메소드를 만들면 의존성 주입 컨테이너에 서비스를 등록하는 프로세스를 간단화할 수 있습니다.

Program.cs 파일에 모든 것을 넣는 대신, 의존성 주입 컨테이너에 서비스를 등록하는 확장 메서드를 만들 것입니다. 이렇게 하면 코드를 깨끗하고 조직화된 상태로 유지할 수 있습니다.

Extensions 폴더에 ServiceExtensions.cs라는 새 파일을 만들고 다음 코드를 추가하십시오:

using System.Reflection;
using bookapi_minimal.AppContext;
using FluentValidation;
using Microsoft.EntityFrameworkCore;

namespace bookapi_minimal.Extensions
{
    public static class ServiceExtensions
    {
        public static void AddApplicationServices(this IHostApplicationBuilder builder)
        {
            if (builder == null) throw new ArgumentNullException(nameof(builder));
            if (builder.Configuration == null) throw new ArgumentNullException(nameof(builder.Configuration));

            // 데이터베이스 컨텍스트 추가
            builder.Services.AddDbContext<ApplicationContext>(configure =>
            {
                configure.UseSqlServer(builder.Configuration.GetConnectionString("sqlConnection"));
            });

            // 현재 어셈블리에서 유효성 검사기 추가
            builder.Services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
        }
    }
}

위의 코드를 살펴보겠습니다:

  • AddApplicationServices라는 확장 메서드를 포함하는 ServiceExtensions라는 정적 클래스를 정의합니다. 이 메서드는 애플리케이션의 요청 처리 파이프라인을 구성하는 데 사용되는 IHostApplicationBuilder 인터페이스를 확장합니다.

  • AddApplicationServices 메서드는 IHostApplicationBuilder의 인스턴스를 매개변수로 받습니다. 이 매개변수는 애플리케이션의 구성 및 서비스에 액세스하는 데 사용됩니다.

  • 우리는 ApplicationContext를 의존성 주입 컨테이너에 추가하고 데이터베이스 공급자로 SQL Server를 사용하도록 구성합니다. GetConnectionString 메서드를 사용하여 appsettings.json 파일에서 연결 문자열을 검색합니다.

  • 우리는 AddValidatorsFromAssembly 메서드를 사용하여 현재 assembly에서 validators를 추가합니다. 이 메서드는 IValidator 인터페이스를 구현하는 클래스를 현재 어셈블리에서 스캔하고 의존성 주입 컨테이너에 등록합니다.

다음으로, 연결 문자열을 appsettings.json 파일에 추가해야 합니다. 다음 코드를 appsettings.json 파일에 추가하세요:

{ 
     "ConnectionStrings": {
    "sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
  }
  }

your_password를 실제 SQL Server 암호로 바꿔주세요.

appsettings.json 파일은 다음과 같아야 합니다:


{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "ConnectionStrings": {
    "sqlConnection": "Server=localhost\\SQLEXPRESS02;Database=BookAPIMinimalAPI;Integrated Security=true;TrustServerCertificate=true;"
  },
  "AllowedHosts": "*"
}

축하합니다! 데이터베이스 컨텍스트, 확장 메서드 및 응용 프로그램의 연결 문자열을 성공적으로 생성했습니다. 다음 섹션에서는 계약을 생성할 것입니다.

계약서 작성 방법

계약서는 클라이언트와 서버 간에 교환되는 데이터 구조를 정의하는 데이터 전송 객체(DTO)입니다. 우리의 응용 프로그램에서는 API 엔드포인트에서 송수신되는 데이터를 나타내는 계약서를 작성할 것입니다.

다음과 같이 생성할 계약서 목록은 다음과 같습니다:

  • CreateBookRequest: 새 책을 생성할 때 전송되는 데이터를 나타냅니다.

  • UpdateBookRequest: 기존 책을 업데이트할 때 전송되는 데이터를 나타냅니다.

  • BookResponse: 책을 검색할 때 반환되는 데이터를 나타냅니다.

  • ErrorResponse: 예외가 발생할 때 반환되는 오류 응답을 나타냅니다.

  • ApiResponse: API에 의해 반환되는 응답을 나타냅니다.

Contracts 폴더에서 CreateBookRequest라는 새 파일을 생성하고 다음 코드를 추가하세요:

// Contracts/CreateBookRequest.cs

namespace bookapi_minimal.Contracts
{

    public record CreateBookRequest
    { 

        public string Title { get; init; }
        public string Author { get; init; }
        public string Description { get; init; }
        public string Category { get; init; }
        public string Language { get; init; }
        public int TotalPages { get; init; }
    }
}

Contracts 폴더에서 UpdateBookRequest라는 새 파일을 생성하고 다음 코드를 추가하세요:


// 계약/UpdateBookRequest.cs

namespace bookapi_minimal.Contracts
{

    public record UpdateBookRequest
    {
       public string Title { get; set; }
        public string Author { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public string Language { get; set; }
        public int TotalPages { get; set; }

    }
}

Contracts 폴더에 BookResponse라는 새 파일을 만들고 다음 코드를 추가하십시오:

// 계약/BookResponse.cs
namespace bookapi_minimal.Contracts
{

    public record BookResponse
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public string Author { get; set; }
        public string Description { get; set; }
        public string Category { get; set; }
        public string Language { get; set; }
        public int TotalPages { get; set; }
    }
}

Contracts 폴더에 ErrorResponse라는 새 파일을 만들고 다음 코드를 추가하십시오:



// 계약/ErrorResponse.cs
namespace bookapi_minimal.Contracts
{

        public record ErrorResponse
    {
        public string Title { get; set; }
        public int StatusCode { get; set; }
        public string Message { get; set; }

    }

}

Contracts 폴더에 ApiResponse라는 새 파일을 만들고 다음 코드를 추가하십시오:

// 계약/ApiResponse.cs
namespace bookapi_minimal.Contracts
{

    public class ApiResponse<T>
    {
        public T Data { get; set; }
        public string Message { get; set; }

        public ApiResponse(T data, string message)
        {
            Data = data;
            Message = message;
        }
    }
}

이러한 계약은 클라이언트와 서버 간에 교환되는 데이터의 구조를 정의하는 데 도움이 되어 응용 프로그램에서 데이터 처리를 쉽게 할 수 있습니다.

다음 섹션에서는 응용 프로그램의 비즈니스 로직을 구현하는 서비스를 만들 것입니다.

서비스 추가 방법

서비스는 응용 프로그램에 기능을 제공하는 구성 요소입니다. 우리의 응용 프로그램에서는 책의 CRUD 작업을 처리하고, 책 데이터를 유효성 검사하며, 예외를 처리하는 서비스를 만들 것입니다.

ASP.NET Core에서 서비스는 의존성 주입 컨테이너에 등록되며, 컨트롤러 및 엔드포인트와 같은 다른 구성 요소에 주입할 수 있지만, 이것은 최소 API이므로 서비스를 직접 엔드포인트에 주입할 것입니다.

우리의 서비스를 위한 인터페이스를 만들어 봅시다. Interfaces 폴더에 IBookService.cs라는 새 파일을 만들고 다음 코드를 추가하세요:

 // Interfaces/IBookService.cs



using bookapi_minimal.Contracts;

namespace bookapi_minimal.Interfaces
{
      public interface IBookService
    {
        Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest);
        Task<BookResponse> GetBookByIdAsync(Guid id);
        Task<IEnumerable<BookResponse>> GetBooksAsync();
        Task<BookResponse> UpdateBookAsync(Guid id,  UpdateBookRequest  updateBookRequest);
        Task<bool> DeleteBookAsync(Guid id);
    }
}

위의 코드를 살펴보겠습니다: 우리는 IBookService라는 인터페이스를 정의했고, 이 인터페이스는 책에 대한 CRUD 작업을 처리하는 메소드를 포함합니다. 이 인터페이스는 다음 메소드를 정의합니다:

  • AddBookAsync: 데이터베이스에 새 책을 추가합니다.

  • GetBookByIdAsync: ID로 책을 검색합니다.

  • GetBooksAsync: 데이터베이스에서 모든 책을 검색합니다.

  • UpdateBookAsync: 기존 책을 업데이트합니다.

우리는 이전에 만든 Contract를 Contracts 폴더에서 사용하고 있습니다. IBookService 인터페이스는 서비스 클래스에서 구현될 메소드의 구조를 정의합니다. 이는 인터페이스를 구현에서 분리하여 코드를 유지보수하고 테스트하기 쉽게 만들어줍니다.

이제 인터페이스를 만들었으니, 인터페이스를 구현하는 서비스 클래스를 만들어 봅시다.

책 서비스 구현 방법

이 서비스는 IBookService 인터페이스를 구현하고 우리 애플리케이션의 비즈니스 로직을 제공할 것입니다. Services 폴더에서 BookService.cs라는 새 파일을 만드세요. 초기 파일은 다음과 같아야 합니다:


// Services/BookService.cs

namespace bookapi_minimal.Services
{
    public class BookService
    {

    }
}

첫 번째 할 일은 BookService 클래스에 인터페이스를 추가하는 것입니다. 다음과 같이 BookService 클래스를 IBookService 인터페이스를 구현하도록 업데이트하세요:



// Services/BookService.cs



using bookapi_minimal.Interfaces;

namespace bookapi_minimal.Services
{
    public class BookService:IBookService
    {

    }
}

이렇게 하면 VS Code에서 오류가 표시될 수 있습니다. 이는 인터페이스의 메서드를 구현하지 않았기 때문입니다. 이제 BookService 클래스에서 메서드를 구현해 봅시다.

VS Code에서는 단축키 Ctrl + .를 사용하여 인터페이스의 메서드를 구현할 수 있습니다. 그러면 다음과 같이 코드가 생성됩니다:


using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;

namespace bookapi_minimal.Services
{
     // 책을 관리하는 서비스 클래스
   public class BookService : IBookService
   {
       // 데이터베이스에 새 책을 추가하는 메서드
       public Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
       {
           throw new NotImplementedException();
       }

      // 데이터베이스에서 책을 삭제하는 메서드
       public Task<bool> DeleteBookAsync(Guid id)
       {
           throw new NotImplementedException();
       }

       // ID로 데이터베이스에서 책을 가져오는 메서드

       public Task<BookResponse> GetBookByIdAsync(Guid id)
       {
           throw new NotImplementedException();
       }

      // 데이터베이스에서 모든 책을 가져오는 메서드
       public Task<IEnumerable<BookResponse>> GetBooksAsync()
       {
           throw new NotImplementedException();
       }

       // 데이터베이스에서 책을 업데이트하는 메서드
       public Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest updateBookRequest)
       {
           throw new NotImplementedException();
       }
   }
}

이제 인터페이스의 메서드가 BookService 클래스에 구현되었음을 볼 수 있습니다. 다음 섹션에서 각 메서드에 대한 비즈니스 로직을 구현할 것입니다.

그 전에 BookService 클래스에 필요한 종속성을 추가해 봅시다. BookService 클래스에 ApplicationContextILogger 종속성을 주입해야 합니다. ApplicationContext은 데이터베이스와 상호 작용하는 데 사용되고, ILogger는 로깅에 사용됩니다.

종속성을 주입하려면 다음과 같이 BookService 클래스를 업데이트하세요:


// Services/BookService.cs

// ...
 private readonly ApplicationContext _context; // 데이터베이스 컨텍스트
  private readonly ILogger<BookService> _logger; // 정보와 오류를 기록하는 로거

//..

종속성을 추가했으므로 BookService 생성자를 업데이트하여 해당 종속성을 수락해야 합니다. 다음과 같이 BookService 생성자를 업데이트하세요:


// Services/BookService.cs

// ...

  // 데이터베이스 컨텍스트 및 로거를 초기화하는 생성자
 public BookService(ApplicationContext context, ILogger<BookService> logger)
 {
            _context = context;
            _logger = logger;
}

// ...

종속성을 추가하고 생성자를 업데이트했으므로 이제 BookService 클래스의 각 메서드에 대한 비즈니스 로직을 구현할 수 있습니다.

클래스에서 CREATE, READ, UPDATE 및 DELETE 작업을 위한 로직을 작성합시다.

AddBookAsync 메서드 구현 방법

이전에 언급한대로, AddBookAsync 메서드를 사용하여 데이터베이스에 새 책을 추가할 것입니다. 이 메서드에서는 새 책 엔티티를 생성하고, CreateBookRequest 객체에서 데이터를 책 엔티티로 매핑하고, 책 엔티티를 데이터베이스에 저장할 것입니다. 또한 책 엔티티를 BookResponse 객체로 반환할 것입니다.

BookService 클래스의 AddBookAsync 메서드를 다음과 같이 업데이트하세요:

// Services/BookService.cs

// ...
 /// <summary>
        /// 새 책 추가
        /// </summary>
        /// <param name="createBookRequest">추가할 책 요청</param>
        /// <returns>생성된 책의 세부 정보</returns>
        public async Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
        {
            try
            {
                var book = new BookModel
                {
                    Title = createBookRequest.Title,
                    Author = createBookRequest.Author,
                    Description = createBookRequest.Description,
                    Category = createBookRequest.Category,
                    Language = createBookRequest.Language,
                    TotalPages = createBookRequest.TotalPages
                };

                // 데이터베이스에 책 추가
                _context.Books.Add(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book added successfully.");

                // 생성된 책의 세부 정보 반환
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error adding book: {ex.Message}");
                throw;
            }
        }
// ...

이 코드에서는 새로운 책 엔티티를 CreateBookRequest 객체에서 생성하고, CreateBookRequest 객체에서 데이터를 책 엔티티로 매핑하여 데이터베이스에 책 엔티티를 저장하고, 책 엔티티를 BookResponse 객체로 반환합니다.

ILogger 의존성을 사용하여 정보와 오류를 기록합니다. 프로세스 중에 예외가 발생하면 오류 메시지를 기록하고 예외를 다시 던집니다.

AddBookAsync 메서드를 구현했으므로, 이제 GetBookByIdAsync 메서드를 구현해 봅시다.

GetBookByIdAsync 메서드 구현 방법

GetBookByIdAsync 메서드는 데이터베이스에서 ID로 책을 검색하는 데 사용됩니다. 이 메서드에서는 지정된 ID로 책을 검색하고, 책 엔티티를 BookResponse 객체로 매핑하여 BookResponse 객체를 반환합니다.

BookService 클래스의 GetBookByIdAsync 메서드를 다음과 같이 업데이트하세요:


// Services/BookService.cs

//... 

    /// <summary>
        /// ID로 책을 가져옵니다
        /// </summary>
        /// <param name="id">책의 ID</param>
        /// <returns>책의 상세 정보</returns>
        public async Task<BookResponse>  GetBookByIdAsync(Guid id)
        {
            try
            {
                // ID로 책을 찾습니다
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // 책의 상세 정보를 반환합니다
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving book: {ex.Message}");
                throw;
            }
        }

//...

이 코드에서는 지정된 ID로 데이터베이스에서 책을 쿼리하고, 책 엔티티를 BookResponse 객체에 매핑한 후, BookResponse 객체를 반환합니다. 또한 ILogger 의존성을 사용하여 정보와 오류를 기록합니다.

지정된 ID로 책을 찾을 수 없는 경우 경고 메시지를 기록하고 null을 반환합니다. 프로세스 중에 예외가 발생하면 오류 메시지를 기록하고 예외를 다시 던집니다.

이제 GetBookByIdAsync 메서드를 구현했으니, GetBooksAsync 메서드를 구현해 봅시다.

GetBooksAsync 메서드를 구현하는 방법

GetBooksAsync 메서드는 데이터베이스에서 모든 책을 검색하는 데 사용됩니다. 이 메서드에서는 데이터베이스에서 모든 책을 쿼리하고, 각 책 엔티티를 BookResponse 객체로 매핑한 후, BookResponse 객체의 목록을 반환합니다.

다음과 같이 BookService 클래스의 GetBooksAsync 메서드를 업데이트하십시오:



// Services/BookService.cs

//... 


  /// <summary>
        /// 모든 책 가져오기
        /// </summary>
        /// <returns>모든 책의 목록</returns>
        public async Task<IEnumerable<BookResponse>> GetBooksAsync()
        {
            try
            {
                // 데이터베이스에서 모든 책 가져오기
                var books = await _context.Books.ToListAsync();

                // 모든 책의 세부정보 반환하기
                return books.Select(book => new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                });
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving books: {ex.Message}");
                throw;
            }
        }
//...

여기에서 우리는 데이터베이스에서 모든 책을 쿼리하고, 각 책 엔티티를 BookResponse 객체로 매핑한 후, BookResponse 객체의 목록을 반환합니다. 또한 ILogger 종속성을 사용하여 정보와 오류를 기록하고 있습니다. 프로세스 중에 예외가 발생하면 오류 메시지를 기록하고 예외를 다시 발생시킵니다.

이제 GetBooksAsync 메서드를 구현했으므로 UpdateBookAsync 메서드를 구현해 보겠습니다.

UpdateBookAsync 메서드 구현 방법

UpdateBookAsync 메서드는 데이터베이스에서 기존 도서를 업데이트하는 데 사용됩니다. 이 메서드에서는 지정된 ID로 데이터베이스에서 도서를 쿼리하고, UpdateBookRequest 객체의 데이터로 도서 엔티티를 업데이트하고, 업데이트된 도서 엔티티를 데이터베이스에 저장한 후 업데이트된 도서 엔티티를 BookResponse 객체로 반환합니다.

BookService 클래스의 UpdateBookAsync 메서드를 다음과 같이 업데이트합니다:

// Services/BookService.cs
 //...
 /// <summary>
        /// 기존 도서 업데이트
        /// </summary>
        /// <param name="id">업데이트할 도서의 ID</param>
        /// <param name="book">업데이트된 도서 모델</param>
        /// <returns>업데이트된 도서의 세부정보</returns>
        public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
        {
            try
            {
                // ID로 기존 도서 찾기
                var existingBook = await _context.Books.FindAsync(id);
                if (existingBook == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // 도서 세부정보 업데이트
                existingBook.Title = book.Title;
                existingBook.Author = book.Author;
                existingBook.Description = book.Description;
                existingBook.Category = book.Category;
                existingBook.Language = book.Language;
                existingBook.TotalPages = book.TotalPages;

                // 변경 사항을 데이터베이스에 저장
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book updated successfully.");

                // 업데이트된 도서의 세부정보 반환
                return new BookResponse
                {
                    Id = existingBook.Id,
                    Title = existingBook.Title,
                    Author = existingBook.Author,
                    Description = existingBook.Description,
                    Category = existingBook.Category,
                    Language = existingBook.Language,
                    TotalPages = existingBook.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error updating book: {ex.Message}");
                throw;
            }
        }
//...

여기서는 지정된 ID를 가진 책에 대한 데이터베이스 쿼리를 수행하고, UpdateBookRequest 객체에서 받은 데이터로 책 엔티티를 업데이트한 뒤, 업데이트된 책 엔티티를 데이터베이스에 저장하고 BookResponse 객체로 업데이트된 책 엔티티를 반환합니다. 우리는 또한 ILogger 종속성을 사용하여 정보와 오류를 로깅합니다.

지정된 ID를 가진 책을 찾을 수 없는 경우, 경고 메시지를 로깅하고 null을 반환합니다. 프로세스 중에 예외가 발생하는 경우, 오류 메시지를 로깅하고 예외를 다시 던집니다.

이제 UpdateBookAsync 메서드를 구현했으므로, DeleteBookAsync 메서드를 구현해 봅시다.

DeleteBookAsync 메서드 구현 방법

DeleteBookAsync 메서드는 데이터베이스에서 기존 책을 삭제하는 데 사용됩니다. 이 메서드에서는 지정된 ID를 가진 책에 대한 데이터베이스 쿼리를 수행하고, 데이터베이스에서 책 엔티티를 제거한 뒤, 책이 성공적으로 삭제되었는지를 나타내는 부울 값이 반환됩니다.

BookService 클래스의 DeleteBookAsync 메서드를 다음과 같이 업데이트하세요:

// Services/BookService.cs

 //...


/// <summary>
        /// ID로 책 삭제
        /// </summary>
        /// <param name="id">삭제할 책의 ID</param>
        /// <returns>책이 삭제되면 true, 그렇지 않으면 false</returns>
        public async Task<bool> DeleteBookAsync(Guid id)
        {
            try
            {
                // ID로 책 찾기
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return false;
                }

                // 데이터베이스에서 책 제거
                _context.Books.Remove(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation($"Book with ID {id} deleted successfully.");
                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error deleting book: {ex.Message}");
                throw;
            }
        }
//...

이 코드에서는 지정된 ID로 데이터베이스에서 책을 쿼리하고, 책 엔티티를 데이터베이스에서 제거한 후, 책이 성공적으로 삭제되었는지 여부를 나타내는 불리언 값을 반환합니다. 또한 ILogger 의존성을 사용하여 정보와 오류를 기록합니다.

지정된 ID의 책을 찾지 못하면 경고 메시지를 기록하고 false를 반환합니다. 프로세스 중에 예외가 발생하면 오류 메시지를 기록하고 예외를 다시 발생시킵니다.

이제 AddBookAsync, GetBookByIdAsync, GetBooksAsync, UpdateBookAsync, 그리고 DeleteBookAsync 메소드에 대한 비즈니스 로직을 성공적으로 구현했습니다. 이러한 메소드들은 책에 대한 CRUD 작업을 처리하고, 책 데이터를 유효성 검사하며, 예외를 처리합니다. 이제 당신의 BookService 클래스는 다음과 같아야 합니다:



using bookapi_minimal.AppContext;
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;
using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;

namespace bookapi_minimal.Services
{
    public class BookService : IBookService
    {
          private readonly ApplicationContext _context; // 데이터베이스 컨텍스트
        private readonly ILogger<BookService> _logger; // 정보와 오류를 기록하는 로거
          // 데이터베이스 컨텍스트와 로거를 초기화하는 생성자
        public BookService(ApplicationContext context, ILogger<BookService> logger)
        {
            _context = context;
            _logger = logger;
        }

           /// 새 책 추가
        /// </summary>
        /// <param name="createBookRequest">추가할 책 요청</param>
        /// <returns>생성된 책의 세부 정보</returns>
        public async Task<BookResponse> AddBookAsync(CreateBookRequest createBookRequest)
        {
            try
            {
                var book = new BookModel
                {
                    Title = createBookRequest.Title,
                    Author = createBookRequest.Author,
                    Description = createBookRequest.Description,
                    Category = createBookRequest.Category,
                    Language = createBookRequest.Language,
                    TotalPages = createBookRequest.TotalPages
                };

                // 데이터베이스에 책 추가
                _context.Books.Add(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book added successfully.");

                // 생성된 책의 세부 정보 반환
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error adding book: {ex.Message}");
                throw;
            }
        }

          /// <summary>
        /// ID로 책 가져오기
        /// </summary>
        /// <param name="id">책의 ID</param>
        /// <returns>책의 세부 정보</returns>
        public async Task<BookResponse>  GetBookByIdAsync(Guid id)
        {
            try
            {
                // ID로 책 찾기
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // 책의 세부 정보 반환
                return new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving book: {ex.Message}");
                throw;
            }
        }



  /// <summary>
        /// 모든 책 가져오기
        /// </summary>
        /// <returns>모든 책의 목록</returns>
        public async Task<IEnumerable<BookResponse>> GetBooksAsync()
        {
            try
            {
                // 데이터베이스에서 모든 책 가져오기
                var books = await _context.Books.ToListAsync();

                // 모든 책의 세부 정보 반환
                return books.Select(book => new BookResponse
                {
                    Id = book.Id,
                    Title = book.Title,
                    Author = book.Author,
                    Description = book.Description,
                    Category = book.Category,
                    Language = book.Language,
                    TotalPages = book.TotalPages
                });
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error retrieving books: {ex.Message}");
                throw;
            }
        }


         /// <summary>
        /// 기존 책 업데이트
        /// </summary>
        /// <param name="id">업데이트할 책의 ID</param>
        /// <param name="book">업데이트된 책 모델</param>
        /// <returns>업데이트된 책의 세부 정보</returns>
        public async Task<BookResponse> UpdateBookAsync(Guid id, UpdateBookRequest book)
        {
            try
            {
                // ID로 기존 책 찾기
                var existingBook = await _context.Books.FindAsync(id);
                if (existingBook == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return null;
                }

                // 책 세부 정보 업데이트
                existingBook.Title = book.Title;
                existingBook.Author = book.Author;
                existingBook.Description = book.Description;
                existingBook.Category = book.Category;
                existingBook.Language = book.Language;
                existingBook.TotalPages = book.TotalPages;

                // 변경 사항을 데이터베이스에 저장
                await _context.SaveChangesAsync();
                _logger.LogInformation("Book updated successfully.");

                // 업데이트된 책의 세부 정보 반환
                return new BookResponse
                {
                    Id = existingBook.Id,
                    Title = existingBook.Title,
                    Author = existingBook.Author,
                    Description = existingBook.Description,
                    Category = existingBook.Category,
                    Language = existingBook.Language,
                    TotalPages = existingBook.TotalPages
                };
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error updating book: {ex.Message}");
                throw;
            }
        }



        /// <summary>
        /// ID로 책 삭제
        /// </summary>
        /// <param name="id">삭제할 책의 ID</param>
        /// <returns>책이 삭제되면 true, 그렇지 않으면 false</returns>
        public async Task<bool> DeleteBookAsync(Guid id)
        {
            try
            {
                // ID로 책 찾기
                var book = await _context.Books.FindAsync(id);
                if (book == null)
                {
                    _logger.LogWarning($"Book with ID {id} not found.");
                    return false;
                }

                // 데이터베이스에서 책 삭제
                _context.Books.Remove(book);
                await _context.SaveChangesAsync();
                _logger.LogInformation($"Book with ID {id} deleted successfully.");
                return true;
            }
            catch (Exception ex)
            {
                _logger.LogError($"Error deleting book: {ex.Message}");
                throw;
            }
        }

    }
}

축하합니다! AddBookAsync, GetBookByIdAsync, GetBooksAsync, UpdateBookAsync, DeleteBookAsync 메서드에 대한 비즈니스 로직을 성공적으로 구현했습니다.

할 일이 하나 더 있습니다: 서비스를 확장 메서드에 등록해야 합니다. 그럼 바로 해보겠습니다.

ServiceExtensions.cs 파일에 다음 코드를 추가하세요:


// Extensions/ServiceExtensions.cs

//..

 builder.Services.AddScoped<IBookService, BookService>();
//...

이렇게 하면 BookService 클래스가 scoped 서비스로 등록됩니다. 이는 서비스가 요청 당 한 번 생성되고 요청이 완료된 후에 폐기됨을 의미합니다.

이제 서비스가 작동 중이므로 예외 클래스를 생성해보겠습니다.

예외 생성 방법

예외를 적절히 처리하는 것은 응용 프로그램의 안정성과 신뢰성을 보장하는 데 중요합니다. ASP.NET Core의 맥락에서 두 가지 주요 예외 유형이 있습니다:

  • 시스템 예외: .NET 런타임이나 기본 시스템에서 throw되는 예외입니다.

  • 애플리케이션 예외: 애플리케이션 코드에서 발생하여 특정 오류나 조건을 처리하는 예외입니다.

ASP.NET Core와 .NET 8에서는 글로벌 예외 처리를 위한 새로운 기능이 도입되었습니다. 이 기능은 애플리케이션 전역에서 예외를 처리할 수 있게 하여 오류 관리와 일관된 사용자 경험을 제공하는 데 도움을 줍니다.

우리 애플리케이션에서는 특정 오류와 조건을 처리하기 위해 사용자 정의 예외 클래스를 생성할 것입니다. 또한 글로벌 예외 처리 기능을 활용하여 애플리케이션 전반에 걸쳐 예외를 관리하여 일관된 오류 처리 방식을 보장할 것입니다.

다음과 같은 예외 클래스를 생성할 것입니다:

  • 책을 찾을 수 없음 예외: 지정된 ID를 가진 책을 찾을 수 없을 때 발생합니다.

  • 책이 존재하지 않음 예외: 지정된 ID를 가진 책이 존재하지 않을 때 발생합니다.

  • 글로벌 예외 처리기: 애플리케이션에서 예외를 전역적으로 처리합니다.

예외 폴더에 NoBookFoundException.cs라는 이름의 새 파일을 만들고 다음 코드를 추가합니다:


// 예외/NoBookFoundException.cs

namespace bookapi_minimal.Exceptions
{

    public class NoBookFoundException : Exception
    {

        public NoBookFoundException() : base("No books found")
        {}
    }
}

이 코드에서는 NoBookFoundException이라는 사용자 정의 예외 클래스를 생성하고 Exception 클래스를 상속합니다. NoBookFoundException 클래스는 데이터베이스에서 책을 찾을 수 없는 시나리오를 처리하는 데 사용됩니다. 또한 예외를 위한 사용자 정의 오류 메시지를 제공합니다.

Exceptions 폴더에서 BookDoesNotExistException.cs라는 새 파일을 만들고 다음 코드를 추가하십시오:

namespace bookapi_minimal.Exceptions
{
     public class BookDoesNotExistException : Exception
    {
        private int id { get; set; }

        public BookDoesNotExistException(int id) : base($"Book with id {id} does not exist")
        {
            this.id = id;
        } 

    }
}

이 코드에서는 BookDoesNotExistException이라는 사용자 정의 예외 클래스를 생성하고 Exception 클래스를 상속합니다. BookDoesNotExistException 클래스는 데이터베이스에 지정된 ID를 가진 책이 존재하지 않는 시나리오를 처리하는 데 사용됩니다. 또한 예외를 위한 사용자 정의 오류 메시지를 제공합니다.

Exceptions 폴더에서 GlobalExceptionHandler.cs라는 새 파일을 만들고 다음 코드를 추가하십시오:

// Exceptions/GlobalExceptionHandler.cs

using System.Net;
using bookapi_minimal.Contracts;
using Microsoft.AspNetCore.Diagnostics;

namespace bookapi_minimal.Exceptions
{

   // Global exception handler class implementing IExceptionHandler
    public class GlobalExceptionHandler : IExceptionHandler
    {
        private readonly ILogger<GlobalExceptionHandler> _logger;

        // Constructor to initialize the logger
        public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
        {
            _logger = logger;
        }

        // Method to handle exceptions asynchronously
        public async ValueTask<bool> TryHandleAsync(
            HttpContext httpContext,
            Exception exception,
            CancellationToken cancellationToken)
        {
            // Log the exception details
            _logger.LogError(exception, "An error occurred while processing your request");

            var errorResponse = new ErrorResponse
            {
                Message = exception.Message,
                Title = exception.GetType().Name
            };

            // Determine the status code based on the type of exception
            switch (exception)
            {
                case BadHttpRequestException:
                    errorResponse.StatusCode = (int)HttpStatusCode.BadRequest;
                    break;

                case NoBookFoundException:
                case BookDoesNotExistException:
                    errorResponse.StatusCode = (int)HttpStatusCode.NotFound;
                    break;

                default:
                    errorResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
                    break;
            }

            // Set the response status code
            httpContext.Response.StatusCode = errorResponse.StatusCode;

            // Write the error response as JSON
            await httpContext.Response.WriteAsJsonAsync(errorResponse, cancellationToken);

            // Return true to indicate that the exception was handled
            return true;
        }
    }
}

Let’s break down the code above:

  • We define a class named GlobalExceptionHandler that implements the IExceptionHandler interface. The IExceptionHandler interface is used to handle exceptions globally in the application.

  • The GlobalExceptionHandler class contains a constructor that initializes the ILogger<GlobalExceptionHandler> dependency. The ILogger is used for logging information and errors.

  • TryHandleAsync 메서드는 예외를 비동기적으로 처리하는 데 사용됩니다. 이 메서드는 HttpContext, Exception, 및 CancellationToken을 매개변수로 받습니다.

  • 예외 세부 정보를 ILogger 종속성을 사용하여 기록합니다.

  • ErrorResponse 객체를 생성하여 API에서 반환된 오류 응답을 나타냅니다. ErrorResponse 객체에는 오류 메시지, 제목, 및 상태 코드가 포함되어 있습니다.

  • 예외의 유형에 따라 상태 코드를 결정합니다. 예외가 BadHttpRequestException인 경우 상태 코드를 BadRequest로 설정합니다. 예외가 NoBookFoundException 또는 BookDoesNotExistException인 경우 상태 코드를 NotFound로 설정합니다. 그렇지 않은 경우 상태 코드를 InternalServerError로 설정합니다.

  • httpContext.Response.StatusCode 속성을 사용하여 응답 상태 코드를 설정합니다.

  • httpContext.Response.WriteAsJsonAsync 메소드를 사용하여 JSON 형식으로 오류 응답을 작성합니다.

  • 예외가 성공적으로 처리되었음을 나타내기 위해 true를 반환합니다.

이제 예외 클래스를 생성했으므로 의존성 주입 컨테이너에 GlobalExceptionHandler를 등록합시다. 의존성 주입 컨테이너에 서비스를 등록하기 위한 확장 메소드를 만들었기 때문에 GlobalExceptionHandlerServiceExtensions 클래스에 추가할 것입니다.

Extensions 폴더의 ServiceExtensions 클래스를 다음과 같이 업데이트하세요:


// Extensions/ServiceExtensions.cs
//...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

builder.Services.AddProblemDetails();

//...

AddExceptionHandler 메서드는 GlobalExceptionHandler을 의존성 주입 컨테이너에 등록합니다. AddProblemDetails 메서드는 ProblemDetails 클래스를 의존성 주입 컨테이너에 등록합니다.

의존성 주입 컨테이너에 GlobalExceptionHandler을 등록했으므로 이제 응용 프로그램 전반에서 예외를 전역적으로 처리하는 데 사용할 수 있습니다. 다음 섹션에서는 책 데이터와 상호 작용하는 API 엔드포인트를 만들 것입니다.

API 엔드포인트 만드는 방법

ASP.NET Core의 최소 API 컨텍스트에서 엔드포인트를 설정하는 다양한 방법이 있습니다.

Program.cs 파일에서 직접 정의할 수 있습니다. 그러나 프로젝트가 성장하고 더 많은 엔드포인트나 기능을 추가해야 할 때 코드를 더 잘 구성하는 것이 도움이 됩니다. 모든 엔드포인트를 처리하는 별도의 클래스를 만드는 한 가지 방법이 있습니다.

위에서 설명한 대로 최소 API는 전통적인 ASP.NET Core 응용 프로그램과 같이 컨트롤러나 뷰를 사용하지 않습니다. 대신 MapGet, MapPost, MapPut, MapDelete와 같은 메서드를 사용하여 API 엔드포인트의 HTTP 메서드 및 라우트를 정의합니다.

시작하려면 Endpoints 폴더로 이동하여 BookEndpoints.cs라는 새 파일을 만듭니다. 파일에 다음 코드를 추가하세요:


// Endpoints/BookEndpoints.cs



namespace bookapi_minimal.Endpoints
{
     public static class BookEndPoint
    {
        public static IEndpointRouteBuilder MapBookEndPoint(this IEndpointRouteBuilder app)
        {


            return app;
        }
    }
}

BookEndpoints 클래스에는 MapBookEndPoint 메서드가 있으며 IEndpointRouteBuilder 객체를 반환합니다. IEndpointRouteBuilder 객체는 API 엔드포인트의 HTTP 메서드 및 라우트를 정의하는 데 사용됩니다. 다음 섹션에서는 책을 생성, 읽기, 업데이트, 삭제하는 API 엔드포인트를 정의할 것입니다.

AddBookAsync Books 엔드포인트를 생성하는 방법

이 섹션에서는 AddBookAsync 엔드포인트를 생성할 것입니다. 이 엔드포인트는 JSON 페이로드로 Book 객체를 수신하고 데이터베이스에 추가할 것입니다. 이 엔드포인트에 대한 HTTP 메서드 및 라우트를 정의하기 위해 MapPost 메서드를 사용할 것입니다.


// Endpoints/BookEndpoints.cs


//...
   // 새 책을 추가하는 엔드포인트
      app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
        {
        var result = await bookService.AddBookAsync(createBookRequest);
        return Results.Created($"/books/{result.Id}", result); 
        });


//...
  • 라우트 정의: MapPost 메서드는 엔드포인트의 라우트를 /books로 정의합니다.

  • 요청 모델: 엔드포인트는 JSON 페이로드로 CreateBookRequest 객체를 수신합니다. CreateBookRequest 객체에는 새 책을 생성하는 데 필요한 데이터가 포함되어 있습니다.

  • 응답 모델: 이 엔드포인트는 JSON 페이로드로 Book 객체를 반환합니다. Book 객체는 새로 생성된 책에 대한 데이터를 포함합니다.

  • 반환 값: 이 엔드포인트는 Created 결과를 반환합니다. Created 결과는 새로 생성된 책의 위치와 Book 객체를 포함합니다.

GetBookAsync 책 엔드포인트 생성 방법

이 섹션에서는 GetBookAsync 엔드포인트를 생성합니다. 이 엔드포인트는 쿼리 매개변수로 책 ID를 받아 해당 ID를 가진 책을 반환합니다. 우리는 MapGet 메서드를 사용하여 이 엔드포인트의 HTTP 메서드와 경로를 정의할 것입니다.

다음 코드를 BookEndpoints 클래스에 추가하세요:


// 엔드포인트/BookEndpoints.cs

// ...
    // 모든 책을 가져오는 엔드포인트
    app.MapGet("/books", async (IBookService bookService) =>
     {
    var result = await bookService.GetBooksAsync();
    return Results.Ok(result);
});


//...
  • 루트 정의: MapGet 메서드는 엔드포인트의 경로를 /books로 정의합니다.

  • 요청 모델: 엔드포인트는 JSON 페이로드로 Book 객체를 허용합니다. Book 객체에는 새 책을 만들기 위해 필요한 데이터가 포함되어 있습니다.

  • 응답 모델: 엔드포인트는 JSON 페이로드로 Book 객체를 반환합니다. Book 객체에는 새로 생성된 책의 데이터가 포함되어 있습니다.

  • 반환 값: 엔드포인트는 Ok 결과를 반환합니다. Ok 결과에는 Book 객체가 포함됩니다.

GetBookByIdAsync Book 엔드포인트를 만드는 방법

이 섹션에서는 GetBookByIdAsync 엔드포인트를 생성할 것입니다. 이 엔드포인트는 루트 매개변수로 책 ID를 수락하고 해당 ID를 가진 책을 반환할 것입니다. 이 엔드포인트에 대한 HTTP 방법과 라우트를 정의하기 위해 MapGet 메서드를 사용할 것입니다.

BookEndpoints 클래스에 다음 코드를 추가하세요:


// Endpoints/BookEndpoints.cs
//...
// ID로 책 가져오는 엔드포인트

  app.MapGet("/books/{id:guid}", async (Guid id, IBookService bookService) =>
  {
    var result = await bookService.GetBookByIdAsync(id);
    return result != null ? Results.Ok(result) : Results.NotFound();
});

//...
  • 라우트 정의: MapGet 메서드는 엔드포인트의 라우트를 /books/{id:guid}로 정의합니다. {id:guid} 매개변수는 id 매개변수가 GUID여야 함을 지정합니다.

  • 요청 모델: 이 엔드포인트는 JSON 페이로드로 Book 객체를 수락합니다. Book 객체에는 새 책을 만들기 위해 필요한 데이터가 포함되어 있습니다.

  • 응답 모델: 이 엔드포인트는 JSON 페이로드로 Book 객체를 반환합니다. Book 객체에는 새로 생성된 책의 데이터가 포함되어 있습니다.
  • 반환 값: 엔드포인트는 책을 찾을 수 있는 경우 Ok 결과를 반환합니다. 책을 찾을 수 없는 경우 NotFound 결과가 반환됩니다.

UpdateBookAsync 책 엔드포인트 생성 방법

이번 섹션에서는 UpdateBookAsync 엔드포인트를 생성합니다. 이 엔드포인트는 경로 매개변수로 책 ID를 받고, JSON 페이로드로 Book 객체를 받아 지정된 ID의 책을 업데이트합니다. 우리는 MapPut 메서드를 사용하여 이 엔드포인트의 HTTP 메서드와 경로를 정의할 것입니다.

다음 코드를 BookEndpoints 클래스에 추가하세요:


// Endpoints/BookEndpoints.cs

//...
   // ID로 책을 업데이트하는 엔드포인트
    app.MapPut("/books/{id:guid}", async (Guid id, UpdateBookRequest updateBookRequest, IBookService bookService) =>
 {
var result = await bookService.UpdateBookAsync(id, updateBookRequest);
return result != null ? Results.Ok(result) : Results.NotFound();
});

//...
  • 경로 정의: MapPut 메서드는 엔드포인트의 경로를 /books/{id:guid}로 정의합니다. {id:guid} 매개변수는 id 매개변수가 GUID여야 함을 지정합니다.

  • 요청 모델: 이 엔드포인트는 JSON 페이로드로 Book 객체를 수락합니다. Book 객체에는 새 책을 만드는 데 필요한 데이터가 포함되어 있습니다.

  • 응답 모델: 이 엔드포인트는 JSON 페이로드로 Book 객체를 반환합니다. Book 객체에는 새로 만든 책의 데이터가 포함되어 있습니다.

  • 반환 값: 이 엔드포인트는 책을 발견하면 Ok 결과를 반환합니다. 책을 찾을 수 없는 경우 NotFound 결과가 반환됩니다.

DeleteBookAsync 책 엔드포인트를 생성하는 방법

이 섹션에서는 DeleteBookAsync 엔드포인트를 생성할 것입니다. 이 엔드포인트는 루트 매개변수로 책 ID를 받아들이고 해당 ID를 갖는 책을 삭제할 것입니다. 이 엔드포인트에 대한 HTTP 메소드와 라우트를 정의하기 위해 MapDelete 메소드를 사용할 것입니다.


// Endpoints/BookEndpoints.cs

//...
   // ID로 책 삭제하는 엔드포인트
 app.MapDelete("/books/{id:guid}", async (Guid id, IBookService bookService) =>
{
var result = await bookService.DeleteBookAsync(id);
   return result ? Results.NoContent() : Results.NotFound();
});


//...
  • 라우트 정의: MapDelete 메소드는 엔드포인트의 라우트를 /books/{id:guid}로 정의합니다. {id:guid} 매개변수는 id 매개변수가 GUID여야 함을 지정합니다.

  • 요청 모델: 이 엔드포인트는 JSON 페이로드로 Book 객체를 받습니다. Book 객체에는 새 책을 생성하는 데 필요한 데이터가 포함되어 있습니다.

  • 응답 모델: 이 엔드포인트는 JSON 페이로드로 Book 객체를 반환합니다. Book 객체에는 새로 생성된 책의 데이터가 포함되어 있습니다.
  • 반환 값: 책이 성공적으로 삭제된 경우 NoContent 결과가 반환됩니다. 책을 찾을 수 없는 경우 NotFound 결과가 반환됩니다.

이제 모든 책 엔드포인트에 대한 메서드를 정의했습니다. 따라서 엔드포인트 클래스는 다음과 같아야 합니다:

// Endpoints/BookEndpoints.cs
using bookapi_minimal.Contracts;
using bookapi_minimal.Interfaces;

namespace bookapi_minimal.Endpoints
{
     public static class BookEndPoint
    {
        public static IEndpointRouteBuilder MapBookEndPoint(this IEndpointRouteBuilder app)
        {
            // 엔드포인트 정의

            // 새 책 추가를 위한 엔드포인트
            app.MapPost("/books", async (CreateBookRequest createBookRequest, IBookService bookService) =>
            {
                var result = await bookService.AddBookAsync(createBookRequest);
                return Results.Created($"/books/{result.Id}", result); 
            });


               // 모든 책 가져오기를 위한 엔드포인트
            app.MapGet("/books", async (IBookService bookService) =>
            {
                var result = await bookService.GetBooksAsync();
                return Results.Ok(result);
            });

            // ID로 책 가져오기를 위한 엔드포인트
            app.MapGet("/books/{id:guid}", async (Guid id, IBookService bookService) =>
            {
                var result = await bookService.GetBookByIdAsync(id);
                return result != null ? Results.Ok(result) : Results.NotFound();
            });


            // ID로 책 업데이트를 위한 엔드포인트
            app.MapPut("/books/{id:guid}", async (Guid id, UpdateBookRequest updateBookRequest, IBookService bookService) =>
            {
                var result = await bookService.UpdateBookAsync(id, updateBookRequest);
                return result != null ? Results.Ok(result) : Results.NotFound();
            });

            // ID로 책 삭제를 위한 엔드포인트
            app.MapDelete("/books/{id:guid}", async (Guid id, IBookService bookService) =>
            {
                var result = await bookService.DeleteBookAsync(id);
                return result ? Results.NoContent() : Results.NotFound();
            });

            return app;
        }
    }
}

축하합니다! 책 API의 모든 엔드포인트를 만들었습니다. 엔드포인트는 책에 대한 CRUD 작업을 처리하고 요청 및 데이터에 따라 적절한 응답을 반환합니다.

엔드포인트 등록 방법

도서 API의 API 엔드포인트를 정의한 후, 다음 단계는 이러한 엔드포인트를 Program.cs 파일에 등록하는 것입니다. 우리는 MapBookEndpoints 메서드를 사용하여 도서 엔드포인트를 등록할 것입니다.

또한 Program.cs 클래스를 정리하여 체계적이고 유지보수 가능하도록 유지해야 합니다.

// Program.cs

using System.Reflection;
using bookapi_minimal.Endpoints;
using bookapi_minimal.Services;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);


builder.AddApplicationServices();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c=>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "Mimal API", Version = "v1", Description = "Showing how you can build minimal " +
        "api with .net" });


    // Swagger JSON 및 UI에 대한 주석 경로 설정.
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);

});
var app = builder.Build();

// HTTP 요청 파이프라인 구성.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseExceptionHandler();


app.MapGroup("/api/v1/")
   .WithTags(" Book endpoints")
   .MapBookEndPoint();

app.Run();

  • AddApplicationServices: 이 메서드는 API에 필요한 서비스를 등록합니다. 이것은 의존성 주입 컨테이너에 서비스를 추가하기 위해 이전에 만든 확장 메서드입니다.

  • AddSwaggerGen: 이 메서드는 Swagger 생성기를 등록하며, API에 대한 Swagger 문서를 생성하는 데 사용됩니다. Swagger 문서에서 API의 제목, 버전 및 설명을 지정합니다.

  • MapGroup: 이 메서드는 엔드포인트를 그룹화합니다. 경로를 매개변수로 사용하고 IEndpointRouteBuilder 객체를 반환합니다. 엔드포인트에 태그를 추가하려면 WithTags 메서드를 사용하고 책 엔드포인트를 등록하려면 MapBookEndpoints 메서드를 사용합니다.

  • Run: 이 메서드는 애플리케이션을 시작합니다.

Swagger 문서를 활성화하려면 .csproj 파일에 GenerateDocumentationFile 속성을 추가해야 합니다. 이 예에서 파일 이름은 bookapi-minimal.csproj이지만 프로젝트에 따라 이름이 다를 수 있습니다.

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

.csproj 파일에 다음 줄을 추가하십시오:


<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <RootNamespace>bookapi_minimal</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
   <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
  </ItemGroup>

</Project>

이제 Program.cs 파일에서 책 엔드포인트를 등록했으므로 애플리케이션을 실행하고 Swagger를 사용하여 API 엔드포인트를 테스트할 수 있습니다.

애플리케이션을 실행하면 다음 URL에서 Swagger 문서를 볼 수 있어야 합니다: https://localhost:5001/swagger/index.html. Swagger 문서는 API 엔드포인트, 요청 및 응답 모델에 대한 정보를 제공하며, 브라우저에서 직접 엔드포인트를 테스트할 수 있게 해줍니다. 다음과 같은 내용을 볼 수 있어야 합니다:

축하합니다! 여러분은 도서 서비스에 대한 비즈니스 로직을 구현하고, 사용자 정의 예외를 생성하며, API 엔드포인트를 정의하고, Program.cs 파일에 엔드포인트를 등록했습니다. 또한 API 엔드포인트를 테스트하기 위해 Swagger 문서를 활성화했습니다.

데이터베이스에 시드 데이터 추가하기

애플리케이션이 시작될 때 데이터베이스에 초기 데이터로 시드를 추가하는 것은 또 다른 중요한 단계입니다. 이 시드 데이터는 데이터베이스를 채워 API 엔드포인트를 수동으로 데이터 추가 없이 테스트할 수 있게 해줍니다.

마이그레이션을 수행하고 API 엔드포인트를 테스트하기 전에 몇 가지 시드 데이터를 추가해 봅시다.

이를 위해, Configuration 폴더에 BookTypeConfigurations라는 새로운 클래스를 생성하고 다음 코드를 추가하겠습니다:



using bookapi_minimal.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace bookapi_minimal.Configurations
{
    public class BookTypeConfigurations : IEntityTypeConfiguration<BookModel>
    {
        public void Configure(EntityTypeBuilder<BookModel> builder)
        {
            // 테이블 이름 구성
            builder.ToTable("Books");

            // 기본 키 구성
            builder.HasKey(x => x.Id);

            // 속성 구성
            builder.Property(x => x.Id).ValueGeneratedOnAdd();
            builder.Property(x => x.Title).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Author).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Description).IsRequired().HasMaxLength(500);
            builder.Property(x => x.Category).IsRequired().HasMaxLength(100);
            builder.Property(x => x.Language).IsRequired().HasMaxLength(50);
            builder.Property(x => x.TotalPages).IsRequired();

            // 시드 데이터
            builder.HasData(
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "The Alchemist",
                    Author = "Paulo Coelho",
                    Description = "The Alchemist follows the journey of an Andalusian shepherd",
                    Category = "Fiction",
                    Language = "English",
                    TotalPages = 208
                },
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "To Kill a Mockingbird",
                    Author = "Harper Lee",
                    Description = "A novel about the serious issues of rape and racial inequality.",
                    Category = "Fiction",
                    Language = "English",
                    TotalPages = 281
                },
                new BookModel
                {
                    Id = Guid.NewGuid(),
                    Title = "1984",
                    Author = "George Orwell",
                    Description = "A dystopian social science fiction novel and cautionary tale about the dangers of totalitarianism. ",
                  Category = "Fiction",
                  Language = "English",
                  TotalPages = 328
                } 
            );
        }
    }
}

위의 코드를 살펴보겠습니다:

Entity Framework Core에서는 IEntityTypeConfiguration 인터페이스를 사용하여 엔티티 유형 및 데이터를 구성할 수 있습니다. BookTypeConfigurations 클래스는 IEntityTypeConfiguration<BookModel> 인터페이스를 구현하고 BookModel 엔티티의 구성을 제공합니다.

  • Configure 메서드: 이 메서드는 BookModel 엔티티 유형을 구성하는 데 사용됩니다. 테이블 이름, 기본 키 및 BookModel 엔티티의 속성을 정의합니다.

    • 테이블 이름: ToTable 메서드는 데이터베이스에 생성할 테이블 이름을 지정합니다. 이 경우 테이블 이름은 “Books”로 설정됩니다.

    • 기본 키: HasKey 메서드는 BookModel 엔티티의 기본 키를 지정합니다. 기본 키는 Id 속성으로 설정됩니다.

    • 속성: Property 메서드는 BookModel 엔티티의 속성을 구성합니다. 각 속성에 대해 데이터 유형, 길이 및 제약 조건을 지정합니다.

  • 씨드 데이터: HasData 메서드는 초기 데이터로 데이터베이스를 씨드합니다. API 엔드포인트를 테스트하기 위해 샘플 데이터로 BookModel 개체를 세 개 생성합니다.

이제 BookTypeConfigurations 클래스를 생성했으므로, 이 구성을 ApplicationContext 클래스에 등록해야 합니다. 이렇게 하면 데이터베이스가 생성되거나 마이그레이션될 때 구성이 적용됩니다.

우리는 드디어 API를 테스트할 준비가 거의 끝났어요. 그러나 그 전에 데이터베이스를 생성하고 시드 데이터를 적용하기 위해 마이그레이션을 수행해야 합니다.

우리는 appsettings.json 파일에 데이터베이스 연결 문자열을 추가했던 것을 기억하시나요? 이제 마이그레이션을 수행하고 나중에 데이터베이스를 업데이트하여 마이그레이션이 적용되도록 해봅시다.

마이그레이션 수행 방법

마이그레이션을 통해 모델 클래스에 대한 변경 사항을 기반으로 데이터베이스 스키마를 업데이트할 수 있습니다. Entity Framework Core에서는 dotnet ef migrations add 명령을 사용하여 이러한 변경 사항을 반영하는 새로운 마이그레이션을 생성할 수 있습니다.

마이그레이션을 수행하려면 터미널에서 다음 명령을 실행하세요:

dotnet ef migrations add InitialCreate

명령이 성공하면 다음과 유사한 출력이 표시됩니다:

Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'

이제 프로젝트에 Migrations라는 새 폴더가 생겼을 것입니다. 이 폴더에는 모델 클래스에 대한 변경 사항을 기반으로 생성된 마이그레이션 파일이 포함되어 있습니다. 이 마이그레이션 파일에는 데이터베이스 스키마를 업데이트하는 데 필요한 SQL 명령이 포함되어 있습니다.

데이터베이스 업데이트 방법

마이그레이션을 생성한 후에는 데이터베이스 스키마를 업데이트하기 위해 마이그레이션을 적용해야 합니다. dotnet ef database update 명령을 사용하여 마이그레이션을 적용하고 데이터베이스를 업데이트할 수 있습니다. SQL Server가 실행 중인지 확인하세요.

터미널에서 다음 명령을 실행하세요:


dotnet ef database update

이렇게 하면 모델 클래스에 대한 변경 사항을 기반으로 데이터베이스 스키마가 업데이트됩니다. 데이터베이스 연결 문자열에 오류가 없는지 확인하세요.

API 엔드포인트 테스트 방법

이제 Swagger를 사용하여 엔드포인트를 테스트할 수 있습니다. 이를 위해 터미널에서 다음 명령을 실행하여 응용 프로그램을 실행하십시오:


dotnet run

이렇게 하면 응용 프로그램이 실행됩니다. 브라우저를 열고 https://localhost:5001/swagger/index.html로 이동하여 Swagger 문서에 액세스할 수 있습니다. API 엔드포인트 목록, 요청 및 응답 모델, 그리고 브라우저에서 엔드포인트를 직접 테스트할 수 있는 기능이 표시됩니다.

포트 번호가 5001과 다른 경우 걱정하지 마십시오. 포트 번호는 사용 중인 기기의 유형에 따라 변경될 수 있지만 동일한 결과를 얻을 수 있습니다.

모든 책 가져오기 엔드포인트를 테스트하는 방법

모든 책 가져오기 엔드포인트를 테스트하려면 다음 단계를 따르십시오:

  1. Swagger 문서에서 GET /api/v1/books 엔드포인트를 클릭합니다.

  2. 실행해 보기 버튼을 클릭합니다.

  3. 실행 버튼을 클릭합니다.

이렇게 하면 데이터베이스에서 모든 책을 검색하기 위한 API에 요청이 전송됩니다.

데이터베이스에 시드된 책 목록을 포함한 API의 응답이 표시됩니다.

아래 이미지는 API의 응답을 보여줍니다:

ID로 책 가져오기 엔드포인트를 테스트하는 방법은 다음과 같습니다:

  1. Swagger 문서에서 GET /api/v1/books/{id} 엔드포인트를 클릭합니다.

  2. id 필드에 책의 ID를 입력합니다. 데이터베이스에 시드된 책 ID 중 하나를 사용할 수 있습니다.

  3. Try it out 버튼을 클릭합니다.

이렇게 하면 지정된 ID를 가진 책을 검색하기 위해 API에 요청이 전송됩니다. 지정된 ID의 책이 포함된 API의 응답을 볼 수 있어야 합니다.

아래 이미지는 API의 응답을 보여줍니다:

책 추가 엔드포인트를 테스트하는 방법은 다음과 같습니다:

  1. Swagger 문서에서 POST /api/v1/books 엔드포인트를 클릭합니다.

  2. Try it out 버튼을 클릭합니다.

  3. 요청 본문에 책 세부 정보를 입력합니다.

  4. 버튼을 클릭하십시오.

이것은 새 책을 데이터베이스에 추가하기 위해 API에 요청을 보냅니다.

API로부터 응답을 확인해야 합니다. 이 응답에는 새로 생성된 책이 포함될 것입니다.

아래 이미지는 API로부터의 응답을 보여줍니다:

책 업데이트 엔드포인트를 테스트하는 방법

책 업데이트 엔드포인트를 테스트하려면 다음 단계를 따르십시오:

  1. Swagger 문서에서 PUT /api/v1/books/{id} 엔드포인트를 클릭하십시오.

  2. id 필드에 책의 ID를 입력하십시오. 방금 추가한 책 중 하나의 ID를 사용할 수 있습니다.

  3. 실행해 보기 버튼을 클릭하십시오.

이것은 지정된 ID로 책을 업데이트하기 위해 API에 요청을 보냅니다.

API로부터 응답을 확인해야 합니다. 이 응답에는 업데이트된 책이 포함될 것입니다.

아래 이미지는 API로부터의 응답을 보여줍니다:

책 삭제 엔드포인트를 테스트하는 방법

책 삭제 엔드포인트를 테스트하려면 다음 단계를 따르십시오:

  1. Swagger 문서에서 DELETE /api/v1/books/{id} 엔드포인트를 클릭하십시오.

  2. id 필드에 책의 ID를 입력하세요. 방금 추가한 책들이나 시드 데이터 중의 아이디 중 하나를 사용할 수 있습니다.

  3. Try it out 버튼을 클릭하세요.

이렇게 하면 지정된 ID를 가진 책을 삭제하기 위해 API로 요청이 전송됩니다.

아래 이미지는 API로부터의 응답을 보여줍니다:

축하합니다! 책에 대한 모든 CRUD 작업을 구현하고 Swagger를 사용하여 API 엔드포인트를 테스트하여 예상대로 작동하는지 확인했습니다. 이제 이 기반을 활용하여 API에 더 많은 기능과 기능을 추가할 수 있습니다.

결론

이 핸드북에서는 .NET 8을 사용하여 ASP.NET Core에서 최소 API를 생성하는 방법을 탐색했습니다. CRUD 작업을 지원하는 포괄적인 책 API를 구축하고, 사용자 정의 예외를 구현하고, API 엔드포인트를 정의하고 등록하고, Swagger 문서화를 통해 쉽게 테스트할 수 있도록 설정했습니다.

이 튜토리얼을 따라하면 ASP.NET Core에서 최소 API를 구축하는 튼튼한 기반을 얻을 수 있습니다. 이제 이 지식을 적용하여 다양한 도메인과 산업을 위한 견고한 API를 만들 수 있습니다.

본 튜토리얼이 도움이 되고 유익했기를 바랍니다. 읽어 주셔서 감사합니다!

소셜 미디어에서 저와 연락하십시오: