취미겸생업

[Architecture] Hexagonal Architecture 본문

IT/Architecture

[Architecture] Hexagonal Architecture

nohumb 2026. 6. 9. 14:44

Layered Architecture의 한계

Clean 아키텍처와 Hexagonal 아키텍처를 설명하기에 앞서, 아키텍처의 선택은 정답이 없음을 확실히 알고 시작해야 한다. 각 아키텍처마다 생산성과 안정성, 확장성 등의 요소에 따른 장단점이 존재한다. 때문에 소규모 프로젝트 개발에는 복잡한 규칙의 아키텍처보다 직관적이고 단순한 Layered Architecture가 더 유리할 수도 있다.

기존에 사용되던 Layered Architecture의 대략적인 특성과 한계점을 알아보자.

한계

Layered 아키텍처

기존의 Layered Architecture는 위에서 아래로(Controller → Service → Repository → Datasource) 상하관계가 결정된다. 이 구조의 한계점들은 아래와 같다.

  • DB 종속 : Repository와 Datasource가 가장 아래에 존재하기 때문에 그 위의 계층들은 DB에 종속적이다
    • Database 구조 변동에 상위 계층이 모두 영향을 받는다
    • ORM, SQL 프레임워크 등을 교체할 경우 상위 계층이 큰 영향을 받는다 (JPA는 Dirty checking, MyBatis는 직접 Query)
  • 계층 간 경계 모호 : 계층간 상하관계 외에는 엄격한 규칙이 따로 없기 때문에 계층간 경계가 흐릿해진다
    • Controller의 DTO 수정이 Service에 영향을 줄 수 있다
    • Controller에서 직접 Repository를 참조하는 것을 구조적으로 막을 수 없다
  • Fat Service : Service가 비대해지거나 순환참조 하는 것에 대한 제약이 없다 (수평적 오염)
    • 하나의 Service가 특정 도메인의 모든 비즈니스 로직을 가질 수 있다 (SRP 위배, 결합도 상승)
    • 서로 다른 도메인 Service 끼리 참조하는 것에 제약이 없다 (SRP 위배, 결합도 상승, 순환 참조 위험)

Hexagonal Architecture

도메인 중심 (Domain-Centric)

상술한 Layered Architecture의 한계를 타파하기 위한 도메인 중심 사상으로부터 Hexagonal, Clean, Onion 등의 Architecture들이 고안되어 그 뿌리를 같이 한다.

도메인 중심 아키텍처

이들은 기본적으로 도메인을 가장 내부로 감춘다. 그리고 Database, 외부 클라이언트, Framework 등의 Infra 기술은 가장 외부에 위치한다. 이를 통해 외부로부터 비즈니스 로직을 보호하여 도메인을 순수하게 유지할 수 있게 된다.

(데이터 흐름이 아닌 의존성에 대한 방향을 집합으로 표현한 것임에 주의한다)

Ports and Adapters

위의 도메인 중심 아키텍처들의 가장 중요한 점은 계층간 의존성 방향인데, 내부의 계층은 외부의 계층에 대해 몰라야 한다. 다시 말해, 의존하지 않는 계층에 대한 직접적인 참조(Import)를 할 수 없어야 한다.

  • Port-Adapter

Port-Adapter

이를 실현하기 위해 Interface(Port)를 통해 다른 계층을 참조하고, 참조되는 계층은 구현체를 구현하여 논리적으로 반대편 계층의 세부 구현을 알 수 없도록 제한한다. 이 과정에서 interface의 구현체가 Spring DI를 통해 주입된다.

  • 예제 의사(Pseudo) 코드
/* In-Bound Port
    방향 : Controller(외부) -> Domain(내부)
    의존성 : 외부 계층이 내부 인터페이스를 참조 (내부 구현은 숨김)
*/
public interface OrderInboundPort {
    void placeOrder(OrderCommand command);
}

/* In-Bound Port 구현체
    역할 : 비즈니스 로직을 수행하는 내부 도메인 영역의 본체
*/
class OrderInboundPortImpl implements OrderInboundPort {
    private final OrderStorageOutboundPort orderStoragePort;

    public OrderInboundPortImpl(OrderStorageOutboundPort orderStoragePort) {
        this.orderStoragePort = orderStoragePort;
    }

    @Override
    public void placeOrder(OrderCommand command){
        // ...
        orderStoragePort.save(order);
    }
}

/* Out-Bound Port
    방향 : Domain(내부) -> Database(외부)
    의존성 : 외부 인프라 계층이 내부 인터페이스를 구현 (DIP)
*/
public interface OrderStorageOutboundPort {
    void save(Order order);
}

외부에서 내부(In-Bound), 내부에서 외부(Out-Bound) 두 방향의 포트가 존재할 수 있다. 이는 차후 설명하게 될 Hexagonal, Clean Architecture의 핵심 개념이 된다.
다만, 하나의 큰 도메인 단위로 포트를 만들면 기존 Layered 구조처럼 'Fat Service'가 되는 한계가 발생하므로, 이를 방지하기 위해 행위(UseCase) 단위로 Port를 정밀하게 쪼개며 진화하게 된다.

Hexagonal Architecture

Hexagonal 아키텍처

Hexagonal Architecture는 간단히 요약하면 Domain을 중심으로 하여 나머지 모든 영역은 외부로 간주하는 것이다.

즉, 비즈니스 로직(Domain)만이 내부이며 중심이라는 것 외엔 별다른 규칙은 없다고 볼 수 있다.
상술했던 도메인 중심 아키텍처의 특성을 그대로 따르며, 외부와 port, adapter를 통해 상호작용한다.

장점

  • 기술 종속 탈피 : DB, 프레임워크, 3rd-Party와 같은 특정 기술에 종속되는 Infrastructure에 대한 종속성이 사라진다. 다시 말해, MySQL 또는 MongoDB와 같은 데이터베이스 기술이나 JPA, MyBatis 등 ORM 프레임워크 등 외부 기술을 교체하더라도 비즈니스 로직에 전혀 영향이 없다.
  • 테스트 용이 : 비즈니스 로직이 특정 기술에 의존하지 않기 때문에 순수한 Unit Test를 단순한 형태로 유지할 수 있다.

UseCase

Domain 계층은 외부의 Infrastructure 의존이 없이 순수해야 한다는 것 외에는 별다른 조건이 없다.
다만, 도메인 단위의 통짜 포트를 만들면 과거 Layered 구조처럼 클래스가 비대해지는 'Fat Service' 문제가 발생한다.

따라서 현대적인 Hexagonal Architecture는 이를 방지하기 위해 In-Bound Port를 사용자의 행위(UseCase) 단위로 잘게 쪼개어 단일 책임 원칙(SRP)을 준수하는 것이 강력한 관습으로 자리 잡았다.

(UseCase와 Interactor 개념은 Clean Architecture로 부터 차용된 개념)

예제

// In-Bound Port (UseCase)
public interface PlaceOrderUseCase {
    void execute(OrderCommand command);
}

// In-Bound Port 구현체 (Interactor)
class PlaceOrderInteractor implements PlaceOrderUseCase {
    private final OrderStorageOutboundPort orderStoragePort;

    public PlaceOrderInteractor(OrderStorageOutboundPort orderStoragePort) {
        this.orderStoragePort = orderStoragePort;
    }

    @Override
    public void execute(OrderCommand command){
        // ...
        orderStoragePort.save(order);
    }
}

// Out-Bound Port
public interface OrderStorageOutboundPort {
    void save(Order order);
}

멀티 모듈 구조

멀티 모듈로 Hexagonal Architecture를 구성할 경우, 외부 의존성이 최소화 되어 순수한 비즈니스 로직을 갖는 domain 또는 core 모듈을 다른 모든 모듈들이 의존하는 형태를 띌 수 있다.

  • api(controller), storage(db), domain(비즈니스 로직) - 하나의 큰 Domain 모듈
  • api(controller), storage(db), usecase(비즈니스 로직), domain(entities) - UseCase, Entities 계층이 추가된 형태