깨록

쉽게쉽게 알아보는 헥사고날 아키텍처! 본문

개발하다

쉽게쉽게 알아보는 헥사고날 아키텍처!

쓰온 2024. 10. 30. 00:13

글을 쓰게 된 배경

현업에서 헥사고날 아키텍처(Hexagonal Architecture)를 도입하기 위해 스터디를 진행하던 중, 관련된 배경 지식을 정리할 필요성을 느껴 이 글을 작성하게 되었습니다.

 

이 글의 목표는 초보자인 저의 수준에 맞추어 헥사고날 아키텍처를 이해하기 쉽게 설명하는 것입니다.

 

 


헥사고날 아키텍처란?

'만들면서 배우는 클린 코드'에 나오는 이미지를 직접 그려보았습니다.

 

포트 앤 어댑터 아키텍처(Ports and Adapters Architecture)라고도 불리며, 2005년 Alistair Cockburn에 의해 제안된 설계 패턴입니다.

 

헥사고날 아키텍처라는 명칭처럼 예시로 작성된 이미지들을 보면 거의 대부분 육각형 모양으로 되어 있고 이미지에 보이는 것과 같이 각각 표면에 포트와 외부에 어댑터를 가지는 형태입니다.

 

이 아키텍처는 비즈니스 로직을 외부 의존성으로부터 분리하여 더 유연하고 유지보수하기 쉬운 구조를 제공합니다. 이를 통해 비즈니스 로직이 중심에 자리 잡고, 외부의 변화에도 독립적으로 유지될 수 있는 설계를 목표로 합니다.

 

 


헥사고날 아키텍처를 사용하게 된 이유

이는 기존에 많이 사용되던 계층형 아키텍처(Layered Architecture)와 연관이 있습니다.

 

계층형 아키텍처는 웹 계층, 도메인 계층, 영속성 계층 간의 의존성이 발생하여, 데이터베이스 중심 설계를 유도하고 유지보수가 어려워지는 문제가 있었습니다. 예를 들어, 도메인 계층이 영속성 계층에 의존하게 되면, 데이터베이스의 구조적 변화가 도메인 로직에 직접적인 영향을 미치게 됩니다. 또한, 각 계층의 의존성으로 인해 병렬 작업이 어려워지고, 여러 유스케이스를 담당하는 거대한 서비스가 생기면서 테스트와 유지보수가 어려워집니다.

 

즉, 너무 가까운 사이면 좋든 안좋든 닮아가고 영향을 받는 것처럼(?) 계층형 아키텍처는 각 계층이 서로 강하게 의존하여 데이터베이스 변화에 취약하고, 유지보수와 테스트가 어렵게 만드는 구조적인 문제를 가지고 있습니다.

 

이러한 문제를 해결하기 위해 헥사고날 아키텍처가 도입되게 됩니다.

 

 

헥사고날 아키텍처는 핵심 비즈니스 로직을 외부 의존성과 분리하여 더 유연하게 유지보수할 수 있도록 설계합니다.

 

 

설명만으로는 이해가 되기 어렵다고 생각해서 다음 코드 예시를 보며 계층형 아키텍처와 헥사고날 아키텍처의 구조를 비교해 보겠습니다.

 

먼저 계층형 아키텍처는 다음과 같이 구현할 수 있습니다.

// 계층형 아키텍처 예시
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void placeOrder(Order order) {
        // 비즈니스 로직 수행
        if (order.getQuantity() <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than zero");
        }
        // 데이터베이스에 직접 접근
        orderRepository.save(order);
    }
}

public class OrderRepository {
    public void save(Order order) {
        // 데이터베이스에 주문 저장 로직
    }
}

 

위 예시에서는 OrderServiceOrderRepository에 직접 접근하여 데이터를 저장하는 구조입니다. 이는 영속성 계층에 대한 의존성이 생기고, 영속성 계층의 변화가 비즈니스 로직에도 영향을 미칠 수 있는 문제가 있습니다.

 

다음으로 헥사고날 아키텍처는 포트와 어댑터를 사용해 비즈니스 로직을 외부 의존성으로부터 분리합니다.

// 헥사고날 아키텍처 예시
// 포트 정의
public interface OrderPort {
    void saveOrder(Order order);
}

// 도메인 서비스
public class OrderService {
    private final OrderPort orderPort;

    public OrderService(OrderPort orderPort) {
        this.orderPort = orderPort;
    }

    public void placeOrder(Order order) {
        // 비즈니스 로직 수행
        if (order.getQuantity() <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than zero");
        }
        // 포트를 통해 영속성 접근
        orderPort.saveOrder(order);
    }
}

// 어댑터 (영속성 구현체)
public class OrderPersistenceAdapter implements OrderPort {
    @Override
    public void saveOrder(Order order) {
        // 데이터베이스에 주문 저장 로직
    }
}

 

위 코드에서는 OrderServiceOrderPort 인터페이스를 통해 영속성 계층과의 의존성을 분리합니다.


이를 통해 비즈니스 로직은 영속성 계층의 구현체를 몰라도 되고, 테스트 시에는 OrderPort를 쉽게 모의(Mock) 객체로 대체할 수 있어 유지보수성과 테스트 용이성이 향상됩니다.

 

 


헥사고날 아키텍처와 DDD

앞서서 헥사고날 아키텍처를 왜 사용했는가에 대해서 알아보았다면 다음으로는 헥사고날 아키텍처에 대해서 설명할 때 빠질 수 없는 개념인 DDD(Domain-Driven Design)와 함께 알아보겠습니다.

 

DDD는 비즈니스 도메인의 복잡성을 관리하기 위해 도메인 모델을 중심으로 시스템을 설계합니다. 그리고 헥사고날 아키텍처는 이러한 도메인 모델이 외부 요소와 철저히 분리되도록 하여, DDD의 장점을 극대화할 수 있는 구조를 제공합니다.

 

(DDD에 대한 설명은 다른 글에서 또 작성해 보도록 하겠습니다!)

 

헥사고날 아키텍처에서 DDD와 연계된 코드의 예시를 보겠습니다.

// 도메인 모델
public class Order {
    private Long id;
    private String product;
    private int quantity;

    // 비즈니스 로직 메서드
    public void validateOrder() {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be greater than zero");
        }
    }
}

// 포트 정의 (인바운드 포트)
public interface PlaceOrderUseCase {
    void placeOrder(Order order);
}

// 어댑터 (영속성 어댑터)
public class OrderPersistenceAdapter implements OrderPort {
    @Override
    public void saveOrder(Order order) {
        // 실제 데이터베이스 저장 로직
    }
}

// 서비스 (애플리케이션 계층)
public class PlaceOrderService implements PlaceOrderUseCase {
    private final OrderPort orderPort;

    public PlaceOrderService(OrderPort orderPort) {
        this.orderPort = orderPort;
    }

    @Override
    public void placeOrder(Order order) {
        order.validateOrder();
        orderPort.saveOrder(order);
    }
}

 

위 코드에서 도메인 모델(Order) 은 비즈니스 로직을 포함하며, 외부의 데이터베이스와 같은 기술적인 요소와는 완전히 분리되어 있습니다.


OrderPort는 도메인 모델이 외부 시스템과 연결되는 지점을 정의하고, OrderPersistenceAdapter는 실제 데이터 저장 로직을 구현합니다.

 

이로써 비즈니스 로직과 영속성 로직을 분리하여 유지보수성과 테스트 용이성을 높일 수 있습니다.

 

 


헥사고날 아키텍처의 계층 구성

그렇다면 헥사고날 아키텍처를 사용해서 코드를 구현할 때 계층 구성은 어떻게 해야 할까요?

 

전체적인 구조는 다음과 같습니다.

최상위 패키지
- adapter
    -- in
        --- web  // 웹 어댑터, 클라이언트 요청 처리
    -- out
        --- persistence  // 영속성 어댑터, 데이터베이스와의 연결 관리
- domain
- application  // 도메인 모델을 둘러싼 서비스 계층 포함
    -- service
    -- port
        --- in  // 유스케이스 인터페이스 정의
        --- out // 외부 의존성과의 연결 인터페이스 정의

Port

포트는 시스템 내부와 외부가 소통할 수 있도록 인터페이스를 정의합니다.
비즈니스 로직이 외부와의 세부 구현에 의존하지 않도록 분리하는 역할을 합니다.
포트는 내부 비즈니스 로직이 외부 시스템과 어떻게 연결되어야 하는지를 규칙으로 정의하여, 유연하고 테스트 가능한 시스템을 만듭니다.

  • in 포트 : 외부에서 내부로 들어오는 요청을 처리하는 규칙을 정의합니다. 예를 들어, 클라이언트 요청에 대응하는 유스케이스 인터페이스가 이곳에 정의됩니다.
  • out 포트 : 내부에서 외부 시스템과 소통할 때 필요한 인터페이스를 정의합니다. 데이터베이스와의 통신이나 외부 API 호출 등이 여기에 포함됩니다.

 

Adapter

어댑터는 포트가 정의한 인터페이스를 구현하여, 외부 시스템과의 실제 연결을 담당합니다. 어댑터는 포트를 통해 내부와 외부를 연결하여 구체적인 동작을 수행합니다.

  • in 어댑터 : 클라이언트 요청을 받아들여 내부 서비스에 전달하는 역할을 합니다. 예를 들어, 웹 클라이언트로부터 HTTP 요청을 받아 비즈니스 로직에 전달하는 역할을 합니다.
  • out 어댑터 : 내부의 요청을 외부 시스템에 맞게 변환하고 처리합니다. 예를 들어, 비즈니스 로직에서 요청한 데이터를 데이터베이스에 저장하는 구체적인 작업을 담당합니다.

 

Domain

도메인은 시스템의 핵심 비즈니스 로직과 규칙을 포함하는 계층입니다.
여기에는 도메인 모델, 엔티티, 비즈니스 규칙 등이 포함됩니다.
도메인 계층은 외부의 변화에 독립적으로 유지되며, 핵심 로직이 담긴 가장 중요한 부분입니다.
이 계층에서는 비즈니스 로직을 최대한 순수하게 유지하여 다른 계층의 변화로부터 영향을 받지 않도록 합니다.

 

Application

애플리케이션 계층은 도메인 모델을 둘러싼 서비스 계층을 포함하며, 비즈니스 로직을 실행하고 포트와 어댑터를 통해 연결을 관리합니다.
이 계층은 비즈니스 로직을 실행하면서 유스케이스를 처리하고, 외부와의 인터페이스를 조율합니다.

  • 서비스 : 도메인 모델의 유스케이스를 구현하며, 외부에서 들어온 요청을 처리하고 그 결과를 반환합니다. 서비스 클래스는 비즈니스 규칙을 적용하고, 도메인 모델과 상호작용합니다. 이 계층에서는 실제 유스케이스를 정의하고, 해당 로직을 조정하여 시스템의 흐름을 제어합니다.
  • 포트 : 애플리케이션 계층에서 포트는 외부와 상호작용할 수 있는 지점을 정의합니다. 이를 통해 내부 로직이 외부 구현과 독립성을 유지할 수 있으며, 테스트 용이성을 높입니다.

위 구조에서 adapter는 외부 시스템과의 연결을 담당합니다. 예를 들어 클라이언트의 요청은 in 패키지 어댑터로 구현되고, DB에 데이터 저장하는 것은 out 패키지 어댑터로 구현될 수 있습니다.

 

 


글을 마무리하며

실제 헥사고날 아키텍처에 대해 얘기를 시작하면 클린 아키텍처부터 시작해 얘기해야 할 내용이 끝도 없이 많습니다... 만! 현재 작성자인 제가 이해하는 수준과 관심사에 맞춰서 '그래서 코드로 어떻게 만들 수 있는데?'라는 생각으로 해당 내용들을 작성해 보았습니다.

 

조금은 쉽게 설명하고자 했지만 생각보다 일반적인 지식을 그대로 옮기는 것이 아니라 풀어 설명한다는 것이 매우 어렵다는 것도 느꼈습니다.

 

헥사고날 아키텍처 이외에도 디자인 패턴이나 소프트웨어 아키텍처와 같이 개발을 용이하게 해주는 개념들에 대해서도 관심을 가지고 글을 더 작성해나가고자 합니다.

 

해당 내용이 저처럼 처음 헥사고날 아키텍처를 접하는 분들께 도움이 되길 바라고 혹시 헷갈리게 하거나 잘못된 정보에 대해서는 너그러운 마음으로 지적해 주시면 꾸준히 더 좋은 글이 되도록 고쳐나가겠습니다. 감사합니다!