Java 아키텍처·Spring 용어 사전
아키텍처포트와 어댑터 · 클린 아키텍처 · 의존성 역전

헥사고날 아키텍처

도메인을 중심에 두고 외부를 어댑터로 밀어내는 구조. 의존성이 항상 안쪽을 향하게 만든다.

도메인을 중심에 두고 DB·웹·메시징을 바깥 어댑터로 밀어내는 구조. 포트와 어댑터 아키텍처라고도 한다.

레이어드의 한계에서 출발한다

  • [레이어드] — Controller → Service → Repository → DB
    • 의존이 아래로 흐른다
    • 도메인(Service)이 DB(Repository 구현)에 의존한다
    • DB 를 바꾸면 도메인이 흔들린다. 테스트에 DB 가 필요하다

의존성을 뒤집는다

            [ 도메인 ]           ← 아무것도 의존하지 않는다
                ↑ 구현
     ┌──────────┴──────────┐
  웹 어댑터           DB 어댑터
  (Controller)      (JpaRepository 구현)
// 도메인이 '필요한 것' 을 인터페이스로 선언한다 (포트)
package com.example.order.domain;
public interface OrderRepository {          // 도메인 소유
    Order save(Order order);
}

// 바깥이 그 인터페이스를 구현한다 (어댑터)
package com.example.order.adapter.persistence;
@Repository
class JpaOrderRepository implements OrderRepository { ... }

인터페이스를 도메인 쪽에 두는 것이 핵심이다. 그러면 화살표가 안쪽을 향한다.

무엇을 얻나

  • 도메인 테스트에 Spring 도 DB 도 필요 없다 (순수 JUnit)
  • DB·메시징을 바꿔도 도메인 코드가 안 바뀐다
  • 도메인이 프레임워크 애노테이션에서 자유롭다

무엇을 잃나

  • 클래스와 매핑 코드가 는다 (도메인 Order ↔ 엔티티 OrderEntity)
  • 작은 CRUD 서비스에는 과하다
  • 팀이 규칙을 지켜야 유지된다 (강제할 수단이 약하다)
// ArchUnit 으로 규칙을 테스트로 강제할 수 있다
@Test void domain_should_not_depend_on_adapter() {
    noClasses().that().resideInAPackage("..domain..")
        .should().dependOnClassesThat().resideInAPackage("..adapter..")
        .check(new ClassFileImporter().importPackages("com.example"));
}

어디까지 나눌 것인가

  • 작은 CRUD — 레이어드로 충분하다
  • 도메인 규칙이 복잡 — 헥사고날이 값을 한다
  • 팀이 여럿·배포 분리 — 모듈러 모놀리스 → 필요하면 그다음이 MSA

"처음부터 MSA"가 아니라 모듈러 모놀리스로 경계를 먼저 잡는 것이 현재의 일반적 권장이다. 경계가 틀렸을 때 되돌리는 비용이 훨씬 싸다.

면접 함정

  • "헥사고날이 레이어드보다 항상 낫다" → 복잡도 비용이 있다. 도메인이 얇으면 과설계다.
  • "인터페이스만 만들면 헥사고날" → 인터페이스가 도메인 쪽에 있어야 의존이 뒤집힌다.

매핑 비용을 어떻게 다루나

// 도메인 모델과 JPA 엔티티를 분리하면 매핑 코드가 는다
com.example.order.domain.Order              // 순수 자바. 애노테이션 없음
com.example.order.adapter.OrderEntity       // @Entity

// 어댑터가 변환을 책임진다
class JpaOrderRepository implements OrderRepository {
    public Order save(Order order) {
        return OrderEntity.from(order).toDomain();
    }
}
  • 분리하면 — 도메인이 JPA 에 안 묶인다 · 순수 단위 테스트가 가능하다

    • 대신 클래스와 매핑 코드가 두 배가 된다
  • 합치면 — 코드가 적다

    • 대신 @Entity 애노테이션·지연 로딩 제약이 도메인에 스며든다
  • 작은 팀·단순한 도메인이면 합치는 것도 합리적 선택이다

"항상 분리"가 아니라 도메인 복잡도에 비례해 정한다. 규칙이 거의 없는 CRUD에서 이중 모델은 순수한 비용이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 아키텍처 — 왜 계층으로 나누나