기술과 산업/아키텍처

소프트웨어 아키텍처 시리즈 8화 – 헥사고날 아키텍처의 입출력 포트와 어댑터 설계 전략

B컷개발자 2025. 6. 5. 12:23
728x90

헥사고날 아키텍처의 핵심은 ‘포트와 어댑터’입니다. 이 글에서는 입출력 포트를 어떻게 정의하고 어댑터를 어떻게 구현해야 아키텍처의 의도를 온전히 살릴 수 있는지를 실전 중심으로 설명합니다.

 

 

왜 ‘포트와 어댑터’인가?

 

헥사고날 아키텍처에서 가장 핵심이 되는 구조는 다음과 같습니다.

 

  • Port(포트): 도메인 영역과 외부 환경 간의 경계 인터페이스
  • Adapter(어댑터): 외부 시스템을 포트에 맞게 연결하는 구현체

 

이 구조를 통해 우리는 도메인은 외부 기술을 전혀 알지 않고도 동작 가능하게 만들 수 있습니다.

 

포트는 계약(Contract)이고, 어댑터는 그것의 구체적 실행입니다. 즉,

 

도메인은 “이런 기능이 필요해요”라고 말하고,
어댑터는 “그 기능, 제가 담당하겠습니다”라고 응답하는 구조입니다.

 


 

입출력 포트 구분의 개념적 정의

 

헥사고날 아키텍처에서는 포트를 크게 두 가지로 구분합니다.

 

 

1. Inbound Port (입력 포트)

 

  • 도메인/애플리케이션을 외부에서 호출할 수 있도록 만든 인터페이스
  • 유스케이스별로 정의됨
  • 일반적으로 Service Interface나 UseCase 인터페이스로 표현

 

예시:

public interface PlaceOrderUseCase {
    void placeOrder(OrderRequestDto request);
}

 

2. Outbound Port (출력 포트)

 

  • 도메인 로직이 외부 시스템을 필요로 할 때 호출하는 인터페이스
  • 기술 세부사항(DB, Kafka, 외부 API 등)은 모름

 

예시:

public interface PaymentGateway {
    boolean requestPayment(PaymentInfo info);
}

이렇게 경계를 정의하면 도메인은 외부와 철저히 분리되고, 테스트 가능한 순수 로직으로 유지됩니다.

 


 

어댑터(Adapter) 설계 전략

 

어댑터는 포트를 구현합니다. 따라서 다음 두 가지 형태로 나뉩니다.

 

 

1. Inbound Adapter (입력 어댑터)

 

  • HTTP Controller, GraphQL Resolver, CLI Command 등
  • Inbound Port를 호출하는 역할

 

예:

@RestController
public class OrderController {
    private final PlaceOrderUseCase placeOrderUseCase;
    // POST /orders
    public void placeOrder(@RequestBody OrderRequestDto dto) {
        placeOrderUseCase.placeOrder(dto);
    }
}

 

2. Outbound Adapter (출력 어댑터)

 

  • JPA Repository, Kafka Producer, Redis Client 등
  • Outbound Port의 구현체

 

예:

@Component
public class PaymentGatewayImpl implements PaymentGateway {
    public boolean requestPayment(PaymentInfo info) {
        // 외부 API 호출 로직
    }
}

이렇게 하면 모든 외부 의존성은 어댑터에만 머무르고, 포트에는 도메인 로직과 연결된 인터페이스만 남게 됩니다.

 


 

구현 시 고려사항: ‘경계’의 모호함

 

실제 프로젝트에서는 다음과 같은 질문들이 자주 발생합니다.

 

  • 유스케이스 단위로 포트를 나눠야 하나?
  • DTO는 포트에 포함되나, 도메인 객체만 써야 하나?
  • 어댑터가 너무 많아지면 관리가 복잡하지 않나?

 

정답은 없습니다. 하지만 명확한 기준을 두고 설계하면 해결 가능합니다.

 

 

실무 권장 기준

항목권장 기준

유스케이스별 포트 단일 책임 원칙에 따라 분리
DTO 사용 여부 Application 계층까지만 허용, Domain에서는 Value Object 중심
어댑터 관리 기술 영역별 디렉토리 구분 (ex. adapter/web, adapter/jpa 등)
중요한 건 ‘정해진 규칙’이 아니라, 팀이 공유할 수 있는 설계 기준을 명확히 세우고 일관성 있게 적용하는 것입니다.

 


 

포트와 어댑터가 주는 실전적 가치

 

 

1. 테스트 가능성(Testability)

 

  • 모든 유스케이스는 Port 인터페이스로 테스트 가능
  • 외부 어댑터를 Mock으로 대체하여 빠르고 안정적인 테스트 작성 가능

 

 

2. 기술 변경 유연성

 

  • Kafka → RabbitMQ로 변경해도 Outbound Adapter만 바꾸면 됨
  • REST API → gRPC 전환 시 Inbound Adapter만 수정

 

 

3. 도메인 중심 설계(Domain-centric)

 

  • 비즈니스 로직은 언제나 포트 안쪽에서만 존재함
  • 도메인은 외부 기술에 대한 단 한 줄의 코드도 없어야 한다

 


 

오해와 반론에 대한 솔직한 입장

 

 

오해: “포트랑 인터페이스는 그냥 분리된 서비스랑 다를 게 없잖아?”

 

반쯤 맞고 반쯤 틀립니다.
기존 계층 아키텍처에서도 인터페이스 분리는 가능합니다. 하지만 포트/어댑터 구조는 단순 인터페이스 분리를 넘어 의존성 방향성과 경계 유지라는 목적을 명확히 가집니다.

 

 

오해: “어댑터를 너무 세분화하면 복잡도만 늘지 않나?”

 

네, 무분별하게 나누면 ‘설계과잉’입니다.
하지만 팀 규모가 커지고 기술 스택이 늘어날수록, 어댑터 단위 분리는 유지보수에서 결정적 차이를 만듭니다.

 


 

실전 예제 구조 예시 (디렉토리 기준)

src
 ┣ domain
 ┃ ┗ order
 ┃    ┣ Order.java
 ┃    ┣ OrderValidator.java
 ┣ application
 ┃ ┗ order
 ┃    ┣ PlaceOrderUseCase.java  // Inbound Port
 ┃    ┣ PlaceOrderService.java  // UseCase 구현
 ┃    ┣ PaymentGateway.java     // Outbound Port
 ┣ adapter
 ┃ ┣ web
 ┃ ┃ ┗ OrderController.java     // Inbound Adapter
 ┃ ┣ payment
 ┃ ┃ ┗ PaymentGatewayImpl.java  // Outbound Adapter

 


 

마무리하며 – 포트는 ‘관점’이고 어댑터는 ‘전략’이다

 

포트와 어댑터를 단지 구조적으로만 보면, 그냥 인터페이스와 구현체의 다른 말처럼 보일 수 있습니다.

하지만 그것은 어디에 책임을 둘 것인지, 도메인을 얼마나 보호할 것인지,

그리고 기술 변화에 얼마나 강인한 구조를 만들 것인지에 대한 깊은 전략입니다.

 

헥사고날 아키텍처는 단순한 기술 구조가 아니라, 개발자가 시스템을 바라보는 방식 자체를 바꾸는 도구입니다.

728x90