이번 프로젝트에서 헥사고날 아키텍처를 적용한다고 해서 블로그에 정리했습니다.
1. 헥사고날 아키텍처 개요
헥사고날 아키텍처(또는 포트와 어댑터 아키텍처)는 소프트웨어 시스템을 외부 시스템과 격리하여 비즈니스 로직을 중심으로 개발하도록 돕는 아키텍처 패턴입니다.
이 아키텍처는 외부 의존성을 관리하고 시스템을 더 쉽게 확장하고 테스트할 수 있도록 합니다.
핵심 아이디어는 애플리케이션의 내부와 외부 시스템 간의 상호작용을 포트(Ports) 와 어댑터(Adapters) 라는 추상화로 나누어 설계하는 것입니다. 외부 시스템은 어댑터를 통해 시스템과 상호작용하며, 포트는 애플리케이션 내부의 핵심 도메인 로직과 외부 시스템 간의 인터페이스 역할을 합니다.
2. 헥사고날 아키텍처의 구성 요소
- 핵심 도메인(Core Domain): 비즈니스 로직을 담당하는 부분입니다. 외부 시스템과의 상호작용을 최소화하고, 도메인 로직을 독립적으로 관리합니다.
- 포트(Ports): 외부 시스템과의 상호작용을 위한 인터페이스입니다. 예를 들어, REST API나 데이터베이스와의 연결 등이 있습니다.
- 어댑터(Adapters): 포트를 통해 외부 시스템과 연결하는 구현체입니다. 예를 들어, 데이터베이스에 데이터를 저장하는 어댑터나 외부 REST API를 호출하는 어댑터가 될 수 있습니다.
3. 스프링 프레임워크에서의 헥사고날 아키텍처 구현
이제 스프링 프레임워크를 사용하여 헥사고날 아키텍처를 어떻게 구현할 수 있는지에 대한 예시를 살펴보겠습니다. 아래는 스프링 부트를 사용하여 간단한 도서 관리 시스템을 구현하는 예제입니다.
/src
├── main
│ ├── java/com/example
│ │ ├── application # Use Case (서비스) 계층
│ │ │ ├── service # 유즈케이스 구현 (비즈니스 로직)
│ │ │ ├── dto # 데이터 전송 객체 (DTO)
│ │ ├── domain # 도메인 계층 (핵심 비즈니스 로직)
│ │ │ ├── model # 도메인 엔티티 및 VO
│ │ │ ├── repository # 도메인 인터페이스 (포트)
│ │ ├── infrastructure # 인프라 계층 (어댑터)
│ │ │ ├── persistence # JPA/Hibernate 등 구현체
│ │ │ ├── api # 외부 API 연동
│ │ │ ├── config # 환경 설정
│ │ ├── presentation # 어댑터 (REST API 등)
│ │ │ ├── controller # REST 컨트롤러
│ │ │ ├── mapper # DTO <-> 도메인 변환기
│ ├── resources
│ │ ├── application.yml # 설정 파일
├── test
│ ├── java/com/example # 테스트 코드
- Domain (핵심 비즈니스 로직)
- 도메인 모델을 정의하고 비즈니스 규칙을 포함합니다.
- 외부 의존성을 가지지 않습니다.
- repository 인터페이스를 정의하지만, 구현은 하지 않습니다.
- Application (Use Case 서비스 계층)
- 도메인 모델을 조작하는 유즈케이스(서비스)를 구현합니다.
- 인터페이스를 통해 도메인과 인프라 계층을 분리합니다.
- DTO를 활용해 데이터를 주고받습니다.
- Infrastructure (인프라 계층)
- 데이터베이스 연동(JPA, MyBatis), 메시지 브로커(Kafka, RabbitMQ) 및 API 호출 등을 담당합니다.
- repository 인터페이스를 실제 구현하는 JpaRepository 등이 위치합니다.
- Presentation (컨트롤러 계층)
- REST API, GraphQL, WebSocket 등 사용자와 상호작용하는 계층입니다.
- DTO와 도메인 객체 간 변환을 수행합니다.
1. 도메인 계층 (Domain)
package com.example.domain.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class Order {
private Long id;
private String product;
private int quantity;
public Order(Long id, String product, int quantity) {
this.id = id;
this.product = product;
this.quantity = quantity;
}
public void updateQuantity(int newQuantity) {
if (newQuantity <= 0) {
throw new IllegalArgumentException("Quantity must be greater than zero.");
}
this.quantity = newQuantity;
}
}
도메인 인터페이스 (포트)
package com.example.domain.repository;
import com.example.domain.model.Order;
import java.util.Optional;
public interface OrderRepository {
Optional<Order> findById(Long id);
Order save(Order order);
}
package com.example.application.service;
import com.example.domain.model.Order;
import com.example.domain.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public Order createOrder(Long id, String product, int quantity) {
Order order = new Order(id, product, quantity);
return orderRepository.save(order);
}
public Optional<Order> getOrderById(Long id) {
return orderRepository.findById(id);
}
}
3. 인프라 계층 (Infrastructure) - 도메인 인터페이스의 실제 구현체를 포함합니다.
package com.example.infrastructure.persistence;
import com.example.domain.model.Order;
import com.example.domain.repository.OrderRepository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface JpaOrderRepository extends JpaRepository<Order, Long>, OrderRepository {
@Override
default Optional<Order> findById(Long id) {
return JpaRepository.super.findById(id);
}
}
4. 프레젠테이션 계층 (Presentation)
package com.example.presentation.controller;
import com.example.application.service.OrderService;
import com.example.domain.model.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
@PostMapping
public Order createOrder(@RequestParam Long id, @RequestParam String product, @RequestParam int quantity) {
return orderService.createOrder(id, product, quantity);
}
@GetMapping("/{id}")
public Optional<Order> getOrder(@PathVariable Long id) {
return orderService.getOrderById(id);
}
}
4. 스프링에서 헥사고날 아키텍처의 장점
- 유연성: 핵심 도메인 로직이 외부 시스템과 독립적이기 때문에, 시스템의 각 부분을 변경하거나 교체하기 쉽습니다. 예를 들어, 데이터베이스를 변경하더라도 핵심 비즈니스 로직에 영향을 주지 않습니다.
- 테스트 용이성: 외부 시스템과의 의존성이 적기 때문에, 도메인 로직을 독립적으로 단위 테스트할 수 있습니다.
- 확장성: 새로운 외부 시스템과의 통합이 용이합니다. 예를 들어, 새로운 API나 데이터베이스를 추가하려면 새로운 어댑터만 작성하면 됩니다.
5. 결론
헥사고날 아키텍처는 스프링 프레임워크에서 구현하기에 적합한 아키텍처 패턴입니다. 이를 통해 비즈니스 로직을 중심으로 시스템을 설계하고, 외부 시스템과의 의존성을 최소화하여 유지보수와 확장성을 높일 수 있습니다.
스프링 부트를 사용하면 포트와 어댑터 패턴을 쉽게 구현할 수 있으며, 이로 인해 코드의 응집력과 테스트 가능성도 크게 향상됩니다. 헥사고날 아키텍처를 도입하는 것은 애플리케이션의 품질을 높이고, 미래의 요구 사항 변화에 쉽게 대응할 수 있게 합니다.
'프로그래밍 > Java' 카테고리의 다른 글
자바 synchronized (0) | 2024.03.29 |
---|---|
[Java] List<?> indexof (equals, hascode @Override) (0) | 2023.11.04 |
[Java] Jsoup를 이용한 웹 크롤링 (0) | 2023.06.15 |
[Java] "|" 를 구분자로 하여 split 하기 (1) | 2023.06.14 |
자바로 OpenAPI로 받은 XML 데이터 파싱하기 (0) | 2023.06.13 |