[서버캠퍼스 1기] ASP.NET Core

Oak_Cassia·2023년 4월 20일
0

ASP.NET Core

MS DOC를 참고하거나 발췌하였습니다. 각 개념이 처음 등장할 때 문서의 링크로 연결했습니다.

  • 크로스 플랫폼
    • Windows, macOS, Linux에서 개발 및 실행 가능합니다.
  • 오픈 소스
    • 꾸준한 업데이트 및 커뮤니티 지원이 있습니다.
  • 모듈화 및 확장성
    • 필요한 기능만 선택하여 사용할 수 있고 미들웨어 구성 요소를 사용해 쉽게 확장할 수 있습니다.
  • .NET Core에서 실행됩니다.

ASP.NET Core 기본 사항 개요를 읽고 다음으로 넘어가는 것을 추천합니다.


미들웨어

  • 요청과 응답을 처리하기 위해 앱 파이프라인에 포함된 소프트웨어입니다.
  • 파이프라인의 미들웨어 구성요소는 다음과 같은 역할을 합니다.
    1. 요청을 다음 구성 요소로 전달할지 결정합니다.
      • 전달하지 않으면 터미널 미들웨어라고 부르며, 요청 처리에 대한 추가 미들웨어를 차단합니다.
    2. 다음 구성 요소 이전 및 이후에 작업을 수행할 수 있습니다.

요청 대리자

  • 요청 파이프라인을 구축하는 데 사용됩니다.
  • 각 HTTP 요청을 처리합니다.
  • Run, Map, Use 확장 메서드로 구성됩니다.
  • 요청 대리자는 인라인 미들웨어 혹은 다시 사용할 수 있는 클래스에서 정의될 수 있습니다.
  • 이 때 다시 사용할 수 있는 클래스와 인라인 미들웨어를 통칭하여 미들웨어라고 합니다.

미들웨어 파이프라인

  • HTTP 요청을 순서대로 처리하는 일련의 구성 요소입니다.
  • 일련의 요청 대리자 시퀀스로 구성됩니다.
  • 각 대리자는 다음 대리자의 전과 후에 작업을 수행할 수 있습니다.
    • 예외 처리 대리자는 초기에 호출하여 이후에 발생하는 예외를 처리할 수 있게 합니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    // Do work that can write to the Response.
    await next.Invoke();
    // Do logging or other work that doesn't write to the Response.
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from 2nd delegate.");
});

app.Run();
  • Use를 사용하여 다음 대리자로 연결할 수 있습니다.
    • next 매개변수는 파이프 라인의 다음 대리자를 나타냅니다.
    • 호출하지 않으면 short-circuit 합니다.
    • 클라이언트에 응답을 전송한 뒤에는 next.Invoke를 호출하면 안됩니다.
      • 자세한 사항은 문서에서 확인 가능합니다.
  • Run 대리자는 next 매개변수를 받지 않습니다.
    • 첫 번째 Run 대리자는 항상 터미널이며 파이프라인을 종료합니다.
    • Run 뒤에 다른 대리자가 추가되어도 호출되지 않습니다.

미들웨어 순서

  • 원하는 대로 기존 미들웨어의 순서를 바꾸거나 시나리오의 필요에 따라 새 사용자 지정 미들웨어를 추가할 수 있습니다.
  • Program.cs 파일에 추가되는 순서로 요청과 응답에 따른 순서가 정의됩니다.
    • 순서는 보안, 성능, 기능에 영향을 미칩니다.

미들웨어 파이프라인 분기

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Map("/map1", HandleMapTest1);

app.Map("/map2", HandleMapTest2);

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from non-Map delegate.");
});

app.Run();

static void HandleMapTest1(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Map Test 1");
    });
}

static void HandleMapTest2(IApplicationBuilder app)
{
    app.Run(async context =>
    {
        await context.Response.WriteAsync("Map Test 2");
    });
}
  • Map확장은 파이프라인을 분기하는 규약으로 사용됩니다.

  • Map은 주어진 요청 경로와 일치하는 것을 기반으로 요청 파이프라인을 분기합니다.

    • 요청 경로가 주어진 경로로 시작하면 해당 분기가 실행됩니다.
    • 중첩이 가능하고 여러 세그먼트를 한 번에 일치시킬 수도 있습니다.
    app.Map("/level1", level1App => {
      level1App.Map("/level2a", level2AApp => {
          // "/level1/level2a" processing
      });
      level1App.Map("/level2b", level2BApp => {
          // "/level1/level2b" processing
      });
    });
    app.Map("/map1/seg1", HandleMultiSeg);
    
  • MapWhen, UseWhen으로 주어진 조건자의 결과에 따라 요청 파이프라인을 분기할 수 있습니다.

사용자 지정 미들웨어

인라인 미들웨어로 만드는 것도 가능하지만 별도의 클래스를 만들어서 사용하는 방법을 보겠습니다.

미들웨어 클래스는 다음을 포함해야 합니다.

  1. RequestDelegate 타입의 매개변수가 있는 public 생성자
  2. Invoke 또는 InvokeAsync public 메서드
    • 해당 메서드는 Task를 반환하고 HttpContext 타입의 첫 번째 매개변수를 받아야 합니다.
  • 생성자와 Invoke의 추가 매개 변수는 뒷절에서 나오는 DI로 채워집니다.
public class RequestCultureMiddleware
{
    private readonly RequestDelegate _next;

    public RequestCultureMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var cultureQuery = context.Request.Query["culture"];
        if (!string.IsNullOrWhiteSpace(cultureQuery))
        {
            var culture = new CultureInfo(cultureQuery);

            CultureInfo.CurrentCulture = culture;
            CultureInfo.CurrentUICulture = culture;
        }

        // Call the next delegate/middleware in the pipeline.
        await _next(context);
    }
}

미들웨어 클래스를 만들었으면 확장 메서드로 노출시킵니다.

  • 메서드는 static 이어야 하고 만약 클래스에 포함시켰다면 클래스역시 static 으로 만듭니다.
public static class RequestCultureMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestCulture(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestCultureMiddleware>();
    }
}

다음 코드를 호출할 때 this IApplicationBuilder builderapp객체가 할당 됩니다.

app.UseRequestCulture()

라우팅

  • 라우팅은 들어오는 HTTP요청을 매칭하고 앱의 실행 가능한 엔드포인트에 전달합니다.
    • 엔드포인트는 앱의 실행 가능한 요청 처리 코드의 단위입니다.
    • 엔드포인트는 앱에서 정의되고 앱이 시작될 때 구성됩니다.
  • 엔드포인트 매칭 과정에서 URL의 값을 추출하고 처리를 위해 해당 값을 제공할 수 있습니다.
  • 라우팅은 UseRouting과 UseEndpoints에 의해 등록된 미들웨어 쌍을 사용합니다.
    • UseRouting은 미들웨어 파이프라인에 경로 매칭을 추가합니다.
      • 앱에서 정의된 엔드포인트 집합을 살펴보고 요청에 따라 적합한 것을 선택합니다.
    • UseEndpoints는 미들웨어 파이프라인에 엔드포인트 실행을 추가합니다.
      • 선택된 엔드포인트와 관련된 대리자를 실행합니다.
    • 위의 두 메서드는 호출하지 않아도 됩니다.
      • WebApplicationBuilder는 해당 메소드를 래핑하여 미들웨어 파이프라인을 구성합니다.

라우팅을 구성하는 방법: ASP.NET Core의 컨트롤러 작업에 라우팅

엔드포인트

  • 앱에서 일치시키고 실행할 수 있는 엔드포인트는 UseEndpoints에서 구성됩니다.
    • MapControllers
    • app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
  • 엔드포인트는 URL 및 HTTP 메서드를 일치시켜 선택되거나
  • 대리자를 실행하여 수행됩니다.

REST API에 대한 어트리뷰트 라우팅

  • REST API는 어트리뷰트 라우팅을 사용하여 앱의 기능을 HTTP verb로 표현되는 일련의 리소스로 모델링해야 합니다.
  • 어트리뷰트 라우팅은 일련의 어트리뷰트를 사용하여 작업을 직접 라우팅 템플릿에 매핑합니다.
//...
app.MapControllers();

app.Run();
  • 위 코드처럼 MapControllers가 호출되어야 어트리뷰트 라우팅된 컨트롤러에 매핑할 수 있습니다.

다음 코드는 conventional route와의 차이를 볼 수 있는 코드입니다.

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}
  • 어트리뷰트 라우팅은 각 작업에 적용되는 라우트 템플릿을 정확하게 제어할 수 있습니다.
  • 컨트롤러와 작업 이름은 토큰 교체(token replacement)가 사용되지 않는 한 매칭에 영향을 주지 않습니다.
  • 단점으로 라우트를 지정하기 위해 conventional route 보다 더 많은 입력이 필요합니다.

HTTP verb 템플릿

다음 코드에서 [controller]는 test2와 대응됩니다.

[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
    [HttpGet]   // GET /api/test2
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

의존성 주입

  • 의존성은 한 객체가 다른 객체에 의존하는 관계를 말합니다.
    • 코드의 유지 보수와 테스트에 악영향을 끼칩니다.
  • 의존성 주입은 객체 간의 의존 관계를 관리하기 위한 기법입니다.
    • 클래스 간의 결합도를 낮추고, 유지보수와 테스트에 용이합니다.
  • ASP.NET Core에서 의존성 주입은 다음 코드와 같습니다.
  1. 인터페이스를 정의합니다.
public interface IMyDependency
{
    void WriteMessage(string message);
}
  1. 인터페이스를 구현하는 클래스를 작성합니다.
public class MyDependency : IMyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
    }
}
  1. 의존성을 서비스 컨테이너에 등록합니다. 그러면 생성자에서 IMyDependency 인터페이스를 요청하는 모든 클래스에 인스턴스를 제공합니다.
using Microsoft.Extensions.DependencyInjection;
using YourNamespace.Interfaces;
using YourNamespace.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();
  1. 클래스의 생성자에 서비스를 주입합니다.
public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("IndexModel.OnGet");
    }
}

앞선 코드에서는 AddScoped를 사용했지만 ASP.NET Core에서는 수명에 따라 다음 세 가지가 존재합니다.

  1. AddTransient
    • Transient 수명 서비스는 서비스 컨테이너에서 요청할 때마다(DI 주입 시 마다) 생성됩니다.
    • 이 수명은 간단한 상태 비저장 서비스에 가장 적합합니다.
    • 요청을 처리하는 앱에서 Transient 서비스는 요청이 끝날 때 삭제됩니다.
  2. AddScope
    • 클라이언트 요청(연결) 마다 한 번씩 서비스가 생성됩니다.
    • 요청이 끝날 때 삭제됩니다.
  3. AddSingleton
    • 처음 요청하거나 개발자가 구현 인스턴스를 컨테이너에 직접 제공하는 경우 생성됩니다.
    • 애플리케이션 종료 시 삭제됩니다.

구성

구성 공급자

WebApplication.CreateBuilde는 미리 구성된 기본 값을 사용하여 클래스의 새 인스턴스를 초기화 합니다.

  • 우선순위는 문서에서 찾아볼 수 있습니다.

뒤에서 살펴볼 코드는 JSON 구성 공급자를 사용합니다.

  • appsettings.json 파일에서 키-값 쌍의 구성 데이터를 읽습니다.

옵션

ASP.NET Core는 옵션 패턴을 제공합니다. 구성이 별도의 클래스로 분리되면 두 가지 소프트웨어 공학 원칙을 따르게 됩니다.

  1. 캡슐화: 구성 설정에 의존하는 클래스는 사용하는 구성 설정에만 의존합니다.
  2. 관심사 분리: 애플리케이션의 다른 부분에 대한 설정이 서로 종속되거나 연결되지 않습니다.

옵션 클래스는 다음과 같아야 합니다.

  • 매개 변수가 없는 public 생성자를 사용하는 비추상 클래스여야 합니다.
  • 타입의 모든 public read-write 프로퍼티가 바인딩 됩니다.
  • 필드는 바인딩되지 않습니다. name 변수는 바인딩되지 않고 구성공급자에 바인딩할 때 "DbConfig"를 하드코딩할 필요 없도록 사용됩니다.
public class DbConfig
{
	public const string name = "DbConfig";
    public String MasterDb { get; set; }
    public String AccountDb { get; set; }
    public String GameDb { get; set; }
    public String Memcached { get; set; }
}

API Server 코드로 보는 구성과 옵션

var builder = WebApplication.CreateBuilder(args);

IConfiguration configuration = builder.Configuration;

builder.Services.Configure<DbConfig>(configuration.GetSection(nameof(DbConfig)));
//또는 configuration.GetSection(DbConfig.name)
//...
  1. 첫 번째 행에서 WebApplicatoin은 HTTP 파이프라인과 routes를 구성하는데 사용됩니다.

    • CreateBuilder 함수는 미리 구성된 기본값을 사용하여 WebApplicationBuilder 클래스의 새 인스턴스를 초기화합니다. 이 때 appsettings.json에서 구성 데이터를 가져옵니다.
  2. IConfiguration configuration = builder.Configuration; 코드를 통해 구성 정보에 접근할 수 있는 IConfiguration인스턴스를 가져옵니다.

  3. 세 번째 문장은 Configure함수를 통해 구성 데이터를 서비스 컨테이너에 등록합니다. 등록된 데이터는 DI를 통해 다른 클래스에서 사용될 수 있습니다.

    • 주입될 생성자는 IOptions를 사용합니다.
      • IOptions는 앱 시작 후의 JSON 구성 파일의 변경사항은 읽지 않습니다.
      • 앱 시작 후의 변경사항을 읽으려면 IOptionsSnapshot을 사용해야 합니다.
public class MyClass
{
    private readonly DbConfig _dbConfig;

    public MyClass(IOptions<DbConfig> dbConfig)
    {
        _dbConfig = dbConfig.Value;
    }

    // ...
}
  1. Program.cs 코드 내에서는 다음 코드처럼 구성 데이터에 접근합니다.
  • GetSection은 IConfigurationSection 반환합니다.
    configuration.GetSection("DbConfig")

  • 키로 임의 접근을 하면 문자열을 반환합니다.
    configuration["logdir"]

  • json 예시

    {
         "logdir": "./log/", 
         "DbConfig": {
               "Redis": "127.0.0.1",
             ...    
         }
    }

로깅

로깅은 실행 동안 발생한 이벤트를 기록하고 추적할 수 있는 매커니즘입니다. 성능, 문제 해결 및 보안에 큰 도움이 됩니다.

로깅 공급자

로그를 표시하는 Console을 제외하면 로깅 공급자는 로그를 저장합니다.
일반적으로 WebApplicartion.CreateBuilder를 호출하면 다음 공급자를 추가합니다.

  • 콘솔
  • 디버그
  • EventSource
  • EventLog

로그 생성(Create logs)

로그도 마찬가지로 의존성 주입(DI)을 통해 만들어질 수 있습니다.

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        _logger.LogInformation("About page visited at {DT}", 
            DateTime.UtcNow.ToLongTimeString());
    }
}
  • ILogger<TCategoryName> 객체를 사용합니다.
    • ILogger에 DI 하기 위해 Program.cs에서 직접 함수를 호출할 필요가 없습니다.

로그 카테고리(범주)

로그 카테고리는 각 로그와 연결된 문자열입니다. 다음 방법으로 정할 수 있습니다.

  1. 해당 문자열은 ILogger<T>T에 해당하는 클래스의 이름

  2. _logger = logger.CreateLogger("AboutModel");

    • 위 문장의 loggerILoggerFactory 객체 입니다.

로그 카테고리는 JSON 파일에서 로깅을 구성할 때 사용될 수 있습니다. 로깅 구성은 일반적으로 appsettings.{ENVIRONMENT}.json 파일의 Logging섹션에서 제공됩니다.

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

로그 레벨

로그 레벨은 로그 이벤트의 심각성을 나타냅니다. 로그 레벨은 Trace, Debug, Information, Warning, Error, Critical 및 None으로 구성되어 있으며, 각각 0부터 6까지 할당됩니다.

json 파일에서 공급자별로 로그 레벨을 설정할 수 있습니다.

  • Logging.{PROVIDER NAME}.LogLevel
{
  "Logging": {
    "LogLevel": { // All providers, LogLevel applies to all the enabled providers.
      "Default": "Error", // Default logging, Error and higher.
      "Microsoft": "Warning" // All Microsoft* categories, Warning and higher.
    },
    "Debug": { // Debug provider.
      "LogLevel": {
        "Default": "Information", // Overrides preceding LogLevel:Default setting.
        "Microsoft.Hosting": "Trace" // Debug:Microsoft.Hosting category.
      }
    },
    "EventSource": { // EventSource provider
      "LogLevel": {
        "Default": "Warning" // All categories of EventSource provider.
      }
    }
  }
}

실습 4 파일로 본 로그

//Program.cs 파일
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLogging(); // 로그 추가
builder.Services.AddControllers(); 

// 로그 설정
builder.Logging.ClearProviders(); // 공급자 제거
builder.Logging.AddConsole();  // 콘솔 공급자 추가


//...
//LoginController.cs 파일
[ApiController]
[Route("[controller]")]
public class Login : ControllerBase
{
    private readonly ILogger Logger;

	//DI를 위한 생성자
    public Login(ILogger<Login> logger)
    {
        Logger = logger;
    }
    //...
    
    [HttpPost]
    public async Task<PkLoginResponse> Post(PkLoginRequest request)
    {
        //...
        //로그 출력
    	Logger.LogInformation($"[Request Login] Email:{request.Email}, request.Password:{request.Password},  saltValue:{userInfo.SaltValue}, hashingPassword:{hashingPassword}");
    }
    //...

WebAPI 만들기

WebAPI를 단계 별로 만들어 보겠습니다.

기본적인 컨트롤러 및 데이터베이스 연결

  1. WebAPI 프로젝트 생성
//Program.cs
var builder = WebApplication.CreateBuilder(args);
IConfiguration configuration = builder.Configuration;
builder.Services.AddControllers();

var app = builder.Build();

app.UseRouting();

app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

app.Run(configuration["ServerAddress"]);
  1. 데이터베이스를 관리할 클래스를 작성하겠습니다.
    • DI를 위해 인터페이스를 작성하고 이를 상속 받습니다.
    • ILoggerIOptions 매개변수를 갖는 생성자를 작성합니다.
    • 생성자에서 MySQL에 연결을 시도합니다.
    • IDisposable을 구현한 객체가 소멸할 때 Dispose가 호출 됩니다.
//AccountDatabase.cs

namespace firstAPI.Services
{
	public interface IAccountDatabase : IDisposable
	{

	}
	public class AccountDatabase : IAccountDatabase
	{
		private readonly IOptions<DatabaseConfiguration> _configurationOptions;
		private readonly ILogger<AccountDatabase> _logger;

		private IDbConnection _databaseConnection;
		QueryFactory _queryFactory;

		public AccountDatabase(ILogger<AccountDatabase> logger, IOptions<DatabaseConfiguration> configurationOptions)
		{
			_configurationOptions= configurationOptions;
			_logger= logger;

			_databaseConnection = new MySqlConnection(configurationOptions.Value.AccountDatabase);
			_databaseConnection.Open();

			var compiler = new MySqlCompiler();
			_queryFactory = new QueryFactory(_databaseConnection, compiler);
		}


		public void Dispose()
		{
			_databaseConnection.Dispose();
			//_queryFactory.Dispose();
		}
	}
}

public class DatabaseConfiguration
{
	public String AccountDatabase { get; set; }
	public String GameDb { get; set; }
	public String Redis { get; set; }
}
  1. 계정 생성 요청을 처리하는 컨트롤러 CreateAccountController를 작성합니다.
    • 생성자의 IAccountDatabase 변수에 DI 하기 위해 Program.cs에서 명시적인 함수 호출이 필요합니다.
    • http://localhost:11500/CreateAccount Post 요청을 받으면 콘솔 창에 출력 합니다.
[Route("[controller]")]
[ApiController]
public class CreateAccountController : ControllerBase
{
	private readonly IAccountDatabase _accountDatabase;
	private readonly ILogger<CreateAccountController> _logger;

	public CreateAccountController(IAccountDatabase accountDatabase, ILogger<CreateAccountController> logger)
	{
		_accountDatabase = accountDatabase;
		_logger = logger;
	}

	[HttpPost]
	public async Task<PacketTest> Post(PacketTest packet)
	{
		Console.WriteLine(packet.Email);
		Console.WriteLine(packet.Password);

		return packet;
	}
}

public class PacketTest
{
	public String Email { get; set; }
	public String Password { get; set; }
}
  1. Program.cs에 DI를 위한 코드를 작성합니다.
    • 1, 2 행은 애플리케이션 설정 정보에 접근하여 DatabaseConfiguration 섹션의 정보를 가져와 DI 컨테이너에 의존성 등록합니다.
    • 3행은 IAccountDatabase를 구현하는 AccountDatabase 클래스에 대한 의존성을 등록합니다.
IConfiguration configuration = builder.Configuration;
builder.Services.Configure<DatabaseConfiguration>(configuration.GetSection(nameof(DatabaseConfiguration)));
builder.Services.AddTransient<IAccountDatabase, AccountDatabase>();

데이터 베이스 연결과 요청 처리가 잘 되었음을 볼 수 있다.

  • Postman 요청 및 응답

  • 콘솔 출력


계정을 생성하고 데이터베이스에 입력하기

  1. AccountDatabase클래스에 CreateAccountAsync함수를 만듭니다.
    • 데이터베이스에 Insert 합니다.
public async Task<ErrorCode> CreateAccountAsync(String email, String password)
{
	try
	{
    	//임시 값
		var saltValue = 991;
		var hashingPassword = 119;
		Console.WriteLine($"[CreateAccount] Email: {email}, Password: {password}");
		var count = await _queryFactory.Query("account")
				.InsertAsync(new { Email = email, SaltValue = saltValue, HashedPassword = hashingPassword });
		if (count != 1)
		{
			return ErrorCode.CreateAccountFailInsert;
		}
        
		return ErrorCode.None;
	}
	catch (Exception e)
	{
		Console.WriteLine(e);
		return ErrorCode.CreateAccountFailException;
	}
}
  1. 응답 패킷 클래스를 만들고 CreateAccountController.Post함수를 수정한다.
	public class PkCreateAccountResponse
	{
		public ErrorCode Result { get; set; }
	}
[HttpPost]
public async Task<PkCreateAccountResponse> Post(PkCreateAccountRequest packet)
{
	var response = new PkCreateAccountResponse();

	var errorCode= await _accountDatabase.CreateAccountAsync(packet.Email, packet.Password);
	response.Result = errorCode;
	if (errorCode != ErrorCode.None)
	{
		return response;
	}
	Console.WriteLine("Account is Created!");
	return response;
}
  • Postman 요청 및 응답

  • MySQL 생성 결과

  • 콘솔 출력


로그인 하고 Redis에 토큰 입력하기

  1. LoginController를 생성하고 함수를 생성합니다.
    • 토큰에 임시 값 999를 저장합니다.
[Route("[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
	private readonly IAccountDatabase _accountDatabase;
	private readonly IMemoryDatabase _memoryDatabase;

	public LoginController(ILogger<LoginController> logger, IAccountDatabase accountDb, IMemoryDatabase memoryDb)
	{
	
		_accountDatabase = accountDb;
		_memoryDatabase = memoryDb;
	}

	[HttpPost]
	public async Task<PkLoginResponse> Post(PkLoginRequest request)
	{
		var response = new PkLoginResponse();

		//유저 정보 확인
		var (errorCode, accountId) = await _accountDatabase.VerifyAccount(request.Email, request.Password);
		if (errorCode != ErrorCode.None)
		{
			response.Result = errorCode;
			return response;
		}
		//토큰 발행 후 추가
		var tempToken = "999";

		errorCode = await _memoryDatabase.RegisterUserAsync(request.Email, tempToken,accountId);

		if (errorCode != ErrorCode.None)
		{
			response.Result = errorCode;
			return response;
		}

		response.AuthToken = tempToken;
		return response;

	}

}
public class PkLoginRequest
{
	public String Email { get; set; }

	public String Password { get; set; }
}

public class PkLoginResponse
{
	public ErrorCode Result { get; set; }

	public string AuthToken { get; set; }
}
  1. AccountDatabase클래스에 VerifyAccount함수를 만듭니다.
    • 패스워드가 알맞다고 가정합니다.
public async Task<Tuple<ErrorCode, Int64>> VerifyAccount(string email, string password)
{

	try
	{
		var accountInformation =
			await _queryFactory.Query("account").Where("Email", email).FirstOrDefaultAsync<Account>();

		if (accountInformation == null || accountInformation.AccountId ==0)
		{
			return new Tuple<ErrorCode, Int64>(ErrorCode.LoginFailUserNotExist, 0);
		}

		return new Tuple<ErrorCode, Int64>(ErrorCode.None, accountInformation.AccountId);
	}
	catch (Exception e)
	{
		return new Tuple<ErrorCode, Int64>(ErrorCode.LoginFailException, 0);
	}
	
}

public class Account
{
	public Int64 AccountId { get; set; }

	public String Email { get; set; }
	public String HashedPassword { get; set; }
	public String SaltValue { get; set; }
}
  1. IMemoryDatabase인터페이스와 이를 상속한 RedisDatabase 클래스를 만듭니다.
    • RegisterUser 키와 1번에서 임시로 만든 토큰을 저장합니다.
public interface IMemoryDatabase
{
	Task<ErrorCode> RegisterUserAsync(string id, string authToken, Int64 accountID);
}
public class RedisDatabase : IMemoryDatabase
{
	public RedisConnection _redisConnection;


	public RedisDatabase(IOptions<DatabaseConfiguration> configuration)
	{
		var config = new RedisConfig("default", configuration.Value.Redis);
		_redisConnection = new RedisConnection(config);
	}

	public async Task<ErrorCode> RegisterUserAsync(string email, string authToken, Int64 accountId)
	{
		var tempKey = "RegisterUser";
		
		var result = ErrorCode.None;
		try
		{
			var redis = new RedisString<string>(_redisConnection, tempKey, TimeSpan.FromMinutes(15));

			redis.SetAsync(authToken);
		}
		catch (Exception e)
		{

			result = ErrorCode.LoginFailRegisterToRedis;
			return result;
		}
		

		return result;
	}
}
  • Postman 요청 및 응답

  • Redis 저장 확인

profile
어떻게 살아야 하나

0개의 댓글