Hexagonal Study

tacoyaggi ㅣ 2024. 2. 8. 08:07

목적

헥사고날 아키텍처에 대해서 공부하기 위한 헥사고날 스터디 프로젝트

 

 

기술

  • 언어
    • C#
  • 프레임워크
    • ASP.NET CORE7
  • ORM
    • Dapper
  • 아키텍처
    • 헥사고날
  • 디자인 패턴
    • DTO

 

 

구조

헥사고날 아키텍처는 외부에서 내부로 향하는 여러 Port와 Adapter로 구성 됌.

사전적인 의미로는 육각형 건축물을 의미하지만 풀어서 생각해보면 아래와 같다고 생각함.

핵심은 외부 요청은 오로지 In Adapter를 통해서 내부로 진입 할 수 있다.

In Adapter는 In Port를 통해서 도메인 로직으로 들어 올 수 있음.

Out Adapter도 마찬가지로 Out Port를 통해서 도메인 로직으로 데이터를 전달할 수 있음.

핵심은 Applicatoin 단계에서 외부로 향하는 의존성이 없다는 것임.

모두 Port와 Adapter를 통해 처리하기 때문에 외부 영향으로 도메인 로직이 변경되는 일이 없음.

외부와의 접촉을 인터페이스로 추상화하여 비즈니스 로직 안에 외부 코드나 로직의 주입을 막고 외부로 부터 분리시키는 것이 핵심.

 

 

이번 헥사고날 스터디 프로젝트의 폴더 구조임.

ASP.NET Core는 패키지 단위가 아니라 프로젝트 단위라서 In Adapter 부분에 프로젝트 전체가 들어갔음.

 

 

포트는 어댑터와 도메인 서비스를 연결하는 매개체로 추상적이어야 하므로 Interface로 생성해줌.

namespace InPort
{
    public interface IAccountService
    {
        public Task<bool> Login(AccountDto requestDto);
    }
}

 

 

Out Port도 마찬가지로 Interface 생성해줌.

다만 아웃 포트의 경우 DB 와 관련된 인터페이스를 생성함.

namespace OutPort
{
    public interface IAccountRepository
    {
        public  Task<SupplierAdminEntity> GetAccount(AccountDto requestDto);
    }
}
namespace OutPort
{
    public interface IDbContext
    {
        IDbConnection GetConnection(string conn = "TacoDB");
    }
}

 

 

컨트롤러인 In Adapter 부분이며 Out Adapter 와 차이점은 In Port 인터페이스를 상속 받는게 아닌 의존성 주입으로 처리함.

API 컨트롤러의 역할도 수행하면서 In Adapter 의 역할도 수행해야 하기 때문에 의존성 주입으로 처리함.

    [Route("api/[controller]")]
    [ApiController]
    public class AccountController : ControllerBase
    {
        private readonly IAccountService _accountService;

        public AccountController(IAccountService accountService)
        {
            _accountService = accountService;
        }

        [HttpGet]
        public async Task<IActionResult> Login([FromQuery] AccountDto requestDto)
        {
            return await _accountService.Login(requestDto) ? Ok() : BadRequest();
        }
    }

 

 

의존성 주입을 적용한 코드.

Service 부분이 In Port 를 의미하고 Repository 는 Out Port를 의미함.

(3Tier 아키텍처의 Service -> Repository 방향성을 네이밍에 적용해봄.)

builder.Services.AddScoped<IDbContext, DbContext>();
builder.Services.AddScoped<IAccountService, AccountService>();
builder.Services.AddScoped<IAccountRepository, AccountRepository>();

 

 

Out Adapter 부분임. Out Port를 상속 받고 인터페이스 구현 소스를 작성함.

생성자를 통해 의존성 주입된 서비스를 호출하고, Dapper와 프로시저를 통해 ORM 적용.

이부분이 조금 헷갈리는데 ORM 부분을 Out Adapter 단계에서 구현해야하는지 Application 단계에서 구현해야하는지 고민을 했음. 결과적으로는 Out Adapter 단계에서 구현체를 구성했음.

이유는 Application 에서 Port 를 거쳐 Adapter 로 실행되는 프로세스인데 Adapter에서 Application으로 다시 돌아가는 프로세스가 비효율적이라고 생각했기 때문에, Out Adapter부분에 구현체 소스를 작성함.

namespace OutAdapter
{
    public class AccountRepository : IAccountRepository
    {
        private readonly IDbContext _dbContext;

        public AccountRepository(IDbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public async Task<SupplierAdminEntity> GetAccount(AccountDto requestDto)
        {
            using IDbConnection db = _dbContext.GetConnection();

            DynamicParameters param = new DynamicParameters();
            param.Add("@Id", requestDto.Id);
            param.Add("@Pw", requestDto.Pw);

            return await db.QueryFirstAsync<SupplierAdminEntity>(ExtraSP.SP_GetCustomer, param, commandType: CommandType.StoredProcedure);
        }
    }
}

 

 

Application 단계에서는 실제로 서비스 하는 구간임. Out Port를 통해 DB에 접근함.

가장 중요한 부분이라고 생각하고 In Port 그리고 Out Port가 공존하는 공감임.

상속은 In Port만 받고 Out Port는 의존성으로 처리함. Out Port의 구현체는 Out Adatper에 구현되었기 때문임.

항상 데이터 전달 형태는 DTO를 사용했고 ORM이 적용되는 부분만 Entity를 사용함.

namespace Applicatoin
{
    public class AccountService : IAccountService
    {
        private readonly IAccountRepository _accountRepository;

        public AccountService(IAccountRepository accountRepository)
        {
            _accountRepository = accountRepository;
        }

        public async Task<bool> Login(AccountDto requestDto)
        {
            try
            {
                requestDto.Pw = SHA256.EncryptSHA256PassWord(requestDto.Pw!);

                SupplierAdminEntity resultEnitty = await _accountRepository.GetAccount(requestDto);

                if (resultEnitty == null)
                {
                    throw new Exception("등록된 회원이 없습니다.");
                }

                return true;
            }
            catch (Exception ex)
            {
                return false;
            }
        }
    }
}