Ports and Adapter 패턴 (Hexagonal)

2025. 2. 17. 20:04·Spring

1. 개요 

Ports & Adapter 아키텍처란?

: 비즈니스 로직을 외부 시스템(웹, DB, 메시지 큐 등)과 독립적으로 유지하기 위한 아키텍처 패턴

핵심은 어플리케이션의 중심에 핵심 비즈니스 로직을 두고, 외부 시스템과의 연결을 유연하게 한다는 것

 

왜 Ports & Adapter 아키텍처를 사용하나?

: 전통적인 레이어드 아키텍처(Presentation - Domain - Persistence)의 문제점을 해결

레이어드 아키텍처의 의존성 방향은 Controller -> Service -> Repository -> DataBase

이 구조에서 Service는 Repository에 강하게 의존하게 된다.

즉, Service는 특정 구현체(JPA, JDBC, MongoDB 등)와 직접적으로 결합되어버린다.

 

🔴 문제점

  • 만약 JPA에서 MongoDB로 변경하려면?
    -> Service 내부 코드를 변경해야 함
  • 테스트하려고 하는데 DB가 필요하다면?
    -> Repository를 Mock으로 대체하기 어려움
  • 비즈니스 로직(Service)이 영속성 기술을 알아야 함
    -> Repository가 특정 ORM/JDBC 기술에 종속되면서 유지보수가 어려워짐 

🟢 Ports & Adapter 적용

  • 비즈니스 로직과 외부 기술을 분리 → DB, API, UI 변경에도 핵심 로직 영향 없음
  • 테스트가 용이함 → 가짜 Out-Port 구현(Mock Repository)으로 유닛 테스트 가능
  • 확장성이 높음 → JPA 대신 NoSQL, API 호출로 쉽게 변경 가능

2. 주요 개념

✅ In-Port & Out-Port (포트)

  • In-Port(입력 포트)
    - 애플리케이션 핵심 로직을 호출하기 위한 인터페이스
    - Controller, CLI,  메시지 큐 등이 In-Port를 호출
  • Out-Port(출력 포트)
    - 애플리케이션이 외부 시스템(DB, API 등)에 접근할 때 사용하는 인터페이스
    - Out-Port는 실제 구현을 모르고, 단지 "이런 동작이 필요하다"는 인터페이스만 정의(In-Port도 동일)
    - Repository, API Client 등이 Out-Port의 구현체가 됨 

✅ Adapter (어댑터)

  • Inbound Adapter(입력 어댑터)
    사용자의 요청을 In-Port로 전달하는 역할 즉, In-Port의 구현체
    ex) Controller(Spring @RestController), CLI, 이벤트 리스너 등
  • Outbound Adapter(출력 어댑터)
    Out-Port를 구현해서 실제 외부 시스템과 통신하는 역할, Out-Port의 구현체
    ex) JPA Repsitory, API 호출용 HTTP Client 등 

3. 예제 코드로 이해

💡 "사용자가 회원 정보를 조회"하는 기능을 Ports & Adapter 아키텍처로 구현

 

3.1) In-Port 정의(어플리케이션이 제공하는 기능을 정의하는 인터페이스)

public interface GetUserUseCase {
	UserResponse getUserById(Long userId);
}
  • GetUserUseCase는 어플리케이션에서 제공하는 "회원 조회"기능을 정의한 인터페이스이다.
  • Controller 입장에서는 이 인터페이스를 호출하고, 내부 로직은 몰라도 된다.

 

3.2) Out-Port 정의(외부 시스템과의 연결을 위한 인터페이스)

public interface UserRepository {
	Optional<User> findById(Long userId);
}
  • UserRepository는 DB에서 사용자 정보를 가져오는 역할이다.
  • 실체 구현은 모르고, 단순히 findById를 호출하면 유저 데이터를 가져올 거야 라고만 인지한다.

 

이제 인터페이스를 구현했으니 실제 서비스를 구현해보자.

 

3.3) In-Port 구현 (Service)

@Service
public class GetUserService implements GetUserUseCase {
    private final UserRepository userRepository;

    public GetUserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserResponse getUserById(Long userId) {
        User user = userRepository.findById(userId)
                                  .orElseThrow(() -> new UserNotFoundException(userId));
        return new UserResponse(user);
    }
}

 

  • GetUserService는 In-Port(GetUserUseCase)의 구현체
  • UserRepository(Out-Port)를 사용하여 데이터를 가져온다.
  • 핵심 비즈니스 로직이 이 서비스에 구현된다.

 

3.4) Out-Port 구현 (JPA Adapter)

@Repository
public class UserJpaRepositoryAdapter implements UserRepository {
    private final JpaUserRepository jpaRepository;

    public UserJpaRepositoryAdapter(JpaUserRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public Optional<User> findById(Long userId) {
        return jpaRepository.findById(userId);
    }
}
  • UserRepository(Out-Port)를 실제 JPA Repository로 연결
  • 어플리케이션 핵심 로직은 JPA의 존재를 몰라도 된다.

 

3.5) Inbound Adapter (Controller - 외부 요청을 받아 In-Port 호출)

@RestController
@RequestMapping("/users")
public class UserController {
    private final GetUserUseCase getUserUseCase;

    public UserController(GetUserUseCase getUserUseCase) {
        this.getUserUseCase = getUserUseCase;
    }

    @GetMapping("/{userId}")
    public ResponseEntity<UserResponse> getUser(@PathVariable Long userId) {
        return ResponseEntity.ok(getUserUseCase.getUserById(userId));
    }
}
  • UserController는 Inbound Adapter이다.
  • GetUserUseCase를 호출해서 비즈니스 로직을 수행하게 된다.
  • 실제 DB에 어떻게 접근하는지는 신경쓰지 않는다.
Outbound Adapter는 Out-Port를 구현하여 실제 외부 시스템과 연결하는 것으로 예) UserJpaRepsitoryAdapter를 의미

전체 구조 정리

┌────────────────────────────────┐
│        Web / API (Controller)  │  →  Inbound Adapter
└────────────────────────────────┘
                 │
                 ▼
┌────────────────────────────────┐
│  Application (UseCase, Service)│  →  In-Port 구현체
└────────────────────────────────┘
                 │
                 ▼
┌────────────────────────────────┐
│     Persistence (Repository)   │  →  Out-Port 인터페이스
└────────────────────────────────┘
                 │
                 ▼
┌────────────────────────────────┐
│        Database (JPA, SQL)      │  →  Outbound Adapter
└────────────────────────────────┘

 


그러나 단점,

- 코드량 증가 (더 많은 클래스 & 인터페이스)

- 구현의 복잡성 증가 -> 기존에는 userRepository.findById(id) 이렇게 바로 호출하면 됐는데,

                                           이제는 Port를 통해 Adapter를 거쳐야 하는 구조가 됨.

'Spring' 카테고리의 다른 글

Spring Security + JWT  (1) 2025.02.21
@RequestPart 알아보기  (0) 2025.02.12
열어봐요 @RequestBody로 데이터 받는 과정  (0) 2025.02.10
@RequestBody 와 <form> 태그  (0) 2025.02.05
Spring Pageable 안 쓰면 바보  (1) 2025.01.31
'Spring' 카테고리의 다른 글
  • Spring Security + JWT
  • @RequestPart 알아보기
  • 열어봐요 @RequestBody로 데이터 받는 과정
  • @RequestBody 와 <form> 태그
힐안
힐안
나 지금 학비 내면서 개발 독학하잖아.
  • 힐안
    후라이
    힐안
  • 전체
    오늘
    어제
    • 분류 전체보기 (59)
      • HTTP (9)
      • Spring (28)
        • 웹 게시판 (4)
      • Neural Network (1)
        • CNN (1)
        • RNN (0)
      • Deep Learning (6)
      • Audio (3)
      • 알고리즘(JAVA) (2)
      • 백준 (10)
        • Bronze (0)
        • Silver (2)
        • Gold (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    딥러닝
    스프링부트
    Spring
    MVC
    플로이드와샬
    springboot
    오블완
    HTTP
    다익스트라
    JSON
    Genrative_model
    웹
    백엔드
    백준
    web
    비지도학습
    MNIST
    프레임워크
    최단경로알고리즘
    dcgan
    GAN
    벨만포드
    API
    티스토리챌린지
    자바
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
힐안
Ports and Adapter 패턴 (Hexagonal)
상단으로

티스토리툴바