Layered Architecture의 한계를 극복하고, 비즈니스 로직을 기술로부터 격리시키는 헥사고날 아키텍처(Ports and Adapters)를 코드 예제와 함께 알아봅니다.

Test Code 공부 중에 등장한 키워드, 'Hexagonal Architecture(헥사고날 아키텍처)'. 이 아키텍처는 테스트 용이성과 유지보수성을 극대화하기 위해 고안되었습니다.
MSA와 같은 시스템 단위의 아키텍처가 아닌, 하나의 애플리케이션 내부 코드를 어떻게 짤지에 대한 소프트웨어 디자인 아키텍처입니다.
이 아키텍처의 정식 명칭은 **'Ports and Adapters(포트 앤 어댑터)'**입니다. 핵심은 비즈니스 로직을 외부 세계로부터 철저히 격리시키는 것입니다.
쉬운 비유로 컴퓨터의 USB 포트를 생각하면 됩니다. USB 포트(Port)에 마우스, 키보드, 프린터 등 어떤 기기의 어댑터(Adapter)를 꽂아도 컴퓨터 본체(도메인)의 동작 방식은 변하지 않습니다. 헥사고날 아키텍처는 소프트웨어를 이렇게 만드는 것입니다.
우리가 흔히 접하는 Spring의 Layered Architecture(계층형 아키텍처)는 다음과 같이 3개 레이어로 구성됩니다.
이 구조는 의존성이 위에서 아래로 흐르는 수직적인 구조이며, 시작점인 DB(Persistence Layer)에 중점을 두고 설계되는 경향이 있습니다.
Layered Architecture에서는 DB가 연결되어야만 제대로 된 테스트가 가능한 경우가 많습니다.
하지만 헥사고날 아키텍처의 경우, 비즈니스 로직은 오직 인터페이스(Port)만 바라봅니다. 따라서 테스트할 때 실제 DB 어댑터 대신 **가짜 어댑터(Mock 객체)**를 포트에 끼워 넣기만 하면 됩니다.
DB가 죽어도 비즈니스 로직 테스트는 100% 성공할 수 있습니다. 이것이 헥사고날 아키텍처가 주는 가장 큰 선물입니다.
전통적인 Layered Architecture와 Hexagonal Architecture가 코드로 어떻게 다른지 비교해 봅시다.
비즈니스 로직이 JPA(@Entity, JpaRepository)에 강하게 결합되어 있습니다.
/* 1. Domain Entity (JPA 엔티티가 곧 도메인 모델) */
@Entity
@Getter @NoArgsConstructor
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
// ...
}
/* 2. Service (비즈니스 로직) */
@Service
@RequiredArgsConstructor
public class MemberService {
/* Service가 기술(JPA Repository)에 직접 의존 */
private final MemberRepository memberRepository;
@Transactional
public void register(String name, String email) {
Member member = new Member(name, email);
memberRepository.save(member);
}
}
문제점: JPA를 MyBatis나 NoSQL로 바꾸려면 MemberService 코드까지 다 뜯어고쳐야 합니다.
내부(Inner) 코드를 다 짰다면, 이제 바깥세상(Outer)과 연결할 어댑터를 구현합니다.
/* [Outer] 1. JPA Entity (DB 매핑용 별도 클래스) */
@Entity
@Table(name = "member")
class MemberJpaEntity {
/* 도메인 모델 <-> JPA 엔티티 변환 메서드 필요 */
static MemberJpaEntity from(Member member) { ... }
}
/* [Outer] 2. Persistence Adapter (연결 고리) */
@Component
@RequiredArgsConstructor
public class MemberPersistenceAdapter implements SaveMemberPort {
private final SpringDataMemberRepository repository;
@Override
public void saveMember(Member member) {
/* 1. 도메인 모델을 JPA 엔티티로 변환 (Mapping) */
MemberJpaEntity jpaEntity = MemberJpaEntity.from(member);
/* 2. 실제 DB 저장 수행 */
repository.save(jpaEntity);
}
}
비교해 보면 알 수 있듯이, 헥사고날 아키텍처는 확실한 비용(Trade-off) 이 든다.
| 구분 | Layered Architecture | Hexagonal Architecture |
|---|---|---|
| 복잡도 | 단순함 | 복잡함 (파일 수 2배 이상 증가) |
| 의존성 | Service ➡ Repository (JPA) | Adapter ➡ Port ⬅ Service |
| 매핑 | 불필요 (Entity = Domain) | 필수 (Entity $\leftrightarrow$ Domain 변환) |