cd ../
Architecture·2026-01-18·5 min read·# entry/008

Hexagonal Architecture: 테스트하기 좋은 코드를 위한 아키텍처

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

헥사고날 아키텍처

Test Code 공부 중에 등장한 키워드, 'Hexagonal Architecture(헥사고날 아키텍처)'. 이 아키텍처는 테스트 용이성과 유지보수성을 극대화하기 위해 고안되었습니다.

MSA와 같은 시스템 단위의 아키텍처가 아닌, 하나의 애플리케이션 내부 코드를 어떻게 짤지에 대한 소프트웨어 디자인 아키텍처입니다.

1. Hexagonal Architecture (Ports And Adapters) 란?

이 아키텍처의 정식 명칭은 **'Ports and Adapters(포트 앤 어댑터)'**입니다. 핵심은 비즈니스 로직을 외부 세계로부터 철저히 격리시키는 것입니다.

3가지 구성 요소
  • 내부 (육각형 안): 순수한 비즈니스 로직(도메인). 프레임워크, UI, DB 어디에도 의존하지 않습니다.
  • 외부 (육각형 밖): 웹(Web), 데이터베이스(DB), 외부 API 등 기술적인 세부 사항들입니다.
  • 연결고리 (포트와 어댑터): 내부는 오직 **'포트(인터페이스)'**를 통해서만 외부와 소통하고, 외부는 **'어댑터(구현체)'**를 통해 이 포트에 끼워집니다.

쉬운 비유로 컴퓨터의 USB 포트를 생각하면 됩니다. USB 포트(Port)에 마우스, 키보드, 프린터 등 어떤 기기의 어댑터(Adapter)를 꽂아도 컴퓨터 본체(도메인)의 동작 방식은 변하지 않습니다. 헥사고날 아키텍처는 소프트웨어를 이렇게 만드는 것입니다.


2. 왜 Layered Architecture 대신 언급될까?

우리가 흔히 접하는 Spring의 Layered Architecture(계층형 아키텍처)는 다음과 같이 3개 레이어로 구성됩니다.

  • Presentation Layer: API 등 클라이언트에 노출되는 레이어
  • Business Layer: 비즈니스 로직을 수행하는 레이어
  • Persistence Layer: DB, API 등 데이터와 연결되는 레이어

이 구조는 의존성이 위에서 아래로 흐르는 수직적인 구조이며, 시작점인 DB(Persistence Layer)에 중점을 두고 설계되는 경향이 있습니다.

결정적인 차이: DIP (의존성 역전 원칙)

Layered Architecture의 문제
  • Service가 Repository(DB 구현체)를 직접 의존하기 쉽습니다.
  • 비즈니스 로직이 DB 기술(JPA, MyBatis 등)에 종속될 수 있습니다.
Hexagonal Architecture의 해결책
  • 의존성 역전: Service는 Repository의 **인터페이스(Port)**만 바라봅니다.
  • 실제 DB 구현체(Adapter)가 그 인터페이스를 구현하여, 의존성의 화살표가 바깥에서 안쪽(도메인)으로 향하게 만듭니다.

3. 테스트 코드 관점에서의 장점

Layered Architecture에서는 DB가 연결되어야만 제대로 된 테스트가 가능한 경우가 많습니다.

하지만 헥사고날 아키텍처의 경우, 비즈니스 로직은 오직 인터페이스(Port)만 바라봅니다. 따라서 테스트할 때 실제 DB 어댑터 대신 **가짜 어댑터(Mock 객체)**를 포트에 끼워 넣기만 하면 됩니다.

테스트 격리

DB가 죽어도 비즈니스 로직 테스트는 100% 성공할 수 있습니다. 이것이 헥사고날 아키텍처가 주는 가장 큰 선물입니다.


4. 코드 예제로 비교하기

전통적인 Layered Architecture와 Hexagonal Architecture가 코드로 어떻게 다른지 비교해 봅시다.

비즈니스 로직이 JPA(@Entity, JpaRepository)에 강하게 결합되어 있습니다.

layered-project
Member.java (Entity)
MemberRepository.java (Interface extends JpaRepository)
MemberService.java
/* 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 코드까지 다 뜯어고쳐야 합니다.

Hexagonal Architecture의 외부 어댑터 구현

내부(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);
    }
}

5. 결론: 언제 써야 할까?

비교해 보면 알 수 있듯이, 헥사고날 아키텍처는 확실한 비용(Trade-off) 이 든다.

구분Layered ArchitectureHexagonal Architecture
복잡도단순함복잡함 (파일 수 2배 이상 증가)
의존성Service ➡ Repository (JPA)Adapter ➡ Port ⬅ Service
매핑불필요 (Entity = Domain)필수 (Entity $\leftrightarrow$ Domain 변환)
한계점
늘 이런 복잡한 아키텍처나 디자인 패턴의 단점은 오버 엔지니어링입니다. 단순한 CRUD 프로젝트보다는 비즈니스 로직이 매우 복잡하고, 장기간 유지보수가 필요하며, 기술 스택 변경 가능성이 있는 핵심 코어 시스템에 도입하는 것이 적합합니다.