.NET/ASP.NET

ASP.NET Core - 4. Platform 기능 활용하기 - 1

클리엘 2022. 11. 7. 08:39
728x90

ASP.NET Core는 일반적으로 Web개발에 필요한 여러 기능들을 제공하는 일련의 Service와 Middleware component를 포함하고 있습니다. 이번에는 기본적인 Service와 Middleware에 초점을 맞추어 3가지 정도의 가장 중요하고 폭넓게 사용되는 기능인 application 설정(구성), logging, 정적 content등에 대해 알아보고자 합니다.

 

1. Project 준비하기

 

실습을 위한 Project는 이전 글에 이어서 계속 사용할 것입니다. 다만 Program.cs는 아래와 같이 작성했던 Service와 Middleware를 모두 삭제하고

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();
app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

appsettings.Development.json 설정 file 또한 기존에 설정된 내용을 모두 삭제하고 기본 설정만을 남겨두도록 합니다.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    }
}

위와 같이 설정 후 Project를 실행하여 아래와 같은 결과가 나오는지를 확인합니다.

2. Configuration Service 사용

 

ASP.NET Core에서 제공되는 기본기능중 하나는 Service로 표현되는 application의 구성 설정에 접근하는 것입니다. Configuration data는 appsettings.json file에 기반하는데 기본적으로 아래 설정을 포함하고 있습니다.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

configuration service는 JSON file을 처리하고 각 개별 설정을 포함하는 중첩된 구성 section을 생성합니다. 예제 Project의 appsettings.json의 경우 configuration service는 LogLevel section을 포함하여 Logging 구성 section을 생성할 것입니다.

 

LogLevel section은 Default, Microsoft, Microsoft.Hosting.Lifetime등의 설정을 포함할 수 있습니다. 또한 configuration section의 일부가 아닌 AllowedHosts 설정도 포함할 수 있는데 이 설정의 값은 *로 지정되어 있습니다.

 

configuration service는 구성 section의 의미 혹은 appsettings.json file의 설정 자체에 의미를 부여하지 않고 JSON Data file에 대한 처리와 환경변수 혹은 명령행 매개변수와 같은 다른 source로부터 가져온 구성설 정의 병합만을 담당할 뿐입니다. 그 결과 구성 속성은 아래와 같은 계층구조를 이룰 수 있습니다.

(1) 환결별 구성파일

 

대부분의 Project는 하나 이상의 JSON 구성 파일을 가지고 있으며 이를 통해 개발과정에서 필요한 다른 구성 파일을 정의할 수 있도록 지원합니다. 기본적으로는 Development, Staging, Production이라는 3개의 사전 정의된 환경이 존재하는데 각각의 단어는 개발과정에서 일반적으로 사용되는 단어이며 Application이 시작할 때 configuration service는 현재 환경을 포함하는 이름의 JSON file을 찾게 됩니다. 기본 환경은 Development로서 configuration service는 appsettings. Development.json을 Load 하여 주된 appsettings.json file의 content를 보완하게 됩니다.

Visual Studio Solution Explorer를 보면 appsettings.Development.json을 appsettings.json file안으로 중첩하여 표시하고 있습니다. 따라서 appsettings.json을 확장하거나 Solution Explorer위의 Button을 click하여 중첩기능을 비 활성하면 appsettings.Development.json항목을 볼 수 있습니다.

다음은 System이라는 설정이 추가된 appsettings.Development.json file의 내용을 보여줍니다.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    }
}

appsettings.Development.json과 appsettings.json에서 같은 설정이 존재하는 경우 appsettings.Development.json file의 값은 appsettings.json file의 값을 대체하게 되는데 이는 두 JSON file의 content가 구성설정을 다음과 같이 계층적으로 만들어낸다는 것을 의미하기도 합니다.

추가적인 구성설정은 logging message의 단계를 좀 더 세분하는 효과를 가져오게 됩니다.

 

(2) 구성설정 가져오기

 

Configuration data는 Service를 통해 접근할 수 있습니다. 만약 Middleware의 설정을 위해 Configuration data만을 필요로 하는 경우라면 configuration service의 의존성은 매개변수를 사용해 선언될 수 있습니다.

var app = builder.Build();

app.MapGet("config", async (HttpContext context, IConfiguration config) => {
    string defaultDebug = config["Logging:LogLevel:Default"];

    await context.Response.WriteAsync($"Logging:LogLevel:Default : {defaultDebug}");
});

app.Run();

Configuration data는 IConfiguration interface를 통해 제공되는데 이 interface는 Microsoft.Extensions.Configuration에서 정의되어 있으며 설정 계층을 탐색하기 위한 API를 제공하고 구성 설정을 읽어 들입니다. 이때 구성 설정은 설정 section을 통한 경로를 특정함으로써 읽을 수 있습니다.

config["Logging:LogLevel:Default"];

따라서 Default설정값을 읽게되는 위 구문을 통해 Default는 구성설 정의 Logging부분에서 LogLevel section에 정의되어 있음을 알 수 있습니다. 또한 예제에서처럼 각 section과 구성 설정은 colon(:)을 통해 구분됩니다.

 

예제에서 읽어들인 Configuration data의 값은 Middleware component가 /config URL을 처리하기 위해 사용됩니다. Project를 실행하여 /config URL을 요청하여 아래와 같은 결과가 생성되는지를 확인합니다.

실제 설정값에 따라 위 결과는 약간씩 달라질 수 있습니다.

(3) Program.cs에서 Configuration data 사용하기

 

이전에도 언급한 적이 있지만 WebApplication과 WebApplicationBuilder class는 Configuration 속성을 제공하여 IConfiguration interface의 구현체를 가져올 수 있도록 지원하고 있습니다. 이는 Application service의 설정을 위해 Configuration data를 사용할 때 유용하게 쓰일 수 있는데 아래 예제는 Configuration data를 사용하는 2가지 용법을 보여주고 있습니다.

var builder = WebApplication.CreateBuilder(args);

var servicesConfig = builder.Configuration;

var app = builder.Build();

var pipelineConfig = app.Configuration;

app.MapGet("config", async (HttpContext context, IConfiguration config) => {
    string defaultDebug = config["Logging:LogLevel:Default"];

    await context.Response.WriteAsync($"Logging:LogLevel:Default : {defaultDebug}");
});

app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

첫 번째 serviceConfig는 Service의 구성 설정에 사용하며 두 번째 pipelineConfig는 pipeline의 구성 설정을 위해 사용할 수 있습니다.

 

(4) Option Pattern을 통한 Configuration data사용

 

Option Pattern은 Middleware component를 구성하기에 좋은 방법이 될 수 있습니다. IConfiguration service에 의해 제공되는 기능 중에는 Configuration data로부터 option을 직접적으로 생성할 수 있는 것이 있습니다. 이를 확인해 보기 위해 appsettings.json file에서 아래와 같이 구성 설정을 추가합니다.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    },
    "AllowedHosts": "*",
    "Member": {
        "MemberName": "Kim"
    }
}

위와 같이 추가된 Location section은 다음과 같이 option pattern값을 제공하는 데 사용됩니다.

using MyWebApp.Middleware;

var builder = WebApplication.CreateBuilder(args);

var servicesConfig = builder.Configuration;
builder.Services.Configure<Member>(servicesConfig.GetSection("Member"));

var app = builder.Build();

var pipelineConfig = app.Configuration;

app.UseMiddleware<MemberMiddleware>();

app.MapGet("config", async (HttpContext context, IConfiguration config) => {
    string defaultDebug = config["Logging:LogLevel:Default"];

    await context.Response.WriteAsync($"Logging:LogLevel:Default : {defaultDebug}");
});

app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

예제는 GetSection Method를 통해 configuration data를 가져온 다음 option이 생성될 때 이를 Configure Method로 전달하고 있습니다. 선택된 Section의 설정값은 특정 분석과정을 거친 뒤 option class에 있는 같은 이름의 기본값을 대체하게 됩니다. Project를 실행하여 /member URL을 요청하고 아래와 같은 결과가 생성되는지 확인합니다. 결과가 제대로 표시되었다면 MemberName은 configuration data로부터, MemberGroup은 option class로부터 가져온 값이 표시될 것입니다.

(5) Launch Settings File

 

Properties folder에 있는 launchSettings.json file은 ASP.NET Core Platform이 시작하기 위한 구성 설정을 가지고 있는데 여기에는 HTTP/HTTPS요청을 받기 위한 TCP Port와 추가 JSON 구성 file을 선택하는 데 사용되는 환경이 포함됩니다.

Visual Studio에서는 Properties folder가 보이지 않을 수 있습니다. 이런 경우에는 Solution Explorer의 상단에 'Show All Files'button을 click 하면 Properties folder와 그 아래 launchSettings.json file을 볼 수 있습니다.
{
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:4069",
      "sslPort": 0
    }
  },
  "profiles": {
    "MyWebApp": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5232",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

iisSettings section은 ASP.NET Core Platform이 IIS Express를 통해 시작할 때 HTTP/HTTPS Port를 사용하기 위한 설정에 사용되며 이전 Version의 ASP.NET Core가 배포되는 방법이기도 합니다.

 

profiles section은 Application이 동작하는 여러 방법을 위해 구성 설정을 정의하는 일련의 launch profile을 나열한 것입니다.Platform section은 dotnet command의 동작에서 필요한 구성을 저장하며 IIS Express section은 Application이 IIS Express를 통해 실행될때 사용되는 구성설정을 정의합니다.

 

profiles와 IIS Express section은 모두 environmentVariables section을 포함하고 있는데 이 section은 application의 Configuration data에 추가된 환경변수를 정의합니다. 예제에서는 단지 기본으로 정의된 ASPNETCORE_ENVIRONMENT이라는 환경 변수만이 존재하고 있습니다.

 

Application이 시작하는 동안 ASPNETCORE_ENVIRONMENT의 설정값은 JSON 설정 file을 선택하는 데 사용되며 따라서 Development값을 지정하면 appsettings.Development. json file이 load 될 것입니다.

 

ASPNETCORE_ENVIRONMENT의 값을 표시하기 위해 Middleware component에 /config URL로 응답을 수행하는 구문을 추가합니다.

app.MapGet("config", async (HttpContext context, IConfiguration config) => {
    string defaultDebug = config["Logging:LogLevel:Default"];
    await context.Response.WriteAsync($"Logging:LogLevel:Default : {defaultDebug}");

    string enviroment = config["ASPNETCORE_ENVIRONMENT"];
    await context.Response.WriteAsync($"\nenviroment setting : {enviroment}");
});

Project를 실행하고 /config URL을 요청하여 아래와 같이 현재 ASPNETCORE_ENVIRONMENT 설정값이 표시되는지 확인합니다.

ASPNETCORE_ENVIRONMENT 설정값이 변경되는 효과를 보기 위해 launchSettings.json file을 다음과 같이 변경합니다.

"profiles": {
"MyWebApp": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "http://localhost:5232",
    "environmentVariables": {
    "ASPNETCORE_ENVIRONMENT": "Production"
    }
},

만약 Visual Studio를 사용하고 있다면 Project의 Properties(속성)으로 들어가 Debug -> General -> Open debug launch profiles UI를 선택하면 아래와 같이 각 launch profile별 설정값을 확인할 수 있고 특정 환경변수의 값을 바꿀 수 있는 UI화면을 볼 수 있습니다.

값을 변경한 뒤 다시 Project를 실행하고 /config URL로 요청을 보내면 바뀐 값이 표시될 것입니다. 여기서 주목해야 할 부분은 Logging:LogLevel:Default 값입니다. 이 값은 Project를 실행하는 각자의 상황에 따라 달라질 수 있는데 그 이유는 ASPNETCORE_ENVIRONMENT의 값을 예제에서처럼 Production으로 변경하였으므로 기존의 appsettings. Development.json file은 load 되지 않고 appsettings.Production.json file은 현재 존재하지 않는 file이므로 appsettings.json file만이 사용되고 해당 file의 설정값이 표시될 것이기 때문입니다.

 

(6) Environment Service 사용

 

ASP.NET Core platform은 IWebHostEnvironment Service를 제공하므로 구성 설정을 수동적으로 가져올 필요가 없습니다. IWebHostEnvironment는 다음 표와 같은 method와 속성을 제공하고 있는데 method의 경우 확장 method로서 Microsoft.Extensions.Hosting namespace에 정의되었습니다.

EnvironmentName 이 속성은 현재 환경을 반환합니다.
IsDevelopment() 이 method는 Development environment가 선택된 상태이면 true를 반환합니다.
IsStaging() 이 method는 Staging environment가 선택된 상태이면 true를 반환합니다.
IsProduction() 이 method는 Production environment가 선택된 상태이면 true를 반환합니다.
IsEnvironment(env) 이 method는 매개변수로 전달한 환경이 선택된 상태이면 true를 반환합니다.

services를 설정할 때 environment로의 접근이 필요하다면 WebApplicationBuilder.Envirionment속성을 사용할 수 있습니다. 혹은 pipeline을 구성하는 경우라면 WebApplication.Envirionment속성을 사용할 수 있고 middleware component나 endpoint안에서 environment의 접근이 필요하다면 IWebHostEnvironment 매개변수를 정의할 수 있습니다. 아래 예제는 이러한 3가지 접근법을 보여줍니다.

using MyWebApp.Middleware;

var builder = WebApplication.CreateBuilder(args);

var servicesConfig = builder.Configuration;
builder.Services.Configure<Member>(servicesConfig.GetSection("Member"));

var servicesEnv = builder.Environment; //Service 설정 시 사용

var app = builder.Build();

var pipelineConfig = app.Configuration;

var pipelineEnv = app.Environment; //pipeline 설정 시 사용

app.UseMiddleware<MemberMiddleware>();

app.MapGet("config", async (HttpContext context, IConfiguration config, IWebHostEnvironment env) => { //IWebHostEnvironment 매개변수 추가
    string defaultDebug = config["Logging:LogLevel:Default"];
    await context.Response.WriteAsync($"Logging:LogLevel:Default : {defaultDebug}");

    //string enviroment = config["ASPNETCORE_ENVIRONMENT"];
    //await context.Response.WriteAsync($"\nenviroment setting : {enviroment}");

    await context.Response.WriteAsync($"\nenviroment setting : {env.EnvironmentName}");
});

app.MapGet("/", async context => {
    await context.Response.WriteAsync("Hello World!");
});

app.Run();

Project를 실행해 /config로 URL을 요청하여 이전과 동일한 결과가 나타나는지를 확인합니다.

 

(7) 사용자 민감정보 다루기

 

개발과정에서는 application이 의존하는 service의 사용을 위해 종종 API key나 database연결 정보, 관리자 계정 정보 등 민감한 정보를 다뤄야 하는 경우가 발생하곤 합니다. 이런 정보는 service로의 접근과 새로운 database나 사용자 구성을 통한 application의 변경사항을 test 하기 위해 service를 다시 초기화하는 데 사용되기도 합니다.

 

이러한 이유로 민감정보가 C# class나 JSON 설정 file에 포함되어 있다면 이것은 source code version control repository에  접근할 수 있는 모든 개발자와 그 외 source code를 볼 수 있는 모든 사람이 해당 정보를 볼 수 있는 뜻이 됩니다. 그리고 이러한 상황은 open repository의 project나 보안이 허술한 project의 경우 민감정보를 모든 곳에 노출하는 상황이 발생할 것입니다.

 

이때 User Secrets Service는 민감정보를 file에 저장할 수 있도록 하는데 이것은 application의 일부로 포함되지 않으며 version control에 들어갈 필요도 없게 합니다. 따라서 위에서 언급한 version control의 접근을 통해 발생할 수 있는 보안사고를 막을 수 있습니다.

 

※ User Secrets package 사용

 

User Secrets package를 사용하기 위해서는 우선 명령행 도구를 열고 해당 project가 존재하는 folder로 들어가 아래와 같은 명령을 내려줍니다.

dotnet user-secrets init

위 명령은 각 개발자 컴퓨터의 민감정보와 관련될 수 있는 Project의 project file(. csproj)에 고유한 ID값을 포함한 element를 추가합니다. 이어서 아래 명령 또한 순서대로 내려줍니다.

dotnet user-secrets set "MyWeb:ID" "MyID"
dotnet user-secrets set "MyWeb:Key" "MyKey"

Key와 값을 저장하는 위 명령은 공통 접두사, colon(:)및 민감정보 이름을 사용하여 함께 그룹화하고 있습니다. 따라서 MyWeb이라는 접두사를 가진 ID 및 Key라는 민감정보를 생성하도록 합니다.

 

위 명령을 내리고 나면

Successfully saved MyWeb:ID = MyID to the secret store.
Successfully saved MyWeb:Key = MyKey to the secret store.

와 같은 응답을 통해서 지정한 민감정보를 secret store에 잘 저장하였음을 알려주게 됩니다. 이렇게 저장된 민감정보는 아래 명령을 통해 확인할 수 있습니다.

dotnet user-secrets list

위에서 민감정보를 저장하게 하는 명령은 %APPDATA%\Microsoft\UserSecrets folder하위에 init초기화 명령을 통해 Project에 생성된 고유 ID로 folder를 만들고 다시 그 안에 JSON file을 생성하여 지정한 민감정보를 저장하게 합니다.

참고로 Visual Studio를 사용한다면 Project에서 Mouse오른쪽 button을 눌러 나오는 Menu에서 'Manage User Secrets'를 선택한다면 해당 JSON file의 내용을 바로 확인하고 편집할 수 있습니다.

 

※ 저장한 민감정보 가져오기

 

위에서 처럼 등록한 민감정보는 일반적인 구성 설정과 병합되는데 몇 가지 방법을 통해 민감정보를 가져올 수 있습니다. 아래 예제는 Middleware component안에서 어떻게 민감정보를 가져올 수 있는지를 보여주고 있습니다.

app.MapGet("config", async (HttpContext context, IConfiguration config, IWebHostEnvironment env) => { //IWebHostEnvironment 매개변수 추가
    string defaultDebug = config["Logging:LogLevel:Default"];
    await context.Response.WriteAsync($"Logging:LogLevel:Default : {defaultDebug}");

    await context.Response.WriteAsync($"\nenviroment setting : {env.EnvironmentName}");

    string wsID = config["MyWeb:ID"];
    string wsKey = config["MyWeb:Key"];
    await context.Response.WriteAsync($"\nID : {wsID}");
    await context.Response.WriteAsync($"\nKey : {wsKey}");
});

민감정보는 오로지 Application이 개발환경(Development)으로 설정되어 있을 때만 가져올 수 있습니다. 따라서 launchSettings.json file을 열어 ASPNETCORE_ENVIRONMENT의 값을 Development로 설정합니다.

"MyWebApp": {
    "commandName": "Project",
    "dotnetRunMessages": true,
    "launchBrowser": true,
    "applicationUrl": "http://localhost:5232",
    "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
    }
},

Project를 시작하고 /config로 URL을 요청하면 다음과 같은 응답을 볼 수 있습니다.

3. Logging Service

 

ASP.NET Core는 logging service를 제공함으로써 Application의 오류를 추적하기 위한 상태 message를 기록하고 성능을 Monitor 하며 문제점을 진단하는 데 사용할 수 있도록 지원하고 있습니다.

 

Log message는 logging provider로 전송됨으로써 Log를 검토하거나 저장 또는 처리하는 곳으로 message를 전달하고 있습니다. 기본 logging을 위한 내장된 proivder도 존재하지만 message를 수집하고 분석할 수 있도록 logging framework로 message를 공급하는 다양한 third-party provider들도 존재합니다.

 

기본적으로 제공하는 proivder 중에는 console provider, debug provider, EventSource provider가 있습니다. debug provider는 System.Diagnostics.Debug class로 message를 전달하여 이들을 통해서 message가 처리될 수 있도록 하며 EventSource provider는 PerfView와 같은 event tracing도구로 message를 전달합니다. console privider는 가장 간단하게 사용할 수 있는 provider로서 화면에 logging message를 표시하는데 이를 위한 추가적인 설정을 필요로 하지 않습니다.

 

참고로 아래 URL을 통해서는 사용 가능한 proivder의 목록과 함께 이들에 대한 간단한 설명을 살펴볼 수 있습니다.

Logging in .NET Core and ASP.NET Core | Microsoft Learn

 

Logging in .NET Core and ASP.NET Core

Learn how to use the logging framework provided by the Microsoft.Extensions.Logging NuGet package.

learn.microsoft.com

(1) Logging Message 생성

 

우선 Project의 Program.cs에서 기존에 만들었던 service와 middleware, endpoint 등을 모두 삭제합니다. 그리고 Middleware folder의 Fruit.cs file을 아래와 같이 수정하여 unbounded ILogger<> service를 사용해 Logging message를 생성하도록 합니다.

public class Fruit
{
    public static async Task Endpoint(HttpContext context, ILogger<Fruit> logger)
    {
        logger.LogDebug($"start : {context.Request.Path}");

        string? fruit = context.Request.RouteValues["fruit"] as string;
        int? price = null;

        switch ((fruit ?? string.Empty).ToLower())
        {
            case "watermelon":
                price = 1_000;
                break;

            case "grape":
                price = 2_000;
                break;

            case "orange":
                price = 1_500;
                break;
        }

        if (price.HasValue)
            await context.Response.WriteAsync($"fruit: {fruit}, cost: {price}");
        else
            context.Response.StatusCode = StatusCodes.Status404NotFound;

        logger.LogDebug($"End : {context.Request.Path}");
    }
}

다시 Program.cs로 돌아와 Fruit에 대한 Pipeline을 추가합니다.

var app = builder.Build();

app.MapGet("fruit/{fruit?}", Fruit.Endpoint);

app.Run();

logging service는 log message를 message에 할당된 Category에 기반하여 group화를 수행합니다. Log messages느 ILogger<T> interface를 사용해 작성되는데 이때 generic 매개변수인 T를 통하여 category를 특정하게 됩니다. 보통은 message를 생성하는 class를 category type으로 사용하는데 이 것은 위 예제에서 type 매개변수에 Fruit를 사용하여 service에 대한 의존성을 선언한 이유이기도 합니다.

 

이러한 방식은 emdpoint에 의해 생성되는 log message가 category인 Fruit에 할당될 수 있음을 말해줍니다. 이때 Log message는 아래 표의 확장 method를 통해 생성될 수 있습니다.

LogTrace 이 method는 개발주기동안 low-level debugging에 사용되는 Trace-level message를 생성합니다.
LogDebug 이 method는 개발주기 또는 제품화에 발생하는 문제를 해결하는동안 low-level debugging에 사용되는 Debug-level message를 생성합니다.
LogInformation 이 method는 Application의 일반적인 상태에 대한 정보를 제공하는데 사용되는 Information-level message를 생성합니다.
LogError 이 method는 Application에서 처리되지 않은 오류 또는 예외를 기록하는데 사용되는 Error-level message를 생성합니다.
LogCritical 이 method는 심각한 수준의 동작실패를 기록하는데 사용되는 Critical-level message를 생성합니다.

각각의 Log message는 자신의 중요성과 상세함을 반영하는 수준에 할당됩니다. 여기에서 그 수준이라 함은 상세한 진단을 위한 Trace부터 즉각적인 응답에 필요한 가장 중요한 정보에 이르기까지 다양한 범위가 있을 수 있습니다. 위의 표에서 소개된 각 method는 또한 문자열이나 예외를 사용해 Log message를 생성할 수 있도록 하는 overload 된 version이 존재하는데 이전 예제에서는 요청이 처리될 때 logging message를 생성하도록 하는 LogDebug method를 사용하였습니다.

logger.LogDebug($"start : {context.Request.Path}");
logger.LogDebug($"End : {context.Request.Path}");

이러한 구현으로 인해 Debug 수준의 log message는 응답이 시작될 때와 종료될 때 생성됩니다. log message를 확인해 보기 위해 project를 시작하고 /fruit/grape로 URL을 요청하게 되면 console app을 통해 ASP.NET Core로부터 출력되는 log message를 볼 수 있습니다.

만약 위 결과처럼 dbug가 표시되지 않는다면 appsettings.json(appsettingsDevelopment.json) file의 Logging->LogLevel->Default가 Debug로 설정되어 있는지 확인해야 합니다.

 

※ Program.cs에서의 Logging Message 사용하기

 

Logger<> service는 class를 통해 Logging을 수행할 때는 유용하지만 Application을 구성하기 위해 Top-Level 구문이 사용되는 Program.cs안에서의 Logging에는 적합하지 않습니다. 이런 경우 사용할 수 있는 가장 간단한 접근 방법은 WebApplication에서 정의되는 Logger속성을 통해 반환되는 ILogger를 사용하는 것입니다.

var app = builder.Build();

app.Logger.LogDebug("Pipeline start");

app.MapGet("fruit/{fruit?}", Fruit.Endpoint);

app.Logger.LogDebug("Pipeline end");

app.Run();

ILogger interface는 위에서 언급한 모든 method를 정의하고 있습니다. project를 실행하면 곧장 아래와 같은 결과를 볼 수 있습니다.

WebApplication class에서 제공되는 ILogger를 사용해 생성되는 logging message의 Category는 Application의 이름이 되므로 위의 결과에서는 MyWebApp으로 표현되고 있고 따라서 실행되는 Project의 이름마다 달라질 수 있습니다. 물론 이러한 경우에도 임의로 Category를 지정해 줄 수 있는데 그렇게 하기 위해서는 아래와 같은 방법을 사용합니다.

var app = builder.Build();

var logger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("MyLog");

logger.LogDebug("Pipeline start");

app.MapGet("fruit/{fruit?}", Fruit.Endpoint);

logger.LogDebug("Pipeline end");

app.Run();

예제는 Service로서 사용할 수 있는 ILoggerFactory interface를 사용해 Category가 지정된(예제에서는 MyLog) ILogger를 가져오는 CreateLogger method를 호출하고 있습니다.

(2) Attribute를 통한 Logging Message

 

log message를 생성하기 위한 다른 대안은 LoggerMessage Attribute를 사용하는 것입니다.

public partial class Fruit
{
    public static async Task Endpoint(HttpContext context, ILogger<Fruit> logger)
    {
        //logger.LogDebug($"start : {context.Request.Path}");
        StartingResponse(logger, context.Request.Path);

        string? fruit = context.Request.RouteValues["fruit"] as string;
        int? price = null;

        switch ((fruit ?? string.Empty).ToLower())
        {
            case "watermelon":
                price = 1_000;
                break;

            case "grape":
                price = 2_000;
                break;

            case "orange":
                price = 1_500;
                break;
        }

        if (price.HasValue)
            await context.Response.WriteAsync($"fruit: {fruit}, cost: {price}");
        else
            context.Response.StatusCode = StatusCodes.Status404NotFound;

        logger.LogDebug($"End : {context.Request.Path}");
    }

    [LoggerMessage(0, LogLevel.Debug, "start {path}")]
    public static partial void StartingResponse(ILogger logger, string path);
}

LoggerMessage attribute는 partial method에 적용됩니다. 따라서 class 역시 partial class로 정의되어야 합니다. 위와 같은 구현에서는 application이 compile 될 때 attribute는 이것이 적용된 method의 구현체를 생성하는데 결과적으로 다른 기술보다 Microsoft가 제공하는 더 나은 성능의 logging을 생성하게 됩니다. 참고로 아래 link를 통해 해당 기능이 어떻게 동작하는지에 자세한 설명을 찾아볼 수 있습니다.

 

Compile-time logging source generation - .NET | Microsoft Learn

 

Compile-time logging source generation - .NET

Learn how to use the LoggerMessageAttribute and compile-time source generation for logging in .NET.

learn.microsoft.com

project를 실행하여 이전과 동일한 결과가 나오는지 확인합니다.

 

(3) 최소 logging 수준 구성하기

 

위에서는 appsettings.json과 appsettings.Development.json file의 기본 설정을 보고 이들이 어떻게 병합되어 application의 구성 설정이 만들어지는지 알아보았습니다. JSON file의 설정은 ASP.NET Core가 application의 상태 message를 기록하는  Logging service를 구성하는 데 사용될 수 있습니다.

 

appsettings.json file의 Logging:LogLevel section에서는 logging message의 최소 수준을 설정할 수 있는데 최소수준 아래의 Log message는 모두 버려지게 됩니다. 기본적으로 appsettings.json file에서는 아래 수준이 설정되어 있습니다.

"Default": "Information",
"Microsoft.AspNetCore": "Warning"

이전 예제에서처럼 generic type 매개변수를 통해 설정되거나 단순 문자열로 설정된 log message category는 최소 filter 수준을 선택하는 데 사용됩니다.

 

예를 들어 Fruit class의 log messages를 생성하면 Category는 MyWebApp.Fruit가 될 수 있으며 이는 appsettings.json file에 MyWebApp.Fruit entry를 추가하여 직접적으로 일치시키거나 MyWebApp namespace를 지정함으로써 간접적으로 일치시킬 수 있음을 의미합니다. 최소 log수준이 없는 모든 Category는 Information으로 설정된 기본 entry와 일치합니다.

 

Application이 Development환경으로 구성될 때 기본 logging수준은 Debug입니다. 반면 System이나 Microsoft의 수준은 information으로서 해당 설정은 ASP.NET Core와 Microsoft에서 제공하는 Package나 Framework에 의해 생성된 log message에 영향을 줄 수 있는 설정입니다.

 

log수준은 Trace, Debug, Information, Error, Critical 수준 등으로 조정할 수 있으며 Application에서 이들 부분에 Log가 집중될 수 있도록 합니다. 또한 Category에 None을 설정하면 log mesage를 비활성화하는 것도 가능합니다.

 

아래 예제는 appsettings.Development.json file의 Microsoft.AspNetCore에 대한 log수준을 Debug로 설정한 것이며 상세의 기본 수준을 증가시켜 ASP.NET Core에 의해 생성되는 debug-level message를 표시하는데 영향을 주게 됩니다.

"Default": "Information",
"Microsoft.AspNetCore": "Debug"

Project를 실행하여 /fruit/orange로 URL을 요청하면 다양한 ASP.NET Core component로부터 일련의 message를 볼 수 있을 것입니다.

이 상태에서 message가 필요한 namespace를 더욱 구체화하면 상세한 message loging을 줄일 수 있습니다.

"Default": "Debug",
"Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Routing": "Debug"

이 변경은 Microsoft.AspNetCore Category를 Warning으로 돌리고 Microsoft. AspNetCore.Routing category를 Debug로 설정한 것으로 routing을 담당하는 component에 의한 logging message의 상세 수준을 증가시킵니다.

 

이러한 Log Message는 만약 누군가가 routing scheme를 온전히 이해하지 못하는 문제가 있는 경우 Application이 요청을 받게 되면 어떻게 동작하는지를 알아낼 수 있도록 도움을 줄 수 있을 것입니다.

 

(4) HTTP 요청과 응답 Logging 하기

 

ASP.NET Core는 Application이 수신하는 HTTP 요청과 Application이 만드는 응답에 대한 Log message를 생성하는 기본 Middleware를 포함하고 있습니다. 아래 예제는 Program.cs에서 요청 Pipeline으로 HTTP Logging middleware를 추가하는 방법을 보여줍니다.

var app = builder.Build();

app.UseHttpLogging();

//var logger = app.Services.GetRequiredService<ILoggerFactory>().CreateLogger("MyLog");

//logger.LogDebug("Pipeline start");

app.MapGet("fruit/{fruit?}", Fruit.Endpoint);

//logger.LogDebug("Pipeline end");

app.Run();

UseHttpLogging Method는 HTTP 요청과 응답에 관한 Logging message를 생성하는 Middleware component를 추가하게 되는데 이 Log message는 Microsoft. AspNetCore.HttpLogging.HttpLoggingMiddleware category와 Information심각도를 통해 생성되므로 아래와 같은 방법으로 Logging message를 활성화시켜야 합니다.

"LogLevel": {
    "Default": "Debug",
    "Microsoft.AspNetCore": "Warning",
    "Microsoft.AspNetCore.Routing": "Debug",
    "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
}

Project를 실행해 /fruit/orange URL로 요청을 보내면 Browser에 의해 보내는 요청과 Application에 만든 응답에 대한 Logging message를 다음과 비슷한 형식으로 볼 수 있습니다.

HTTP 요청과 응답에 대한 세부사항은 AddHttpLogging method를 통해 설정할 수 있습니다.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpLogging(opts => {
    opts.LoggingFields = HttpLoggingFields.RequestMethod | HttpLoggingFields.RequestPath | HttpLoggingFields.ResponseStatusCode;
});

var app = builder.Build();

이 method는 Logging message에 포함된 field와 header를 포함하고 있으며 HTTP 요청 Method와 Path 그리고 응답에 대한 상태 code를 지정하고 있습니다. HTTP Logging option설정에 대한 자세한 내용은 아래 link를 참고하시기 바랍니다.

 

HTTP Logging in .NET Core and ASP.NET Core | Microsoft Learn

 

HTTP Logging in .NET Core and ASP.NET Core

Learn how to log HTTP Requests and Response.

learn.microsoft.com

Project를 재시작하여 /fruit/orange로 URL을 요청하면 위에서 선택한 세부사항이 포함된 Logging message를 볼 수 있습니다.

ASP.NET Core는 또한 W3C 형식에 맞는 log message를 생성하는 Middleware component를 제공하고 있습니다. 자세한 사항은 아래 link를 참고하시기 바랍니다.

 

W3CLogger in .NET Core and ASP.NET Core | Microsoft Learn

 

W3CLogger in .NET Core and ASP.NET Core

Learn how to create server logs in the W3C standard format.

learn.microsoft.com

4. Static Content와 Client-Side Package 사용

 

대부분의 Web Application은 동적인 생성과 정적 요소를 혼합한 형태의 Service를 제공합니다. 동적 content는 사용자의 식별정보와 action에 기반하여 shopping cart 혹은 선택한 특정 제품의 상세처럼 Application이 각 요청에 따라 새롭게 생성하는 것인데 ASP.NET Core를 사용해 동적 content를 만들어 내는 방법에 관해서는 추후에 별도로 알아볼 것입니다.

 

정적 content는 바뀌지 않는 것을 말하는데 대부분의 경우 image나 CSS, Javascript file등에 대한 것으로 모든 HTTP 요청에 생성되지 않습니다. 또한 ASP.NET Core Project에서 이들 정적 content는 기본적으로 wwwroot folder에 위치합니다.

 

예제를 통해 살펴볼 정적 content를 만들어 보기 위해 Project에서 wwwroot folder를 만들고 여기에 static.html이라는 이름의 file을 아래 내용으로 추가합니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>테스트 페이지 입니다.</title>
</head>
<body>
    안뇽!!
</body>
</html>

위 예제는 browser에 간단한 내용을 표시하기 위한 기본적인 HTML 요소만을 포함하고 있습니다.

 

(1) Static Content Middleware 추가하기

 

ASP.NET Core는 정적 content에 대한 요청을 처리하기 위한 middleware component를 제공하고 있으므로 아래와 같이 요청 Pipeline에 해당 Middleware를 추가해야 합니다.

app.UseHttpLogging();

app.UseStaticFiles();

app.MapGet("fruit/{fruit?}", Fruit.Endpoint);

예제에서 사용된 UseStaticFiles 확장 method는 static file middleware를 요청 Pipeline에 추가합니다. 이 Middleware는 disk상의 file이름에 해당하는 요청에 응답하며 그 외 다른 요청은 모두 Pipeline의 다음 요청으로 전달하게 됩니다. 이러한 특징 때문에 해당 Middleware는 대부분 요청 Pipeline이 시작되는 지점에 추가하여 다른 component가 정적 file에 대한 요청을 처리하는 일이 없도록 합니다.

 

Project를 시작해 /static.html로 URL을 요청하면 static file middleware는 해당 요청을 수신하고 wwwroot folder에 있는 static.html file content를 응답하게 됩니다.

이 middleware component는 요청된 file content를 반환하고 Browser에 content를 묘사하는 Content-Type과 Content-Length와 같은 응답 Header를 설정합니다.

 

※ Static Content Middleware의 기본 option 변경하기

 

UseStaticFiles method가 매개변수 없이 호출되면 middleware는 wwwroot folder를 요청된 URL의 경로와 일치하는 file의 기본 위치 folder로 사용하게 됩니다.

 

이러한 동작은 UseStaticFiles method에 StaticFileOptions객체를 전달함으로써 변경할 수 있습니다. 아래 표는 StaticFileOptions객체에서 사용할 수 있는 속성들을 나열한 것입니다.

ContentTypeProvider 이 속성은 file의 MIME type을 만들어 응답하는 IContentTypeProvider객체를 가져오거나 설정하는데 사용됩니다. interface의 기본구현은 content type을 결정하기 위해 file의 확장자를 사용하며 여기에는 대부분 일반적인 file type을 지원합니다.
DefaultContentType 이 속성은 IContentTypeProvider가 file의 type을 결정할 수 없는 경우 기본 content type을 설정하는데 사용합니다.
FileProvider 이 속성은 요청되는 content의 위치를 지정하는데 사용됩니다.
OnPrepareResponse 이 속성은 정적 content의 응답이 생성되기 전에 호출될 수 있는 동작을 등록하는데 사용됩니다.
RequestPath 이 속성은 Middleware가 응답하게될 URL경로를 지정하는데 사용됩니다.
ServeUnknownFileTypes 기본적으로 static content middleware는 IContentTypeProvider에 의해 content type이 확인되지 않는 file은 제공하지 않는데 이러한 동작은 해당 속성을 true로 설정함으로서 바꿀 수 있습니다.

위 속성 중에는 FileProvider와 RequestPath속성이 가장 많이 사용되는데 FileProvider는 정적 content의 위치를 다른 경로로 지정하기 위해 사용되며 RequestPath속성은 정적 content의 요청을 나타내는 URL의 접두사를 특정하는 데 사용됩니다. 아래 예제는 이 2가지 속성을 사용하여 static file middleware를 구성하는 예를 보여주고 있습니다.

app.UseStaticFiles();

var env = app.Environment;
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new
 PhysicalFileProvider($"{env.ContentRootPath}/staticfiles"),
    RequestPath = "/files"
});

app.MapGet("fruit/{fruit?}", Fruit.Endpoint);

Pipeline에는 middleware component의 다중 instance가 추가될 수 있는데 각각은 URL와 file위치 사이의 개별적인 mapping을 처리합니다. 예제에서 두 번째 static files middleware는 요청 Pipeline에 추가되어 /files로 시작하는 URL의 요청의 경우 staticfiles이름의 folder로부터 files를 사용해 처리할 것입니다. folder로 부터 file을 읽으려면 disk file을 읽는 PhysicalFileProvider class의 instance를 사용해야 합니다. PhysicalFileProvider class는 동작하기 위해 절대 경로를 필요로 하므로 예제에서는 IWebHostEnvironment interface에 의해 정의되는 ContentRootPath 속성의 값을 기반으로 지정하였습니다. 참고로 해당 IWebHostEnvironment interface는 Application이 Development 혹은 Production환경에서 동작하는지의 여부를 판단하기 위해 사용되는 interface와 같습니다.

 

새로운 middleware component를 사용하여 content를 제공하기 위해 Project에 staticfiles이라는 이름의 folder를 추가하고 여기에 test.html이라는 이름의 html file을 추가합니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>test</title>
</head>
<body>
    test
</body>
</html>

project를 실행해 /files/test.html로 URL을 요청합니다. /files로 시작하고 staticfiles folder의 file에 해당하는 URL요청은 새로운 Middleware에 의해 처리되어 다음과 같은 결과를 보여줄 것입니다.

(2) Client-Side Package

 

대부분의 Web Application은 Application이 만들어 내는 content를 위해 여러 client-side package를 사용합니다. CSS Framework를 사용해 content를 좀 더 보기 좋게 다듬을 수 있고 JavaScript package를 통해 WebBrowser수준의 풍부한 기능을 만들어 낼 수 있습니다. Microsoft는 LibMan으로 알려진 Library Manager 도구를 제공하여 client-side package를 내려받고 관리할 수 있도록 지원하고 있습니다.

 

※ Client-Side Package를 위한 Project 준비하기

 

우선 명령 prompt나 Power Shell과 같은 명령행 도구를 통해 Project file(*.csproj)이 존재하는 위치로 이동한 후 아래 명령을 실행합니다. 해당 명령은 기존의 LibMan을 삭제(기존 설치된 LibMan이 없다면 오류가 발생할 수 있으나 무시하도록 합니다.)하고 지정한 version의 LibMan을 설치하도록 합니다.

dotnet tool uninstall --global Microsoft.Web.LibraryManager.Cli
dotnet tool install --global Microsoft.Web.LibraryManager.Cli --version 2.1.175

그다음에는 LibMan 설정 file을 생성하는 명령을 실행합니다. 이 설정 file을 통해 client-side package를 가져올 repository와 package를 내려받을 Directory를 지정합니다.(단, 위에서 LibMan Package를 설치하게 되면 해당 설정 file이 이미 만들어졌을 수 있고 그런 경우라면 관련 오류가 발생할 수 있는데 무시하고 넘어가도록 합니다.)

libman init -p cdnjs

위 명령에서 -p는 package를 가져올 provider를 명시하는 것이며 cdnjs는 provider로 cdnjs.com을 지정하는 것입니다. unpkg이라는 다른 option을 지정할 수도 있는데 이것은 unpkg.com을 의미합니다.

 

Project folder에서 libman.json라는 이름의 설정 file이 만들어지면 아래와 같은 내용을 갖고 있을 것입니다. 해당 file을 직접 선택해 열어보거나 Visual Studio에서 Solution Explorer에 있는 Project를 선택하고 Mouse 오른쪽 button으로 click 하면 나오는 단축 Menu 중 'Manage Client-Side Libraries...'라는 메뉴를 선택하면 동일하게 libman.json file을 열어보거나 직접 편집할 수 있습니다.

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": []
}

※ Client-Side Package 설치하기

 

Visual Studio에서 Solution Explorer에 있는 Project를 선택하고 Mouse 오른쪽 button으로 click한뒤 Add->Client-Side Library를 차례로 선택하면 다음과 같은 화면을 볼 수 있습니다. 이 화면을 통해 package가 repository에 위치시키고 설치할 수 있습니다.

위 화면에서 Library에 특정 Package를 입력하게 되면 입력한 이름과 일치하는 Package를 repository에 질의하게 됩니다.  따라서 Library에 'bootstrap@5.2.2'를 입력하게 되면 해당 Bootstrap CSS framework가 아래와 같이 표시될 것입니다.

Package 이름으로 입력한 @는 Package의 version을 명시한 것입니다. 'Install' button을 눌러 Bootstrap package를 내려받고 설치하도록 합니다. Package는 또한 명령행 도구를 사용해 설치할 수도 있는데 Visual Studio Code와 같은 편집기를 사용하는 경우 아래 명령을 실행하면 동일하게 package를 내려받아 설치할 수 있습니다.

libman install bootstrap@5.2.2 -d wwwroot/lib/bootstrap

이때 Package의 version은 같은 방법으로 @문자를 통해 분리되고 -d를 통해 package가 설치될 위치를 지정합니다. 예제에서 사용된 wwwroot/lib folder는 ASP.NET Core Project에서 client-side package가 설치되는 데 사용되는 관례적인 위치입니다.

 

※ Client-Side Package 사용

 

Package를 설치하고 나면 해당 file들은 script 또는 link HTML 요소나 고수준 ASP.NET Core 기능에 의해 제공되는 기능을 사용해 참조될 수 있습니다.

 

아래 예제에서는 간단하게 link 요소를 생성해 이전에 만든 HTML file에 추가하고 Package가 지원하는 요소(h3)도 같이 추가하여 HTML의 응답을 서식화한 것을 보여주고 있습니다.

<!DOCTYPE html>
<html>
<head>
    <link href="lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <meta charset="utf-8" />
    <title>테스트 페이지 입니다.</title>
</head>
<body>
    <h3 class="p-2 bg-primary text-white">안뇽!!</h3>
</body>
</html>

Project를 실행하여 /static.html로 URL을 요청하면 browser가 응답을 받고 static.html 파일을 처리할 때 link요소를 만나게 되면서 /lib/bootstrap/css/bootstrap.min.css URL의 HTTP 요청을 ASP.NET Core runtime으로 보내게 됩니다. 본래 Program.cs의 UseStaticFiles Method인 static file middleware component는 요청을 수신받아 wwwroot foler에 있는 file에 해당하는 요청인지를 판단한 후 content를 반환하므로 browser로 Bootstrap CSS stylesheet를 제공하게 될 것입니다. Bootstrap style은 h3 요소에 할당된 CSS의 class를 통해 적용되므로 아래와 같은 결과를 표시하게 됩니다.

728x90