|

헥사고날 아키텍처란 무엇인가: ports and adapters를 언제 쓰면 좋을까

헥사고날 아키텍처를 의존 방향과 테스트 격리 관점에서 설명하는 대표 이미지
핵심은 육각형 모양보다 안쪽 규칙을 바깥 기술에서 분리하는 데 있다

헥사고날 아키텍처는 이름보다 개념이 중요합니다. 핵심은 비즈니스 로직이 DB, 외부 API, 메시지 브로커, 웹 프레임워크 같은 바깥 기술에 끌려다니지 않게 만드는 것입니다. 이번 글에서는 ports and adapters를 그림이 아니라 의존 방향, 테스트 격리, 외부 시스템 교체 비용, 과설계 판단 기준까지 포함해 실무적으로 설명합니다.


헥사고날 아키텍처를 한 문장으로 설명하면

헥사고날 아키텍처는 애플리케이션 중심부를 바깥 기술로부터 분리하고, 필요한 연결은 포트와 어댑터를 통해 붙이는 구조입니다. 안쪽은 핵심 규칙과 유스케이스를 담당하고, 바깥은 DB, HTTP, 메시지 큐, 알림, 외부 SDK 같은 구현 세부사항을 맡습니다. 포트는 안쪽이 요구하는 역할 계약이고, 어댑터는 그 계약을 실제 기술과 연결하는 번역기입니다.

즉 안쪽은 무엇이 필요할지만 말하고, 바깥쪽이 그것을 어떻게 할지는 어댑터가 맡습니다.


ports and adapters를 왜 나눌까

구조가 엉키기 시작하는 흔한 장면은 비슷합니다. 컨트롤러가 요청 파싱, 비즈니스 규칙, DB 저장, 외부 API 호출을 한꺼번에 처리하고, 서비스 클래스가 특정 ORM이나 SDK에 직접 묶여 있고, 핵심 규칙을 테스트하려면 서버와 DB를 같이 띄워야 합니다. 이 상태의 진짜 문제는 코드 길이가 아니라 핵심 규칙의 변경과 기술 세부사항의 변경이 서로 묶여버린다는 점입니다.

헥사고날 아키텍처는 이 묶임을 끊으려고 합니다. 안쪽 코드는 저장해야 한다, 알림을 보내야 한다, 결제를 승인해야 한다 같은 요구만 알고, 실제 DB 종류나 SDK 호출 순서는 바깥으로 밀어냅니다. 이렇게 해두면 안쪽은 정책에 집중하고 바깥은 구현에 집중하게 됩니다.


헥사고날 아키텍처에서 가장 중요한 것은 의존 방향

헥사고날 아키텍처에서 가장 먼저 잡아야 할 감각은 의존 방향입니다. 바깥이 안쪽을 알아야지, 안쪽이 바깥 구현 세부사항을 알기 시작하면 구조가 무너집니다. 주문 승인 규칙은 본질적으로 승인 가능 여부, 상태 전이, 추가 검토 여부를 판단하는 일이지 JPA를 쓰는지 Slack으로 알림을 보내는지와 직접 관련이 없습니다.

도메인 코드 안에 ORM 엔티티 규칙, HTTP 클라이언트 호출, 메시지 발행 코드가 섞이면 정책 코드를 읽는 데도 인프라 지식이 필요해집니다. 헥사고날 아키텍처는 이 방향을 뒤집습니다. 안쪽은 포트만 알고, 바깥 어댑터가 그 포트를 구현합니다. 즉 구현에서 정책 쪽으로 의존성이 향하고, 정책은 구현 세부사항을 몰라도 됩니다.


포트는 단순 인터페이스가 아니라 역할 경계다

포트는 문법적으로 인터페이스일 수 있지만, 더 중요한 것은 안쪽이 바깥에 요구하는 역할 경계라는 점입니다. JPARepository나 SlackClient 같은 기술 이름 대신, 안쪽이 정말 필요로 하는 행동만 드러내면 유스케이스는 비즈니스 흐름 중심으로 읽히기 시작합니다.

public interface LoadOrderPort {
    Order load(Long orderId);
}

public interface SaveOrderPort {
    void save(Order order);
}

public interface NotifyApprovalPort {
    void notifyHighValueOrder(Order order);
}
public class ApproveOrderUseCase {

    private final LoadOrderPort loadOrderPort;
    private final SaveOrderPort saveOrderPort;
    private final NotifyApprovalPort notifyApprovalPort;

    public ApproveOrderUseCase(
            LoadOrderPort loadOrderPort,
            SaveOrderPort saveOrderPort,
            NotifyApprovalPort notifyApprovalPort
    ) {
        this.loadOrderPort = loadOrderPort;
        this.saveOrderPort = saveOrderPort;
        this.notifyApprovalPort = notifyApprovalPort;
    }

    public void execute(Long orderId) {
        Order order = loadOrderPort.load(orderId);
        order.approve();
        saveOrderPort.save(order);

        if (order.requiresAttention()) {
            notifyApprovalPort.notifyHighValueOrder(order);
        }
    }
}

이 코드를 읽을 때 우리는 저장 기술이나 알림 SDK보다 주문 승인 흐름에 집중할 수 있습니다. 이게 헥사고날 아키텍처가 주는 첫 번째 실질적 이득입니다.


어댑터는 교체 가능성보다 변경 범위를 줄여준다

헥사고날 아키텍처를 설명할 때 자주 나오는 말은 DB를 바꿀 수 있다는 것입니다. 맞는 말이지만, 실무에서 더 자주 체감하는 이득은 기술 교체 가능성 자체보다 변경 영향 범위가 줄어드는 것입니다. Slack 알림만 보내던 기능이 이메일과 이벤트 발행까지 늘어날 때 유스케이스가 SDK에 직접 묶여 있으면 핵심 흐름도 같이 흔들리기 쉽습니다.

public class SlackApprovalNotifier implements NotifyApprovalPort {
    @Override
    public void notifyHighValueOrder(Order order) {
        // Slack API 호출
    }
}

public class EmailApprovalNotifier implements NotifyApprovalPort {
    @Override
    public void notifyHighValueOrder(Order order) {
        // Email 전송
    }
}

포트 뒤에 어댑터를 두면 바뀌는 곳은 대체로 바깥쪽에 머뭅니다. 이 구조의 진짜 가치는 언젠가 DB를 완전히 갈아엎을 수 있다는 거대한 약속보다, 지금 바뀌는 요구사항이 핵심 정책 코드를 덜 흔들게 만든다는 데 있습니다.


테스트 격리는 왜 이렇게 좋아질까

헥사고날 아키텍처를 실제로 써본 팀이 가장 빨리 체감하는 장점은 대개 테스트 쪽입니다. 핵심 규칙이 포트만 의존하도록 정리되어 있으면 테스트에서는 무거운 바깥 환경을 끌고 오지 않아도 됩니다. 메모리 기반 테스트 어댑터나 fake 어댑터를 붙여서 핵심 규칙만 빠르게 확인할 수 있습니다.

public class InMemoryOrderAdapter implements LoadOrderPort, SaveOrderPort {

    private final Map<Long, Order> store = new HashMap<>();

    @Override
    public Order load(Long orderId) {
        return store.get(orderId);
    }

    @Override
    public void save(Order order) {
        store.put(order.getId(), order);
    }
}

public class FakeApprovalNotifier implements NotifyApprovalPort {
    boolean called;

    @Override
    public void notifyHighValueOrder(Order order) {
        called = true;
    }
}

이렇게 되면 테스트는 주문 승인 규칙이 맞는가에 집중할 수 있습니다. 서버를 띄울 필요가 없고, 실제 DB 연결이 없어도 되고, 외부 알림 API를 붙이지 않아도 되며, 실패 조건을 더 빠르게 재현할 수 있습니다. 헥사고날 아키텍처의 테스트성은 mocking 기술이 멋져서가 아니라, 핵심 규칙이 바깥 환경 없이도 성립하도록 경계를 만들었기 때문에 생깁니다.


외부 시스템 교체보다 더 현실적인 장점

실무에서는 완전 교체보다 부분적 변화가 더 자주 일어납니다. PostgreSQL은 유지하되 읽기 캐시를 Redis로 추가하거나, 동기 API 호출만 하다가 이벤트 발행을 함께 넣거나, 관리자 화면 외에 배치와 CLI 진입점을 추가하는 식입니다. 이때 포트와 어댑터 구조가 있으면 같은 핵심 유스케이스를 여러 진입점이 공유하기 쉬워집니다.

웹 컨트롤러, 배치 잡, CLI 명령어가 모두 같은 안쪽 로직을 호출할 수 있기 때문입니다. 입력 방식이 바뀌어도 핵심 규칙이 복제되지 않고, 데이터 접근 방식이 달라져도 안쪽 판단 로직은 상대적으로 안정적으로 남습니다.


레이어드 아키텍처와 무엇이 다를까

헥사고날 아키텍처는 레이어드 아키텍처를 완전히 부정하는 개념이 아닙니다. 오히려 레이어 분리 감각을 더 강하게 밀어붙인 형태로 이해하는 편이 실무적으로 쉽습니다. 레이어드 아키텍처가 presentation, domain, data access를 나누는 데 초점을 둔다면, 헥사고날 아키텍처는 안쪽 핵심 규칙을 중심에 두고 모든 외부 입출력을 대칭적인 포트와 어댑터로 본다는 감각이 더 강합니다.

즉 웹 요청도 외부 입력이고, DB도 외부 시스템이며, 메시지 발행도 외부 출력입니다. 그래서 웹은 위쪽, DB는 아래쪽이라는 그림보다 모두 바깥이라는 시야를 주는 점이 꽤 유용합니다. 함께 보면 좋은 글로는 레이어드 아키텍처란 무엇인가: 계층을 나누는 이유, 책임, 흔한 오해가 먼저 도움이 됩니다.


헥사고날 아키텍처를 쓰면 좋은 상황

  • 결제, 주문, 정산, 쿠폰, 재고, 권한처럼 핵심 규칙이 자주 바뀌고 실패 비용이 큰 서비스
  • 외부 결제, 알림, 캐시, 메시지 브로커, 검색 엔진처럼 바깥 연결이 많은 시스템
  • 핵심 규칙을 빠르고 가볍게 검증해야 하는 팀
  • REST API, 관리자 페이지, 배치, 이벤트 소비자처럼 진입점이 여러 개인 시스템

이런 시스템에서는 핵심 로직을 중심에 고정해두는 편이 전체 복잡도를 다루기 쉬워집니다.


언제부터 과설계가 될까

핵심 규칙보다 CRUD가 대부분일 때

단순 관리자 화면이나 내부 도구처럼 대부분이 조회와 저장이고 도메인 판단이 거의 없다면 포트와 어댑터를 촘촘하게 나누는 이득이 크지 않을 수 있습니다. 이 경우 클래스 수만 늘고 읽는 비용이 올라갈 수 있습니다.

의미 없는 인터페이스가 늘어날 때

모든 클래스마다 무조건 인터페이스를 만들고 실제로는 단순 전달만 하는 래퍼가 계속 늘어난다면 구조 소음이 커집니다. 포트는 있어 보이기 위한 장식이 아니라 안쪽을 보호할 이유가 있는 경계에 둘 때 힘이 있습니다.

모델 복제만 잔뜩 늘어날 때

Request DTO, Response DTO, Entity, Domain Model, Command, Result가 모두 거의 같은 필드만 복사하고 있다면 경계 보호보다 ceremony가 커졌을 가능성이 있습니다. 분리는 비용이고, 그 비용이 정당화되려면 실제로 보호해야 할 경계가 있어야 합니다.

팀이 아직 운영할 준비가 안 되었을 때

작은 팀에서 구조 원칙보다 구현 속도가 더 중요한 초기 단계라면 완성형 헥사고날 아키텍처를 억지로 밀어 넣는 것이 오히려 독이 될 수 있습니다. 그럴 때는 전체 구조를 한 번에 바꾸기보다 핵심 규칙 하나를 먼저 분리해보는 편이 현실적입니다.


작게 시작하는 방법

  1. 컨트롤러나 서비스에 섞여 있는 핵심 규칙을 먼저 찾는다
  2. 그 규칙을 프레임워크 밖의 유스케이스나 도메인 객체로 옮긴다
  3. 저장소나 외부 API 호출 중 변경 가능성이 큰 부분만 포트로 뽑는다
  4. 테스트에서 인메모리 어댑터나 fake 어댑터를 붙여본다
  5. 효과가 보이면 알림, 캐시, 메시지 브로커 같은 다른 경계로 확장한다

이렇게 하면 구조가 문제를 따라 자랍니다. 반대로 처음부터 모든 입출력 경계를 교과서처럼 인터페이스로 둘러싸면 이해보다 ceremony가 먼저 커질 수 있습니다.


체크리스트

  • 핵심 규칙을 테스트하려면 DB나 서버를 같이 띄워야 하는가
  • 외부 API나 저장 기술의 변경이 비즈니스 로직 수정으로 번지는가
  • 같은 규칙이 API, 배치, 관리자 기능에 반복되는가
  • 팀이 어디까지가 정책이고 어디까지가 구현인지 자주 헷갈리는가
  • 외부 시스템 종류가 늘면서 서비스 클래스가 비대해지고 있는가

정리

헥사고날 아키텍처는 새로운 그림을 그리는 기술이 아닙니다. 핵심 규칙을 중심에 두고 바깥 기술과의 연결을 포트와 어댑터 뒤로 밀어 의존 방향을 관리하는 방법입니다. 이 구조가 특히 빛나는 순간은 핵심 규칙이 중요하고, 외부 시스템이 많고, 테스트 격리가 필요한 때입니다. 반대로 단순 CRUD 위주 서비스에서는 쉽게 과해질 수 있습니다.

그래서 중요한 질문은 이것입니다. 지금 내 시스템에서 정말 보호해야 할 핵심 규칙이 있는가, 그리고 그 규칙이 바깥 기술 변화에 흔들리고 있는가? 이 질문에 그렇다고 답하게 된다면, 헥사고날 아키텍처는 꽤 실용적인 선택지가 됩니다. 같이 읽으면 좋은 글로는 클린 아키텍처: 무엇을 남길까, 인터페이스는 왜 필요할까, SOLID 원칙, 실무에서는 어떻게 봐야 할까를 추천합니다. 외부 참고 자료로는 Martin Fowler의 Presentation Domain Data Layering, Uncle Bob의 Clean Architecture 글, Microsoft Learn의 N-tier Architecture를 함께 보면 맥락을 더 넓게 잡는 데 도움이 됩니다.

함께보면 좋은 글