Search

Hexagonal Architecture 란

Tags

1. 개요

하단의 웹 사이트는 헥사고널 아키텍쳐에 대한 친절한 설명이 영어로 작성된 페이지입니다. 해당 페이지에서 중요한 내용들을 번역하고 기록해보겠습니다.

2. Basic Concept of Hexagonal

우선 저자는 Hexagonal Architecture의 기본 개념이 있다는 전제하에 이야기를 풀어나가고 있습니다. 따라서 설명에 앞서 Hexagonal Architecture 대해 자세히 알아볼 필요가 있습니다.
Hexagonal ArchitecturePorts and Adapters Architecture 라고도 불리며 PortsAdapters를 이용하여 소프트웨어 간 결합이 느슨하게 유지되는 것을 말합니다.
결합이 느슨하다는 것은 각각의 component들이 어떤 상황에서도 교체되어도 무방하다는 것을 의미합니다.

2-1. Port란

포트는 프로토콜을 정의하는 인터페이스로서 어플리케이션과 외부 시스템 간의 통신을 원활하게 합니다.
포트는 크게 Incoming PortOutgoing Port로 나뉘게 됩니다. Incoming Port는 애플리케이션의 핵심 로직으로 들어오는 요청을 처리하는 인터페이스를 의미하는 반면 Outgoint Port는 애플리케이션 핵심 로직이 외부 시스템과 상호작용할 때 사용하는 인터페이스를 의미합니다.
Incoming Port의 대표적인 예제는 Service 인터페이스가 있으며, Outgoing Port의 대표적인 예제는 Repository 인터페이스가 있습니다.

2-2. Adapter란

어댑터는 포트 프로토콜을 이용하여 어플리케이션 내부에서 비즈니스 로직을 정의하거나 외부 시스템과 소통합니다. 즉, 포트 인터페이스의 구현체라고 생각하면 됩니다.
Adapter 또한 Inbound AdapterOutbound Adapter로 나누어질 수 있습니다. Inbound Adapter는 외부 시스템이나 사용자로부터 요청을 받아들여 내부 시스템으로 전달하는 역할을 의미합니다. 쉽게 풀어 말하면 사용자의 요청이나 외부 시스템의 호출을 받아 이를 애플리케이션의 비즈니스 로직으로 전달하는 진입점 역할을 합니다. 대표적인 예로는 웹 API가 있습니다.
반면 Outbound Adapter는 내부 시스템에서 처리한 결과나 데이터를 외부 시스템으로 전송하는 역할을 합니다. 이메일 서비스나 외부 API 호출이 이에 해당될 수 있습니다. 즉, 내부 시스템의 데이터를 외부 시스템이 이해할 수 있는 형태로 변환하고 해당 내부 처리 결과를 외부의 다른 시스템이나 서비스에 전달하는 역할을 담당합니다. 예를 들어, 외부 API를 호출하거나 이메일을 전송하는 등의 작업은 Outbound Adapter에 해당합니다.
+ 추가적인 설명을 하자면 S3 혹은 Google Cloud Storage에 파일을 저장하는 행위는 Outbound Adapter가 될 수 있습니다.

2-3. Domain이란

주요한 비즈니스 로직을 포함하고 있는 도메인 클래스는 Hexagon의 중심입니다. 도메인 클래스는 Database, Messaging System 같은 Infrastructure 혹은 반복적인 boiler plate 코드로 부터 자유롭습니다.

3. REST API Adapter

우리가 흔히 작성하는 ControllerREST API Adapter에 해당합니다. 블로그 서비스를 만든다고 가정했을 때, POST나 GET 메소드를 호출해서 글을 작성하거나 조회하는 경로를 독립적으로 만들 수 있습니다.
REST API Adapter, 글에서는 Endpoint로 구현된 Controller, 는 ApiService와 연결되고 있습니다.
@RestController @RequestMapping("articles") class ArticleEndpoint { private final ArticleApiService apiService; @GetMapping("{articleId}") ArticleResponse get(@PathVariable("articleId") String articleId) { return apiService.get(articleId); } @PostMapping ArticleIdResponse create(@RequestBody ArticleRequest articleRequest) { return apiService.create(articleRequest); } }
Java
복사
대게 많은 수의 아키텍쳐들이 Controller → Service 로 구현을 진행하는데 왜 Controller → ApiService → Service로 향하는 구조로 설계 했을까요?
저자는 ApiService와 Service의 개념적 차이를 제시합니다.
Service, 즉 Application Service 는 도메인에도 속하지 않고 어댑터에도 속하지 않는 개념입니다. 이는 model translationorchestration의 책임을 맡으며 이 과정에서 여러 어댑터들이 포함될 수 있습니다.
쉽게 풀어서 해석하면 Application Service는 클라이언트의 요청을 수행하기 위해 다양한 어댑터와 상호작용 한다는 것이고, ApiService는 그 결과물을 가져오면 되는 것이죠!
@Component class ArticleApiService { private final ArticleService articleService; ArticleResponse get(String articleId) { final Article article = articleService.get(ArticleId.of(articleId)); return ArticleResponse.of(article); } ArticleIdResponse create(ArticleRequest articleRequest) { final ArticleId articleId = articleService.create(articleRequest.authorId(), articleRequest.title(), articleRequest.content()); return ArticleIdResponse.of(articleId); } }
Java
복사
참고로 ApiService에서 DTO 클래스로 변환하는 과정을 거칩니다. 즉, Application Service는 요청에 해당하는 도메인을 반환해주면 됩니다.

4. Port without Interface

port는 무조건 interface를 만들어야 한다고 생각하는 경우가 많습니다. 하지만 위 ArticleApiService 같이 inbound adapter는 예외입니다. 인터페이스는 보통 의존성 역전을 위해 사용됩니다.
Article::validateEligibilityForPublication() 는 article을 검증하고 문제가 있으면 예외를 던지는 메소드입니다. 해당 메소드는 외부 의존성이 필요하지 않기 때문에 ArticleService에 위치하고 있지 않습니다.
우리는 이를 Anemic Model Antipattern 이라고 합니다. 반면 article을 생성하고 조회하는것은 외부 의존성이 필요하기 때문에 불가피하게 ArticleService 내에 구현되었습니다.
public class ArticleService { public ArticleId create(AuthorId authorId, Title title, Content content) { Author author = authorRepository.get(authorId); Article article = articleRepository.save(author, title, content); article.validateEligibilityForPublication(); articlePublisher.publishCreationOf(article); return article.id(); } public Article get(ArticleId id) { Article article = articleRepository.get(id); articlePublisher.publishRetrievalOf(article); return article; } }
Java
복사