Architecture

헥사고날 아키텍처

공부좀하시졍 2025. 10. 29. 16:57

1️⃣ 계층형 아키텍처의 한계

Controller -> Service -> Repository -> DB

이 구조를 계층형(Layered) 아키텍처 라고 한다. 이 구조의 단점은 다음과 같다.

  • 변경 전파 : DB나 외부 API가 바뀌면 Service 코드까지 수정해야 한다.
  • 결합도 증가 : 비즈니스 로직이 JPA, Mybatis 같은 기술 세부사항에 묶인다.
  • 테스트 어려움 : DB 연결 없이 Service 로직을 테스트하기 어렵다.

이 구조의 의존성 방향은 "바깥 👉 안쪽" 이다. 즉, Controller가 Service를 사용하고 Service가 Repository를 사용한다.

👉 👉 도메인 로직이 외부 기술에 의존하는 구조

 

2️⃣ 헥사고날 아키텍처

DB나 API, 웹 UI 같은 기술이 도메인 로직을 몰라야 한다

 

[ Controller / Web / MQ ]  →  (Input Adapter)
             ↓
        [ Input Port ]      ← 인터페이스
             ↓
[ Application / UseCase(Service) ]   ← 핵심 비즈니스 로직
             ↓
        [ Output Port ]     ← 인터페이스
             ↓
[ DB / API / File / etc. ]  ← (Output Adapter)
  • Service (UseCase) : 도메인 로직을 실행하는 곳
  • Port : Service와 외부를 연결하기 위한 통로 (인터페이스)
  • Adapter : Port의 구현체 (ex. DB 접근)
  • Domain : 비즈니스의 규칙을 담는 모델

👀 Service는 Adapter를 모르고 Adapter가 Service도 모른다 >> 의존성 역전

 

3️⃣ 의존성 방향이 반대 ?

  • 계층형 구조
    • Service --> Repository (Service가 Repository를 직접 호출) 👉 Repository에 직접 의존
  • 헥사고날 구조
    • Service --> OutputPort <-- Adapter
    • Service는 OutputPort  인터페이스만 알고 있고 실제 DB 접근 로직은 Adapter가 구현
    • InputPort : 외부 --> 도메인(외부 요청을 받는 인터페이스)
    • OutputPort : 도메인 --> 외부 (외부 시스템(DB, API)에 접근하는 인터페이스)

4️⃣ 스프링에서의 헥사고날

IoC (제어의 역전) 과 DI (의존성 주입) 으로 헥사고날 구현이 쉬어진다.

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

@Repository
public class OrderRepositoryAdapter implements OrderRepositoryPort {
    private final OrderRepository orderRepository;

    @Override
    public void save(Order order) {
        orderRepository.save(order);
    }
}

@Service
public class OrderService implements OrderUseCase {
    private final OrderRepositoryPort orderRepositoryPort;

    // 생성자 주입
    public OrderService(OrderRepositoryPort orderRepositoryPort) {
        this.orderRepositoryPort = orderRepositoryPort;
    }
}

스프링이 OrderRepositoryPort 타입을 찾아 OrderRepositoryAdapter를 자동 주입해줘 헥사고날의 구조적 원리를 살릴 수 있다.

 

  • @Service, @Repository, @Component 등으로 등록된 빈을 스캔함
  • OrderSerivce가 생성될 때 생성자 파라미터인 OrderRepositoryPort 타입을 보고 이 타입을 구현한 빈 (OrderREpositoryAdapter)를 찾아 자동으로 그 객체를 주입함

 

5️⃣ 폴더 구조 예시

gpt의 도움 .. 👀

📦 com.example.order
 ┣ 📂 domain
 ┃ ┣ 📜 Order.java
 ┃ ┣ 📜 OrderItem.java
 ┃ ┣ 📜 Money.java
 ┃ ┗ 📜 DomainService.java
 ┣ 📂 application
 ┃ ┣ 📂 port
 ┃ ┃ ┣ 📂 in
 ┃ ┃ ┃ ┗ 📜 PlaceOrderUseCase.java
 ┃ ┃ ┗ 📂 out
 ┃ ┃    ┗ 📜 OrderRepositoryPort.java
 ┃ ┗ 📂 service
 ┃     ┗ 📜 PlaceOrderService.java
 ┣ 📂 adapter
 ┃ ┣ 📂 in
 ┃ ┃ ┗ 📂 web
 ┃ ┃     ┣ 📜 OrderController.java
 ┃ ┃     ┗ 📂 dto
 ┃ ┃         ┣ 📜 PlaceOrderRequest.java
 ┃ ┃         ┗ 📜 OrderResponse.java
 ┃ ┗ 📂 out
 ┃     ┗ 📂 persistence
 ┃         ┗ 📜 JpaOrderRepositoryAdapter.java
 ┗ 📂 config
     ┗ 📜 BeanConfig.java