기존에 계층형 아키텍처
로 진행중이던 프로젝트가 있었다. 프로젝트 POC 단계 까진 문제가 없었다. 하지만 유지보수를 위한 테스트 코드
, 외부 기술들의 변화
, 복잡한 비즈니스 로직
등 다양한 문제를 직면하게 되었다.
이러한 문제들을 해결하기 위한 방법 중 하나로 헥사고날 아키텍처
를 알게 되었고 공부하게 되었다.
책 만들면서 배우는 클린 아키텍처
을 읽으며 공부하고 프로젝트에 적용하면서 느낀점을 위주로 헥사고날 아키텍처에 대해 정리했다.
소프트웨어 설계에 사용되는 아키텍처 패턴 중 하나이며, 사전적 의미로는 육각형 건축물을 말한다. 사실, 이름만 들어서는 전혀 어떤 아키텍처인지 감이 안온다.
위 그림을 보면 육각형이 있고, 육각형 경계를 기준으로 영역을 내부와 외부로 나눌 수 있다. 헥사고날 아키텍처는 내부의 도메인 비즈니스로직이 외부요소에 의존하지 않도록 설계된 아키텍처이다.
영역의 외부에는 어댑터가, 내부는 유스케이스와 엔티티, 그리고 그 경계는 포트로 이루어져있다. 각 요소에 대해 설명하며 어떻게 헥사고날 아키텍처가 내부와 외부의 결합도를 낮추고, 그렇게 하면 뭐가 좋은지 알아보겠다.
헥사고날 아키텍처는 다른 말로 포트와 어댑터 아키텍처라고도 한다. 그만큼 헥사고날 아키텍처를 이해하기 위해선 포트와 어댑터가 무엇이고, 이들의 역할에 대해 이해할 필요가 있다.
이렇게 설명만 봐서는 감이 안오니까, 송금 기능을 이용하는 과정을 예시로 들며 흐름을 설명하겠다.
위 그림은 송금 기능을 이용하는 과정을 그림으로 그린 것이다. 예시 코드는 여기를 참고하면 된다.
사용자의 HTTP 요청을 인바운드 어댑터인 AccountController
가 받고, 인바운드 포트인 SendMoneyUseCase
를 사용하여 내부 로직에 접근한다.
@WebAdapter
@RestController
@RequiredArgsConstructor
class AccountController {
private final SendMoneyUseCase sendMoneyUseCase;
@PostMapping(path = "/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}")
void sendMoney(
@PathVariable("sourceAccountId") Long sourceAccountId,
@PathVariable("targetAccountId") Long targetAccountId,
@PathVariable("amount") Long amount
) {
sendMoneyUseCase.sendMoney(sourceAccountId, targetAccountId, amount);
}
}
public interface SendMoneyUseCase {
boolean sendMoney(Long sourceAccountId, Long targetAccountId, Long amount);
}
인바운드 포트 SendMoneyUseCase
를 구현한 SendMoneyService
는 비즈니스 로직을 처리하고, 아웃바운드 포트인 LoadAccountPort
를 통해 결과를 외부로 전달한다.
@RequiredArgsConstructor
@UseCase
@Transactional
public class SendMoneyService implements SendMoneyUseCase {
private final LoadAccountPort loadAccountPort;
@Override
public boolean sendMoney(Long sourceId, Long targetId, Long amount) {
LocalDateTime baseDate = LocalDateTime.now().minusDays(10);
Account sourceAccount = loadAccountPort.loadAccount(sourceId, baseDate);
Account targetAccount = loadAccountPort.loadAccount(targetId, baseDate);
return true;
}
}
public interface LoadAccountPort {
Account loadAccount(Long accountId, LocalDateTime baseDate);
}
LoadAccountPort
를 구현한 아웃바운드 어댑터인 AccountPersistenceAdapter
는 받은 결과를 DB에 저장한다.
@RequiredArgsConstructor
@PersistenceAdapter
class AccountPersistenceAdapter implements LoadAccountPort {
private final SpringDataAccountRepository accountRepository;
private final ActivityRepository activityRepository;
private final AccountMapper accountMapper;
@Override
public Account loadAccount(Long accountId, LocalDateTime baselineDate) {
AccountJpaEntity account = accountRepository.findById(accountId)
List<ActivityJpaEntity> activities =
activityRepository.findByOwnerSince(accountId, baseDate);
Long withdrawalBalance = orZero(
activityRepository.getWithdrawalBalanceUntil(accountId, baseDate)
);
Long depositBalance = orZero(
activityRepository.getDepositBalanceUntil(accountId, baseDate)
);
return accountMapper.mapToDomainEntity(
account, activities, withdrawalBalance, depositBalance
);
}
private Long orZero(Long value){
return value == null ? 0L : value;
}
}
이제 헥사고날 아키텍처가 무엇인지 알았을 것이다. 그렇다면, 기존에 많이 사용하던 계층형 아키텍처와 비교해서 어떤 점이 좋은걸까?
우선 계층형 아키텍처는 웹, 도메인, 영속성 계층으로 이루어진 아키텍처이다. 각 계층은 하위 계층에 의존한다. 이러한 계층형 아키텍처는 아래와 같은 문제점들이 있다.
MySQL
을 사용하여 퀴즈 정보를 저장하던 것을 Redis
를 사용하도록 리팩토링을 하게 되었다. 하지만 1번과 같이 도메인 로직이 영속성 관점과 섞여있는 상태에서 이를 리팩토링하는 것은 상당히 힘들었다.그렇다면, 헥사고날 아키텍처는 계층형 아키텍처와 비교했을 때 어떤 장점이 있을까?
@RequiredArgsConstructor
@UseCase
@Transactional
public class SendMoneyService implements SendMoneyUseCase {
private final LoadAccountPort loadAccountPort;
@Override
public boolean sendMoney(Long sourceId, Long targetId, Long amount) {
LocalDateTime baseDate = LocalDateTime.now().minusDays(10);
Account sourceAccount = loadAccountPort.loadAccount(sourceId, baseDate);
Account targetAccount = loadAccountPort.loadAccount(targetId, baseDate);
return true;
}
}
위와 같은 SendMoneyService
입장에서는 외부에서 데이터를 어떻게 가져오는지 알 필요가 없다. 이로 인해 외부 시스템에 구애받지 않고 도메인 중심 설계를 할 수 있다.그렇다면 헥사고날 아키텍처는 은탄환
일까? 절대 아니다. 헥사고날 아키텍처는 아래와 같은 단점들이 있다.
항상 완벽한 아키텍처는 없다. 사실 처음 헥사고날 아키텍처를 들었을 때는 그냥 실속없이 이름만 멋진 개념인 줄 알았다. 하지만, 계층형 아키텍처로 프로젝트를 진행하면서 마주한 문제점들을 해결하기 위해 찾다가 헥사고날 아키텍처를 알게 되었다. 처음에는 반신반의 했지만, 책을 읽어보고 직접 프로젝트에 적용해보며 현재 상황에 잘 맞는 아키텍처라는 생각이 들게되었고, 결국 헥사고날 아키텍처로 리팩토링하게 되었다.
무조건 유행한다고 해보기 보다는 현재 진행하는 프로젝트의 성격과 잘 비교해서 적용하면 좋을 것 같다.