헥사고날 아키텍처의 폴더(패키지) 구조 예제
전통적인 계층형 아키텍처(controller, service, repository, domain 패키지가 나란히 있는 구조)와 달리, 헥사고날 아키텍처는 기능 또는 도메인 중심으로 패키지를 구성합니다.
“온라인 서점”의 “주문(Order)” 기능을 예로 들어보겠습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
com/example/bookstore/ └── order/ // ✅ 최상위 패키지: 'order'라는 도메인(기능) ├── domain/ // noyau (Core) - 가장 안쪽, 순수한 비즈니스 규칙과 데이터 │ ├── Order.java // 주문 도메인 객체 (JPA, JSON 어노테이션 없음!) │ ├── OrderLine.java // 주문 항목 │ └── OrderStatus.java // 주문 상태 (ENUM) │ ├── application/ // 2️⃣ 응용 계층 - 비즈니스 로직의 흐름을 제어 (오케스트레이션) │ ├── port/ // 🔌 포트 (인터페이스) - 내/외부의 연결 통로 │ │ ├── in/ // 📥 인바운드 포트: 외부 -> 내부 호출 규격 │ │ │ └── PlaceOrderUseCase.java // '주문하기' 유스케이스 인터페이스 │ │ │ └── GetOrderQuery.java // '주문조회' 유스케이스 인터페이스 │ │ │ │ │ └── out/ // 📤 아웃바운드 포트: 내부 -> 외부 호출 규격 │ │ └── SaveOrderPort.java // '주문저장'을 위한 포트 │ │ └── LoadOrderPort.java // '주문로드'를 위한 포트 │ │ └── SendSmsPort.java // '문자발송'을 위한 포트 │ │ │ └── service/ // ⚙️ 서비스 - 유스케이스의 실제 구현체 │ └── PlaceOrderService.java // PlaceOrderUseCase를 구현한 클래스 │ └── adapter/ // 🔌 어댑터 - 포트의 실제 구현체 (외부 기술) ├── in/ // 📥 인바운드 어댑터: 외부 요청을 내부로 전달 │ └── web/ // - 웹(HTTP)으로부터의 요청을 처리 │ ├── PlaceOrderController.java // @RestController, PlaceOrderUseCase 호출 │ └── PlaceOrderRequest.java // 주문하기 요청 DTO │ └── out/ // 📤 아웃바운드 어댑터: 내부의 요청을 실제 기술로 처리 ├── persistence/ // - 영속성(DB) 처리를 담당 │ ├── OrderPersistenceAdapter.java // SaveOrderPort, LoadOrderPort 구현 │ ├── OrderJpaEntity.java // DB 테이블과 매핑되는 JPA 엔티티 │ └── OrderJpaRepository.java // Spring Data JPA 인터페이스 │ └── notification/ // - 외부 알림(SMS) 처리를 담당 └── SmsAdapter.java // SendSmsPort 구현 (실제 SMS API 연동) |
각 계층별 상세 설명
1. (가장 핵심, 순수함의 영역)
- 역할: 비즈니스의 핵심 규칙과 데이터를 담습니다.
- 규칙: 어떠한 외부 프레임워크나 라이브러리에도 의존해서는 안 됩니다. (
jakarta.persistence.*,org.springframework.*등의 import가 없어야 합니다.)- 예시 (
Order.java):
123456789101112131415public class Order {private Long id;private List<OrderLine> orderLines;private OrderStatus status;public void cancel() { // 스스로의 상태를 변경하는 비즈니스 로직if (status == OrderStatus.SHIPPED) {throw new IllegalStateException("이미 배송된 상품은 취소할 수 없습니다.");}this.status = OrderStatus.CANCELLED;}// ... 생성자, Getter 등}
- 예시 (
2. application (도메인을 지휘하는 응용 로직)
- 역할: 도메인 객체와 포트를 사용하여 실제 비즈니스 유스케이스(흐름)를 완성합니다.
port.in(유스케이스 인터페이스): “무엇을 할 수 있는가”를 정의합니다.- 예시 (
PlaceOrderUseCase.java):
12345public interface PlaceOrderUseCase {Order placeOrder(PlaceOrderCommand command);}
- 예시 (
port.out(외부 서비스 의존성 인터페이스): “무엇이 필요한가”를 정의합니다.- 예시 (
SaveOrderPort.java):
123456public interface SaveOrderPort{Order save(Order order);}
- 예시 (
service(유스케이스 구현체):- 예시 (
PlaceOrderService.java):
123456789101112131415@Service@RequiredArgsConstructorpublic class PlaceOrderService implements PlaceOrderUseCase {private final LoadOrderPort loadOrderPort; // 외부 포트에 의존private final SaveOrderPort saveOrderPort; // 외부 포트에 의존@Overridepublic Order placeOrder(PlaceOrderCommand command) {// ... 로직 ...Order order = new Order(...);return saveOrderPort.save(order); // DB에 저장해달라고 '요청'}}
- 예시 (
3. adapter (실제 기술이 구현되는 곳)
- 역할: 포트를 구현하여 외부 세계와 실제로 상호작용합니다.
adapter.in(주도하는 어댑터, Driving Adapter): 외부 요청을 받아 애플리케이션을 ‘주도’합니다.- 예시 (
PlaceOrderController.java):
12345678910111213@RestController@RequiredArgsConstructorpublic class PlaceOrderController {private final PlaceOrderUseCase placeOrderUseCase; // 인바운드 포트에 의존@PostMapping("/orders")public void placeOrder(@RequestBody PlaceOrderRequest request) {PlaceOrderCommand command = request.toCommand();placeOrderUseCase.placeOrder(command); // 서비스 호출}}
- 예시 (
adapter.out(주도받는 어댑터, Driven Adapter): 애플리케이션의 요청을 받아 외부 시스템과 ‘연동’합니다.- 예시 (
OrderPersistenceAdapter.java):
123456789101112131415@Repository // Persistence Adapter@RequiredArgsConstructorpublic class OrderPersistenceAdapter implements SaveOrderPort, LoadOrderPort {private final OrderJpaRepository orderRepository; // 실제 기술(JPA)에 의존@Overridepublic Order save(Order order) {OrderJpaEntity entity = OrderJpaEntity.from(order); // 도메인 -> 엔티티OrderJpaEntity savedEntity = orderRepository.save(entity);return savedEntity.toDomain(); // 엔티티 -> 도메인}// ... LoadOrderPort 구현 ...}
- 예시 (
왜 이렇게 복잡하게 할까? (장점)
- 테스트 용이성 (Testability):
PlaceOrderService를 테스트할 때, 실제 DB가 필요 없습니다.SaveOrderPort의 가짜 구현체(Mock Object)를 주입하면 되므로, 빠르고 독립적인 단위 테스트가 가능합니다. - 기술 교체의 유연성 (Flexibility): 만약 데이터베이스를
JPA에서MyBatis로, 또는MySQL에서MongoDB로 바꾸고 싶다면?adapter.out.persistence패키지만 새로 만들어서 교체하면 됩니다.domain과application코드는 단 한 줄도 건드릴 필요가 없습니다. - 비즈니스 로직 집중: 개발자는 외부 기술에 대한 걱정 없이
domain과application계층에서 순수한 비즈니스 로직 구현에만 집중할 수 있습니다.
초기에는 구조가 복잡해 보일 수 있지만, 애플리케이션의 생명주기가 길어지고 변화에 대응해야 할 때 헥사고날 아키텍처의 진정한 가치가 드러납니다.