1. 개요
최근 입사한 회사에선 Hexagonal Architecture와 MSA를 도입하고 있다. 학습을 위해 여러 컨퍼런스 영상과 블로그를 보고 있지만 역시 개발자는 백문이불여일타 아니겠는가?
레이어드 아키텍처로 구성된 프로젝트를 헥사고날 아키텍처로 리펙토링 하고자 한다.
리액트를 학습하기 위해 만든 작은 todo app 프로젝트가 있는데 이 프로젝트로 지지고 볶고 여러 공부를 하고 있다.
이번에도 이 프로젝트 가지고 지지고 볶아 볼 것이다.
2. Hexagonal Architecture란?
간단하게 헥사고날 아키텍처에 대해 언급하고 넘어가면 헥사고날 아키텍처는 애플리케이션의 내부 로직과 외부 인터페이스(예: 데이터베이스, 웹 등)를 분리하여 독립성을 강화하는 아키텍처 스타일이다.
이를 통해 핵심 비즈니스 로직을 외부 변화에 영향을 덜 받게 설계하며, 인터페이스를 통해 다양한 어댑터를 연결할 수 있어 확장성과 테스트 용이성이 높아진다.
자세한 내용은 따로 정리해 둔 글을 참고해주길 바란다. (참고)
3. AS-IS
기존의 패키지 구성이다. 세부 디렉토리 개수 까지 전부 16개로 구성되어 있다.
크게 domain, global로 패키지를 나눴고
domain엔 각 도메인에 관련된 레이어드 아키텍처 구성 요소들이 포함되어 있다.
global엔 프로젝트 전반에서 설정 및 공유하는 내용을 담고 있다. 공통 설정 파일, 예외 처리, 보안 설정, 유틸리티 클래스 등 여러 도메인에서 공통으로 사용하는 요소들을 관리한다.
4. TO-BE
세부 디렉토리 개수 까지 전부 28개로 늘었다.
adapter: 외부 => 내부
외부에서 내부로 들어오는 요청은 messaging 요청, http 요청이다. 따라서 각각 consumer, controller를 배치해서 외부로부터의 요청을 받았다.
📦adapter
┣ 📂in
┃ ┣ 📂message
┃ ┃ ┗ 📜KafkaConsumer.java
┃ ┗ 📂web
┃ ┃ ┣ 📂dto
┃ ┃ ┗ 📜TodoController.java
adaptor: 내부 => 외부
내부에서 외부로 나가는 요청은 open feign, message, persistence 이다. 각각 feign client, producer, repository를 배치했다.
📦adapter
┗ 📂out
┃ ┣ 📂feign
┃ ┃ ┣ 📂dto
┃ ┃ ┣ 📜MemberServerClient.java
┃ ┃ ┗ 📜TempServerClient.java
┃ ┣ 📂mapper
┃ ┃ ┗ 📜TodoEntityMapper.java
┃ ┣ 📂message
┃ ┃ ┗ 📜KafkaProducer.java
┃ ┗ 📂persistence
┃ ┃ ┣ 📜TodoEntity.java
┃ ┃ ┣ 📜TodoRepository.java
┃ ┃ ┗ 📜TodoRepositoryAdaptor.java
repository 앞엔 adaptor을 두었는데 헥사고날 아키텍처에서 adapter를 두는 이유는 애플리케이션 핵심 도메인 로직을 외부 시스템과 분리하여 유연하고 확장 가능한 구조를 유지하기 위함이다.
외부 결제 시스템을 이용하는 서비스가 있다고 가정해보자.
가상의 주문 서비스 시스템에서, 고객이 상품을 주문하면 외부 결제 시스템을 통해 결제가 이루어진다.
이때, 외부 결제 시스템과 결합이 강한 로직을 도메인에 직접 구현하면, 결제 시스템의 변경이 도메인 로직에 영향을 주게 된다. 이를 방지하기 위해 adapter를 두어 외부 결제 시스템과 도메인 로직을 분리할 수 있다.
public class OrderService {
private final PaymentPort paymentPort; // 포트 인터페이스에 의존
public OrderService(PaymentPort paymentPort) {
this.paymentPort = paymentPort;
}
public boolean placeOrder(double amount) {
// 주문 생성 로직 (생략)
// 결제 처리
boolean paymentSuccess = paymentPort.processPayment(amount);
if (paymentSuccess) {
// 결제 성공 시 주문 확정 로직
return true;
} else {
// 결제 실패 시 예외 처리
return false;
}
}
}
OrderService는 PaymentPort를 사용하여 결제를 처리한다. 안에서 어떻게 동작이 이뤄지는지는 OrderService에겐 중요하지 않다.
@Component
public class ExternalPaymentAdapter implements PaymentPort {
@Override
public boolean processPayment(double amount) {
// 외부 결제 시스템 API 호출 로직
System.out.println("Calling external payment system API...");
// 결제 성공 여부 반환
return true; // 성공 가정
}
}
ExternalPaymentAdapter는 PaymentPort 인터페이스를 구현하여, 외부 결제 시스템에 실제 API 호출을 처리하는 로직을 포함한다.
만약 외부 결제 시스템 서비스를 다른 서비스로 갈아 끼운다 하더라도 OrderService엔 영향을 미치지 않는다.
application: 외부 => 내부
adaptor에서 application으로 접근할 때 usecase를 통해 접근할 수 있도록 구성하였다.
📦application
┣ 📂port
┃ ┣ 📂in
┃ ┃ ┣ 📜GetTodoQuery.java
┃ ┃ ┣ 📜SaveTodoUseCase.java
usecase는 in port로써 도메인 로직이 아닌, 애플리케이션 코어에 접근하는 인터페이스이다.
실제 구현체가 도메인 로직을 수행하는 애플리케이션 코어 내부에 위치한다.
즉, 어댑터는 특정 포트를 통해 애플리케이션 코어에 접근하고, 그 포트를 통해 실행하고자 하는 도메인 로직에 해당하는 구현체를 호출하게 된다.
따라서 이를 호출하는 어댑터는 애플리케이션 코어의 구체적인 사항을 알 필요 없이 사용하는 포트만 알면 된다. 이렇게 함으로써 헥사고날 아키텍처는 외부의 변경이 애플리케이션 코어 즉, 비즈니스 로직에 영향을 미치는 것을 방지한다.
그 결과, 각 계층은 자신의 책임에만 집중하면 되므로 결합도는 낮아지고 응집도는 높아지게 된다.
application: service
application에 비즈니스 로직을 수행하는 Service를 두었고, request dto를 domain 객체로 바꿔주는 mapper를 service 패키지 안에 두었다.
📦application
┗ 📂service
┃ ┣ 📂mapper
┃ ┃ ┗ 📜TodoDomainMapper.java
┃ ┗ 📜TodoService.java
application: 내부 => 외부
service 로직에서 외부 호출이 필요할 경우 port를 사용하게 된다. 애플리케이션 코어에서 외부로 나가는 통로라고 보면 된다.
out port는 주로 인프라스트럭처 레이어인 데이터베이스, 외부 서비스 등과 커뮤니케이션을 담당한다. out port는 인터페이스로 정의되고, 실제 구현은 해당 포트의 구현체가 담당한다.
어댑터와 Service로직 사이에 포트를 두는 것으로, 애플리케이션 코어는 외부의 변화에 영향을 받지 않게 되고, 어댑터 역시 포트 인터페이스의 정의를 따르므로 결합도를 낮출 수 있다.
따라서 외부와의 통신 방법이 변경되어도 애플리케이션 코어의 코드는 변하지 않는다.
📦application
┣ 📂port
┃ ┗ 📂out
┃ ┃ ┗ 📜TodoRepositoryPort.java
Infrastructure
Infrastructure 계층에는 애플리케이션의 기술적 세부 사항과 설정을 처리한다.
정답이 있는건 아니지만 나같은 경우, AOP, Config(설정) 관련 클래스, 예외 처리 등을 Infrastructure에서 처리했다.
📦infrastructure
┣ 📂aop
┣ 📂config
┃ ┣ 📂feign
┗ 📂exception
5. 마치며
헥사고날 아키텍처를 도입하면서 많은 패키지와 인터페이스가 생성됐다.
처음 헥사고날을 도입하면서 이거 너무 코드 양이 늘어나는거 아닌가..? 라는 생각을 하게 됐다.
하지만 NHN 기술 컨퍼런스 를 보고 헥사고날 아키텍처의 도입 목적에 대해 확 깨달았다.
우리가 흔히 접하는 코드를 비유한 방 상태이다. 흔히 스파게티 코드로도 불린다. 여기서 새로운 물건(코드)를 놓으려고 하면 어디다가 놓아야 할까? 난감한 상황이다.
이런 방이라면 어떨까? 우리가 생각하는 이상적인 코드 상황이지만, 우리는 프로젝트 초반을 제외하면 계속 기능이 늘어나기 때문에 실제로 이런 코드는 접할 수 없다. 따라서 이런 상황을 목표로 두어선 안 된다.
우리가 추구해야 하는 코드는 이런 것이다. 정말 많은 물건이 있지만 잘 정돈된 모습이다.
우리가 추구해야 하는 코드는 언뜻 보면 복잡해 보일수도 있지만 나름의 질서가 있어서 구조를 파악하기 어렵지 않은 상태를 추구해야 한다.
즉, 헥사고날 아키텍처는 기존의 레이어드 아키텍처보다 많은 패키지 구조를 가져야 하기 때문에 개발하는 입장에서 번거러울 수 있지만 많은 물건(코드)을 정돈하기 위해 수납 공간을 만드는 과정이라고 생각한다.
'아키텍처, 디자인패턴 ⚙️' 카테고리의 다른 글
헥사고날 아키텍처(Hexagonal Architecture) : 지속 가능한 소프트웨어 설계 (2) | 2024.10.19 |
---|---|
객체 지향 설계의 5가지 원칙 SOLID 제대로 짚고 넘어가기 - SRP, OCP, LSP, ISP, DIP (0) | 2024.10.09 |