멋쟁이사자처럼 연합 프로젝트를 진행하면서 헥사고날 아키텍처에 대해 처음 접하게 되었습니다. 프로젝트를 원활히 진행하기 위해서는 이해도가 중요하다고 판단하여 헥사고날 아키텍처에 대해 알아보았습니다.
객체를 직접 참조하지 않고, 대신 선언된 인터페이스를 통해 객체에 접근하면 의존성을 끊을 수 있습니다. 이렇게 하면 인터페이스를 구현한 다른 객체를 연결할 수 있기 때문입니다.
프레젠테이션 계층이 도메인 계층을 참조하고 업무로직은 영속성 계층을 참조하는데 이렇게 되면은 프레젠테이션 계층이 영속성 계층을 직접 참조하는 것과 같습니다.
연쇄 참조로 의존성을 갖으면 한 곳에서 변경이 일어나면 참조된 다른 곳에서도 변경 영향을 가집니다.
테스트가 쉽지 않습니다.
UI와 데이터 변경이 빈번하게 일어나는데 이는 업무로직에 영향을 줍니다.
1. 데이터베이스(+영속성)에 대한 의존성이 퍼지게 됩니다.
전통적인 계층형 아키텍처에서 도메인 계층은 영속성에 의존합니다. 도메인 계층이 데이터베이스에 의존하게 되면, 데이터베이스에 변화가 발생하면 도메인 계층도 이에 영향을 받게 됩니다.
해당 도메인을 사용하는 서비스 계층에서도 즉시 로딩, 지연 로딩, 트랜잭션, 플러시 등을 고려해야 하며, 이러한 과정에서 영속성에 대한 의존이 프로젝트 전반으로 확산되어 변경에 취약해질 수 있습니다.
2. 아키텍처 경계를 강제할 수 없습니다.
행위가 계속해서 쌓이면, 경계가 점점 모호해지고 결국에는 허물어져 버릴 수 있습니다. 이로 인해 모든 계층에서 헬퍼나 유틸리티에 의존하게 될 가능성이 있습니다.
3. 계층을 Skip 할 수 있습니다.
계층형 구조에서는 종종 계층을 건너뛰는 것이 가능합니다. 즉, 구현이 간단한 경우 컨트롤러에서 바로 도메인을 참조하여 로직을 작성할 수 있습니다.
4. 유스케이스를 숨깁니다.
개발자가 해당 유스케이스의 존재 여부를 파악하기 어려워서 동일한 로직을 다른 위치에서 새롭게 구현할 수 있으며, 이는 코드를 더럽힐 수 있습니다.5. 서비스의 크기를 강제할 수 없다.
계층형 구조에서는 서비스의 크기를 강제하지 않습니다. 서비스에는 수십 개의 서비스 로직을 작성할 수 있습니다. 그러나 서비스는 너무 많은 의존성을 가지며, 수많은 웹 계층이 해당 서비스를 의존하게 됩니다. 이로 인해 서비스를 테스트하기 어려워지고 수행해야 할 use case를 찾기도 어려워집니다.
의존성은 안쪽으로 향함
헥사고날 아키텍처는 내부(도메인)와 외부(인프라)로 구분됩니다.
도메인 모델
DDD의 도메인 모델
어떠한 의존성도 없어야 하는 것이 원칙입니다.
예외 상황 : 레포지토리의 경우에는 포트를 이용해 어댑터를 주입받아서 사용합니다.
헥사고날 아키텍처는 사용자 인터페이스나 데이터베이스 모두 비즈니스 로직으로부터 분리해야 하는 외부 요소로 봅니다.
모든 의존성은 코어를 향합니다.
도메인의 비즈니스 로직을 외부 라이브러리 및 도구로부터 분리할 때 사용하는 인터페이스를 포트와 어댑터라고 합니다. 이러한 구조로 인해 이 아키텍처는 포트와 어댑터 아키텍처로도 알려져 있습니다.
Primary(Driving) Adapters(왼쪽)
Secondary(Driven) Adapters(오른쪽)
외부로부터의 도메인 로직의 결합을 제거하며, 변경할 이유가 적을수록 유지보수성이 높은 코드입니다.
payment-system
ㄴ account
ㄴ adapter
ㄴ in
ㄴ web
ㄴ AccountController
ㄴ out
ㄴ persistence
ㄴ AccountPersistenceAdapter
ㄴ SpringDataAccountRepository
ㄴ domain
ㄴ Account
ㄴ Activity
ㄴ application
ㄴ SendMoneyService
ㄴ port
ㄴ in
ㄴ SendMoneyUseCase
ㄴ out
ㄴ LoadAccountPort
ㄴ UpdateAccountStatePort
in 패키지(usecase)
out 패키지(port)
내부에는 persistencePort를 선언하고 외부에는 persistenceAdapter를 구현합니다. 이는 core에는 DB와 관련된 의존성을 제거하기 위함입니다. 도메인이 JPA Entity가 아니기 때문에 변경 감지를 사용하지 못하고 아웃포트를 통해 DB에 update 쿼리가 실행되도록 해주어야 합니다.
// 기존 레이어드 아키텍처 - Member는 JPA Entity
@Service
public class MemberService {
private final MemberRepository memberRepository;
// ...
@Transactional
public void updateNickname(Long memberId, NicknameUpdateRequest nicknameUpdateRequest) {
Member member = findMember(memberId);
member.updateNickname(nicknameUpdateRequest.getNewNickname());
}
// ...
}
// 변경된 헥사고날 아키텍처 - Member는 POJO
@Service
public class MemberService implements MemberUseCase {
private final MemberPersistencePort memberPersistencePort;
// ...
@Override
@Transactional
public void updateNickname(Long memberId, NicknameUpdateCommand nicknameUpdateCommand) {
Member member = findMember(memberId);
memberPersistencePort.updateNickname(member.getId(), nicknameUpdateCommand.getNickname());
}
// ...
}
- 외부와의 연결에 문제가 생기면?
어댑터
- 인터페이스는?
포트
- 처리 중간에 EventBridge에 이벤트를 보내거나 트레이스 로그를 심고 싶다면?
서비스
- 비즈니스 로직이 제대로 동작하지 않으면?
도메인 모델