Hexagonal Architecture

호호빵·2026년 2월 20일

Clean Architecture

  • 핵심 규칙은 도메인 코드가 바깥으로 향하는 어떤 의존성도 없어야 한다는 것





Layered Architecture

  • 같은 목적의 코드들을 같은 계층으로 그룹화한 것으로, 역할과 관심사를 계층으로 분리
  • 구조: Presentation(UI) → Business(Service) → Data Access(DB)
  • 상위 계층이 하위 계층에 의존하며, 데이터가 위에서 아래로 흐름
  • 구현이 빠르고 단순하지만, 비즈니스 로직이 특정 DB 기술(JPA 등)에 강하게 결합되기 쉬움



Hexagonal Architecture(ports-and-adapters)

The idea of Hexagonal Architecture is to put inputs and outputs at the edges of our design. Business logic should not depend on whether we expose a REST or a GraphQL API, and it should not depend on where we get data from — a database, a microservice API exposed via gRPC or REST, or just a simple CSV file.

-Netflix Technology Blog

  • 비즈니스 로직을 설계의 중심으로 하면서,
    노출시키는 영역(outputs)과 데이터를 가져오는 영역(inputs)에 의존하지 않는 디자인을 의미
  • 의존성 역전을 통해 비즈니스 로직이 외부 기술로부터 완전히 독립됨
  • 구조: 핵심 Domain(중심), Usecase ↔ Port(인터페이스) ↔ Adapter(구현체)
  • 장점
    • 테스트 용이성: DB나 외부 API 없이도 비즈니스 로직만 따로 떼어 모킹(Mocking) 테스트하기 매우 좋음
    • 기술 독립성: Java 버전을 올리거나, DB를 MySQL에서 MongoDB로 바꿔도 핵심 로직은 건드릴 필요가 없음
    • 유지보수: 각 기능이 포트와 어댑터로 명확히 분리되어 있어 영향 범위 파악이 쉬움

process

1. Inside: 애플리케이션 핵심 (Core)

  • 가장 안쪽에는 Domain Entities와 Use Cases가 존재
  • Domain: 비즈니스 규칙의 정수. Java의 순수 객체(POJO)로 작성되며 외부 라이브러리에 의존하지 않음
  • Use Case: 애플리케이션의 핵심 비즈니스 로직을 수행하는 최소 단위의 실행 흐름을 의미. 입력 포트를 구현하여 비즈니스 흐름을 제어

2. Boundary: 포트 (Ports)

  • 핵심 로직과 외부 세계를 연결하는 인터페이스
  • Input Port (Inbound): 외부에서 내부로 들어오는 요청을 위한 명세 (예: RegisterUserUseCase)
  • Output Port (Outbound): 내부에서 외부로 나가는 요청을 위한 명세 (예: LoadUserPort, SaveUserPort)

3. Outside: 어댑터 (Adapters)

  • 포트를 통해 실제로 외부와 통신하는 구현체
  • Driving Adapter (Inbound): 사용자의 요청을 변환하여 입력 포트를 호출 (예: UserController, MessageQueueListener)
  • Driven Adapter (Outbound): 출력 포트를 구현하여 실제 인프라와 통신 (예: UserJpaAdapter, ExternalMailAdapter)


구현 순서 및 코드 예시

  • 중심부부터 바깥쪽으로 작성하는 것이 원칙
  1. Domain: 순수 비즈니스 객체
  2. Usecase (Port): 비즈니스 실행 단위 인터페이스
  3. Adapter (Outbound): DB 저장 등 외부 구현체
  4. Adapter (Inbound): 컨트롤러 등 외부 유입 지점

코드 예시

// 1. Domain (순수 java 객체)
public class User {
    private Long id;
    private String name;

    public User(String name) {
        this.name = name;
    }
    // Getter, 비즈니스 로직 등...
}

// 2. Usecase & Port (interface)
// Inbound Port: 외부(Controller)에서 내부로 들어오는 관문
public interface RegisterUserUseCase {
    void register(String name);
}

// Outbound Port: 내부에서 외부(DB)로 나가는 관문
public interface SaveUserPort {
    void save(User user);
}

// Usecase Imolementaion (service)
@Service
@RequiredArgsConstructor
public class RegisterUserService implements RegisterUserUseCase {
    
    private final SaveUserPort saveUserPort; // Outbound Port 주입

    @Override
    public void register(String name) {
        User user = new User(name);
        // 비즈니스 검증 로직 등이 위치함
        saveUserPort.save(user);
    }
}

// Adapters
// 3. Outbound Adapter (Persistence)'
@Component
@RequiredArgsConstructor
public class UserPersistenceAdapter implements SaveUserPort {
    private final UserJpaRepository repository; // 실제 Spring Data JPA 사용

    @Override
    public void save(User user) {
        // Domain 객체를 Entity로 변환하여 저장
        repository.save(new UserEntity(user.getName()));
    }
}

// DB Entity
package com.example.adapter.out.persistence;

import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class UserJpaEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // JPA를 위한 기본 생성자 및 매핑용 메서드들...
    public User toDomain() { // DB 엔티티를 도메인 객체로 변환
        return new User(this.id, this.name);
    }
}

// 4. Inbound Adapter (web)
@RestController
@RequiredArgsConstructor
public class UserController {
    private final RegisterUserUseCase registerUserUseCase; // Port 호출

    @PostMapping("/users")
    public void register(@RequestParam String name) {
        registerUserUseCase.register(name);
    }
}

전체 경로

src
└── main
    └── java
        └── com.example
            ├── domain (핵심 비즈니스)
            │   └── User.java                          <-- [순수 Domain Entity]
            │
            ├── application (비즈니스 로직 조립)
            │   ├── port
            │   │   ├── in
            │   │   │   └── RegisterUserUseCase.java   <-- [Interface]
            │   │   └── out
            │   │       └── SaveUserPort.java          <-- [Interface]
            │   └── service
            │       └── RegisterUserService.java       <-- [UseCase 구현체]
            │
            └── adapter (외부 기술 연결)
                ├── in 
                │   └── web
                │       └── UserController.java        <-- [Spring MVC Controller]
                └── out
                    └── persistence
                        ├── UserJpaEntity.java         <-- [DB Entity (JPA)]
                        ├── UserJpaRepository.java     <-- [Spring Data JPA]
                        └── UserPersistenceAdapter.java <-- [Port 구현체]

// 간략
src
└── main
    └── java
        └── com.example
			├── domain
            ├── application
            │   ├── port
            │   │   ├── in            <-- UseCase 인터페이스 위치
            │   │   └── out           <-- Persistence Port 인터페이스 위치
            │   └── service           <-- [UseCase Impl]
            └── adapter
                ├── in
                │   └── web           <-- Controller 위치
                └── out
                    └── persistence   <-- DB Entity, JPA Repository 위치               


reference

https://gngsn.tistory.com/258#google_vignette

profile
하루에 한 개념씩

0개의 댓글