본문 바로가기
Spring

헥사고날 아키텍처(Hexagonal Architecture)

by zkdlu 2021. 3. 27.

기존의 계층형 아키텍처는 DIP를 적용해도 한계가 있고, 도메인이 인프라에 의존하게 되면서 도메인적인 관심사와 기술적인 관심사가 섞이게 됩니다.

MSA에서는 여러 종류의 어플리케이션을 호출하는 시스템과 상호작용하는 저장소가 존재하기 때문에 다양한 인터페이스를 필요로 합니다. 하지만 시스템의 관점에서는 호출하는 시스템과 Infra 간에 큰 차이가 없습니다.

 

이러한 사상에서 탄생한 헥사고날 아키텍처는 '포트와 어댑터 아키텍처' 라고도 불리며 비즈니스 로직을 표현하는 내부 영역과 인터페이스 처리를 담당하는 외부 영역으로 나뉩니다.

 

내부 영역은 순수한 비즈니스 로직을 표현하는 영역으로 외부 영역과 연계되는 포트(Port)를 가지고 있고, 외부 영역은 외부에서 들어오는 요청을 처리하는 인 바운드 어댑터(Inbound Adpater)와 비즈니스 로직에 의해 호출되어 외부와 연계되는 아웃 바운드 어댑터(Outbound Adapter)로 구성됩니다.

 

포트는 인 바운드(Inbound)/아웃 바운드(Outbound) 포트로 구분되고, 인 바운드 포트는 내부 영역 사용을 위해 표출된 API이며, 외부 영역의 인 바운드 어댑터가 호출한다. 아웃 바운드 포트는 내부 영역이 외부를 호출하는 방법을 정의합니다.

 

헥사고날의 가장 큰 특징은 비즈니스 로직을 표현하는 내부 영역이 어댑터에 의존하지 않고 오직 내부 영역이 제공하는 포트에 의존하여 구현하는 것입니다.

 

다음의 시나리오로 코드를 작성해보겠습니다.

1. 고객은 핸드폰과 웹을 이용해 돈을 입력하면 주문을 할 수 있다.
2. 주문이 완료되면 주문 기록을 저장한다.
3. 관리자는 웹에서 주문번호로 영수증을 발급 받는다.
DTO는 생략하겠습니다.

 

주문 도메인 모델

@Getter
public class Order {
    public enum OrderState { PREPARE, COMPLETE }

    private String id;
    private OrderState orderState;
    private int money;

    public Order(String id, int money) {
        this.id = id;
        this.orderState = OrderState.PREPARE;
        this.money = money;
    }

    public void place() {
        this.orderState = OrderState.COMPLETE;
    }
}

영수증 도메인 모델

@Getter
public class Receipt {
    private String orderId;
    private int money;

    public Receipt(String orderId, int money) {
        this.orderId = orderId;
        this.money = money;
    }
}

 

이제 유즈케이스에 해당하는 Inbound 포트를 작성합니다.

 

    1. 주문을 하고, 기록을 저장한다.

public interface PlaceOrderUseCase {
    OrderResult placeOrder(OrderRequest orderDetail);
}

    2. 주문번호를 입력하면 영수증을 발급한다.

public interface GetReceiptUseCase {
    ReceiptResult getReceipt(String orderId);
}

 

각 포트의 구현체를 작성합니다.

 

주문 서비스에서는 주문 후 주문 정보를 기록하는 포트를 사용해 주문정보를 기록합니다.

@Service
@RequiredArgsConstructor
public class PlaceOrderService implements PlaceOrderUseCase {
    private final RecordOrderPort recordOrderPort;

    @Transactional
    @Override
    public OrderResult placeOrder(OrderRequest orderDetail) {
        Order order = new Order(UUID.randomUUID().toString(), orderDetail.getMoney());
        order.place();

        recordOrderPort.recordOrder(new OrderRecord(order.getId(), order.getMoney()));

        return new OrderResult(order.getId(), order.getMoney());
    }
}

영수증 발급 서비스에서는 영수증을 발급해주는 포트를 이용해 데이터를 조회합니다.

@RequiredArgsConstructor
@Service
public class GetReceiptService implements GetReceiptUseCase {
    private final GetOrderRecordPort orderRecordPort;
    @Override
    public ReceiptResult getReceipt(String orderId) {
        OrderRecord orderRecord = orderRecordPort.getOrder(orderId);
        Receipt receipt = new Receipt(orderRecord.getOrderId(), orderRecord.getMoney());

        return new ReceiptResult(receipt.getOrderId(), receipt.getMoney());
    }
}

 

이어서 outbound 포트도 작성해줍니다. DB를 사용하는 포트를 저장용과 조회용으로 각각 만들어줍니다.

 

   1. 주문 기록을 저장한다.

public interface RecordOrderPort {
    void recordOrder(OrderRecord orderRecord);
}

    2. 주문번호에 해당하는 주문기록을 조회한다.

public interface GetOrderRecordPort {
    OrderRecord getOrder(String orderId);
}

 

만들어진 포트를 이용한 어댑터를 만들어 줍니다. 시나리오 상에서 주문을 하기 위한 인터페이스는 모바일과 웹으로 2개의 종류가 있으나, 우리의 시스템은 포트를 이용해 주문을 할 수 있는 방법을 제공해주기 때문에 비즈니스 로직을 건드리지 않고 구현이 가능해집니다.

@RequiredArgsConstructor
@RestController
public class PhoneOrderController {
    private final PlaceOrderUseCase placeOrderUseCase;

    @GetMapping("/phone/{money}")
    public PhoneOrderResult order(@PathVariable int money) {
        OrderRequest orderRequest = new OrderRequest(money);
        OrderResult orderResult=  placeOrderUseCase.placeOrder(orderRequest);

        return new PhoneOrderResult(orderResult.getOrderId(), orderResult.getMoney());
    }
}


@RequiredArgsConstructor
@RestController
public class WebOrderController {
    private final PlaceOrderUseCase placeOrderUseCase;

    @GetMapping("/web/{money}")
    public OrderResult order(@PathVariable int money) {
        OrderRequest orderRequest = new OrderRequest(money);
        
        return placeOrderUseCase.placeOrder(orderRequest);
    }
}

관리자가 영수증을 발급하기 위한 어댑터를 만들어줍니다.

@RequiredArgsConstructor
@RestController
public class AdminReceiptController {
    private final GetReceiptUseCase getReceiptUseCase;

    @GetMapping("/receipt/{orderId}")
    public ReceiptResult getReceipt(@PathVariable String orderId) {
        return getReceiptUseCase.getReceipt(orderId);
    }
}

 

마지막으로 DB를 사용하는 어댑터를 만들어 두 개의 Outbound 포트를 연결해줍니다.

이 어댑터는 JpaRepository를 이용해 DB에 값을 저장하고 조회할 수 있습니다.

@RequiredArgsConstructor
@Repository
public class RecordOrderAdapter implements RecordOrderPort, GetOrderRecordPort {
    private final OrderRecordRepository orderRecordRepository;

    @Override
    public void recordOrder(OrderRecord orderRecord) {
        orderRecordRepository.save(OrderRecordEntity.from(orderRecord));
    }

    @Override
    public OrderRecord getOrder(String orderId) {
        OrderRecordEntity orderRecord = orderRecordRepository.findByOrderId(orderId).orElseThrow(IllegalAccessError::new);

        return new OrderRecord(orderRecord.getOrderId(), orderRecord.getMoney());
    }
}

public interface OrderRecordRepository extends JpaRepository<OrderRecordEntity, Long> {
    Optional<OrderRecordEntity> findByOrderId(String orderId);
}

 

이걸로 다음과 같은 패키지 구조를 가진 아키텍처가 완성되었습니다.

 

📦orderapp
 ┗ 📂adapter
   ┗ 📂out
     ┗ 📂infrastructure
       ┗ 📂persistence
         ┣ 📜OrderRecordEntity.java
         ┣ 📜OrderRecordRepository.java
         ┗ 📜RecordOrderAdapter.java
   ┗ 📂in
     ┗ 📂presentation
       ┣ 📜AdminReceiptController.java
       ┣ 📜PhoneOrderController.java
       ┣ 📜PhoneOrderResult.java
       ┗ 📜WebOrderController.java
 ┗ 📂application
   ┗ 📂order
     ┗ 📂port
       ┗ 📂in
         ┣ 📜GetReceiptUseCase.java
         ┣ 📜OrderRequest.java
         ┣ 📜OrderResult.java
         ┣ 📜PlaceOrderUseCase.java
         ┗ 📜ReceiptResult.java
       ┗ 📂out
         ┣ 📜GetOrderRecordPort.java
         ┣ 📜OrderRecord.java
         ┗ 📜RecordOrderPort.java
     ┗ 📂service
       ┣ 📜GetReceiptService.java
       ┗ 📜PlaceOrderService.java
 ┗ 📂domain
   ┗ 📂order
     ┣ 📜Order.java
     ┗ 📜Receipt.java

 

 

https://github.com/zkdlu 

 

zkdlu - Overview

Backend Developer . zkdlu has 55 repositories available. Follow their code on GitHub.

github.com