Rest API Template 만들기 - EP 10 (Refactoring - Features)

2023. 1. 19. 00:00ASPNET/ASPNET 7

반응형

이제 UnitTest 를 만들었으니 기존 코드들에서 재사용 가능한 부분들을 refactoring 해보자

또한 design 적으로 수정할 부분이 있다면 이 부분들도 refactoring 해보자

 

UnitTest 를 만들기 전에는 이러한 수정들이 다른 코드들에 영향을 줄 수 있으므로 수정하기 꺼려진다.

Test 를 해보면 10 중 8~9 는 분명히 문제가 생길 것이고  미쳐 생각하지 못한 부분들에서

알수 없는 오류들이 나올 수 있다. UnitTest 는 이러한 문제들을 찾는데 도움이 될 것이다. 

 

ApiServerContext 와 IConfiguration 은 대부분의 Handler 에서 사용하게 될것 같으니 공통 class 에 넣자

또한 apiLogger 도 추가하자. 향후에 사용할 가능성이 많다. 

 

Features/Shared/AbstractBaseHandler.cs

using ApiServer.Shared.Interfaces;
using Infrastructure;

namespace ApiServer.Features.Shared;

public abstract class AbstractBaseHandler
{
    protected readonly ApiServerContext _context;
    protected readonly IConfiguration _configuration;
    protected readonly IApiLogger _apiLogger;

    public AbstractBaseHandler(ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
    {
        _context = context;
        _configuration = configuration;
        _apiLogger = apiLogger;
    }
}

 

이제 각각의 Handler 들을 AbstractBaseHandler 를 상속하도록 수정하자.

Features/User/Register.cs

using ApiServer.Features.Shared;
using ApiServer.Helper;
using ApiServer.Shared.Interfaces;
using Infrastructure;
using MediatR;

namespace ApiServer.Features.User;

public class Register
{
    ...
    public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
    {
        public CommandHandler(ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger) : base(context, configuration, apiLogger)
        {
        }
       ...
    }
}

 

Features/User/Login.cs

using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using ApiServer.Shared.Models;
using Infrastructure;
using MediatR;

namespace ApiServer.Features.User;

public class Login
{
    ...
    public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
    {
        private readonly ITokenGenerator _tokenGenerator;

        // ITokenGenerator 를 Constructor injection 
        public CommandHandler(ITokenGenerator tokenGenerator, ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
            : base(context, configuration, apiLogger)
            => _tokenGenerator = tokenGenerator;
       ...
    }
}

 

Features/User/Logout.cs

using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using Infrastructure;
using MediatR;

namespace ApiServer.Features.User;

public class Logout
{
    ...
    public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
    {
        private readonly ITokenGenerator _tokenGenerator;

        // ITokenGenerator 를 Constructor injection 
        public CommandHandler(ITokenGenerator tokenGenerator, ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
            : base(context, configuration, apiLogger)
        {
            _tokenGenerator = tokenGenerator;
        }
        ...
    }
}

 

Features/User/Refresh.cs

using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using ApiServer.Shared.Models;
using Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace ApiServer.Features.User;

public class Refresh
{
    ...
    public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
    {
        private readonly ITokenGenerator _tokenGenerator;
        public CommandHandler(ITokenGenerator tokenGenerator, ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
            : base(context, configuration, apiLogger)
            => _tokenGenerator = tokenGenerator;
		...
    }

}

 

이제 각 Feature 들의 Response 를 확인해 보자

모두 bool Result 와  Error? Error 을 가지고 있다. (중복은 제거하자!!) Refactoring 해보자

 

Features/Shared/BaseResponse.cs

namespace ApiServer.Features.Shared;

public abstract class BaseResponse
{
    public bool Result { get; set; }
    public Error? Error { get; set; }
}

public class Error
{
    public string Code { get; set; } = string.Empty;
    public string Message { get; set; } = string.Empty;
}

Features/User/Register.cs

...
public class Response:BaseResponse
{
}
...
// Error class 삭제

Features/User/Login.cs

...
public class Response:BaseResponse
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
}
...
// Error class 삭제

Features/User/Logout.cs

...
public class Response:BaseResponse
{
}
...
// Error class 삭제

Features/User/Refresh.cs

...
public class Response:BaseResponse
{
    public string? AccessToken { get; set; }
    public string? RefreshToken { get; set; }
}
...
// Error class 삭제

 

이제 각 handler 들에 있는 db 관련 method 들을 확인한 후 일반화가 가능하다면 refactoring 해보겠다. 

Refresh 만 refactoring 해보겠다. 다음은 refactoring 을 거친 코드이다.

 

Features/Shared/AbstractBaseHandler.cs

using ApiServer.Shared.Interfaces;
using Infrastructure;
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;

namespace ApiServer.Features.Shared;

public abstract class AbstractBaseHandler
{
    protected readonly ApiServerContext _context;
    protected readonly IConfiguration _configuration;
    protected readonly IApiLogger _apiLogger;

    public AbstractBaseHandler(ApiServerContext context, IConfiguration configuration, IApiLogger apiLogger)
    {
        _context = context;
        _configuration = configuration;
        _apiLogger = apiLogger;
    }

    protected Infrastructure.Models.Login? GetLogin(Expression<Func<Infrastructure.Models.Login, bool>> predicate)
         => _context.Logins?.Where(predicate).Include(l => l.User).SingleOrDefault();

    protected void AddLogin(string id, string accessToken, string refreshToken, int refreshExpirationMinute)
    {
        var refreshTokenExpired = DateTime.UtcNow.AddMinutes(refreshExpirationMinute);

        var findUser = _context.Users.Where(u => u.Id == id).SingleOrDefault();
        if (findUser == null) throw new Exception("Can not find user");
        var login = new Infrastructure.Models.Login
        {
            User = findUser,
            UseFlag = true,
            AccessToken = accessToken,
            RefreshToken = refreshToken,
            RefreshTokenExpired = refreshTokenExpired
        };
        _context.Logins.Add(login);
    }

}

 

Features/User/Refresh.cs

using ApiServer.Features.Shared;
using ApiServer.Shared.Interfaces;
using ApiServer.Shared.Models;
using Infrastructure;
using MediatR;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace ApiServer.Features.User;

public class Refresh
{
    public class Command : IRequest<Response>
    {
        public string AccessToken { get; set; } = string.Empty;
        public string RefreshToken { get; set; } = string.Empty;
    }

    public class Response : BaseResponse
    {
        public string? AccessToken { get; set; }
        public string? RefreshToken { get; set; }
    }

    public class CommandHandler : AbstractBaseHandler, IRequestHandler<Command, Response>
    {
        private readonly ITokenGenerator _tokenGenerator;
        public CommandHandler(ITokenGenerator tokenGenerator,
            ApiServerContext context, 
            IConfiguration configuration, 
            IApiLogger apiLogger)
            : base(context, configuration, apiLogger)
            => _tokenGenerator = tokenGenerator;

        public async Task<Response> Handle(Command request, CancellationToken cancellationToken)
        {
            var response = new Response { Result = false };
            var tokenPayload = _configuration.GetSection("TokenManagement").Get<TokenManagement>();
            if (tokenPayload == null) throw new NullReferenceException(nameof(tokenPayload));
            ClaimsPrincipal principal = ValidateToken(request.AccessToken, 
                                                      tokenPayload.Audience, 
                                                      tokenPayload.Issuer,
                                                      tokenPayload.Secret);
            var login = GetLoginInfo(request.AccessToken, request.RefreshToken, principal);
            // expiring
            login.UseFlag = false;
            var (AccessToken, RefreshToken) = ReCreateToken(login.User.Id);
            var refreshExpirationMinute = tokenPayload?.RefreshExpiration ?? 1440;
            AddLogin(login.User.Id, AccessToken, RefreshToken, refreshExpirationMinute);
            await _context.SaveChangesAsync(cancellationToken);

            response.Result = true;
            response.AccessToken = AccessToken;
            response.RefreshToken = RefreshToken;
            return response;
        }

        private Infrastructure.Models.Login GetLoginInfo
            (string accessToken, string refreshToken, ClaimsPrincipal principal)
        {
            var identity = (principal.Identity as ClaimsIdentity);
            var nameClaim = identity?.
                            Claims.
                            SingleOrDefault(c => c.Type == "Name");
            var login = GetLogin(l => l.AccessToken == accessToken
                                && l.RefreshToken == refreshToken
                                && l.RefreshTokenExpired > DateTime.UtcNow);
            if (login == null) throw new Exception("Invalid refreshToken");
            if (!login.User.Id.Equals(nameClaim?.Value, StringComparison.OrdinalIgnoreCase)) 
                throw new Exception("Invalid refreshToken");

            return login;
        }

        private static ClaimsPrincipal ValidateToken
            (string accessToken, string audience, string issuer, string secretKey)
        {
            var handler = new JwtSecurityTokenHandler();
            var validationParameters = new TokenValidationParameters
            {
                ValidAudience = audience,
                ValidIssuer = issuer,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey)),
                ValidateLifetime = false
            };
            return handler.ValidateToken(accessToken, validationParameters, out _);
        }

        private (string AccessToken, string RefreshToken) ReCreateToken(string id)
        {
            // expiring
            var newAccessToken = _tokenGenerator.Create(id);
            var newRefreshToken = _tokenGenerator.CreateRefreshToken();
            return (newAccessToken, newRefreshToken);
        }
    }

}

관련영상

https://youtu.be/WNVsI0541Go

 

 

반응형