Notice
Recent Posts
Recent Comments
Link
후라이
Ports and Adapter 패턴 (Hexagonal) 본문
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 안 쓰면 바보 (0) | 2025.01.31 |