일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Generics
- 알고리즘
- Functional Programming
- typescript
- node.js
- https
- Schema Registry
- Let's Encrypt
- ChatGPT
- MSK
- Certbot
- 자료구조
- nestjs
- html
- 함수형프로그래밍
- stream
- vscode
- MSA
- GIT
- nodeJS
- 비주얼 스튜디오 코드
- docker
- Express
- python
- Linux
- V8
- ES6
- 파이썬
- javascript
- NPM
- Today
- Total
JangBaGeum.gif
헥사고날 아키텍처 찍먹 후기 본문
썸네일하고 본 글의 주제는 관련이 없습니다. '◡'✿
나는 현업에 애플리케이션 구조를 설계할 때 가장 흔하면서 단순한 레이어드 아키텍처 Layerd Architecture를 많이 사용해 왔다.
최근에 MSA Microservice Architecture를 공부하면서 접한 헥사고날 아키텍처 Hexagonal Architecture 패턴을 실무에 적용해 보면서 꽤나 긍정적인 경험을 했다. 이번 글에서는 헥사고날 아키텍처를 간단히 소개하면서 작고 소중한 내 작품에 실제로 적용하고 느낀 레이어드 아키텍처와의 차이점, 그리고 장점을 적어보려고 한다.
1. 헥사고날 아키텍처란 Hexagonal Architecture
헥사고날 아키텍처는 Alistair Cockburn(애자일 방법론을 제시한 인물 중 1인)이 제한한 아키텍처 패턴이다.
서론은 아래와 같다.
애플리케이션의 핵심 도메인을 외부 세계로부터 격리시키는 것이 핵심
외부의 세계란, 예를 들어 데이터베이스, 메시징 시스템, HTTP 요청, 콘솔 입력, 파일 I/O 등이 있을 수 있다.
이 방법론의 구조는 크게 중심을 담당하는 도메인이 존재하고, 이를 둘러싼 Port와 Adapter를 통해 외부 시스템과 통신을 한다.
우리는 다재다능한 사람을 "육각형 인간"이라고 한다.
아마 헥사고날 아키텍처라는 용어가 이랑 비슷한 맥락이 아닐까 싶다.
아마 아래와 같이 육각형 구조의 이미지는 많이 봤을 것 같다. 각 위에서 언급한 도메인, Port, Adapter 등이 서로를 감싸고 있는 형태이다.
2. 실무 적용 사례
왜 헥사고날 아키텍처를 고민하게 되었을까?
실무에서 자사 서비스 플랫폼의 API Gateway를 전담하여 개발을 했었고 현재도 다양한 앤드포인트를 붙이는 중이다.
첫 시작에는 단순한 구조를 가진 레이어드 아키텍처로 구성하여 어렵지 않게 단순한 구조를 가지고 개발을 하였다. 단순했던 서비스가 SaaS형 플랫폼으로 발전하면서 요구사항이 많아지고 다양한 엔드폰이트가 생기면서 말단을 관리하기 복잡해졌다.
- 요구사항이 늘어남에 따라, 요청을 트리거 하는 방식과 관리에 대한 고민이 필요했다.
- 요청을 트리거하는 방식은 그나마 변화가 적었지만 외부 의존성의 경우 앤드포인트와 전문이 바뀌기 다반사이고 프로토콜 자체가 바뀌는 경우도 종종 발생하여 서비스 로직과의 거리를 둘 필요가 있었다.
- 레이어 간의 결합도가 강해 테스트의 어려움이 있었다.
이 과정에서, 외부 연결 점과의 영향을 받지 않고도 도메인 로직을 보호할 수 있는 구조가 필요하다 판단했다.
그러하여 최근 프로젝트에서 알림 전송 시스템을 구축하면서 헥사고날 아키텍처를 적용해 봤다.
요구사항의 일부는 아래와 같았다.
- 외부 API에서 주기적으로 특정 데이터 목록을 조회한다.
- 조회된 데이터 기반으로 주기적으로 외부 API에 알림을 전송한다.
- 애플리케이션 내부 데이터를 매니징 할 수 있도록 자체 API를 제공한다.
- 향후 이 스케줄링 로직은 다양한 외부 트리거(메시지 큐 등)로 교체 가능해야 한다.
요구사항 기반으로 패키지의 구조를 구상할 때, 어떻게 해야지 헥사고날 아키텍처에서 말하는 장점들을 잘 녹여낼 수 있을지 고민을 많이 하고 참고 내용도 여럿 찾아보았다.
결과적으로 패키지 구조에는 정답이 없었다. 단, 참고한 패키지 구조들에는 공통적인 핵심을 담고 있었다.
내가 느꼈던 가장 중요한 핵심은
- 구성간의 소통은 인터페이스로 한다. (의존성 역전)
- 의존성 방향을 내부로만 향하게 한다.
- 도메인은 독립 적어야 되며 비즈니스 로직은 엔티티에서 구현한다.
이다.
이와 같이 내가 느낀 핵심들을 기반으로 애플리케이션 패키지 구조를 구성해 봤다.
src
├── adapters
│ ├── in-bound
│ │ ├── http
│ │ │ ├── healthcheck.adapter.dto.ts
│ │ │ └── healthcheck.adapter.ts
│ │ └── scheduler
│ │ └── scheduler.adapter.ts
│ └── out-bound
│ ├── a-http-client
│ │ ├── a-http-client.adapter.dto.ts
│ │ └── a-http-client.adapter.ts
│ ├── b-http-client
│ │ ├── b-http-client.adapter.dto.ts
│ │ └── b-http-client.adapter.ts
│ ├── a-repository
│ │ └── a-repository.adapter.ts
│ ├── logger
│ └── environment
├── domains
│ ├── entities
│ │ ├── a.entity.ts
│ │ └── b.entity.ts
│ └── ports
│ ├── a-fetcher.port.ts
│ ├── b-fetcher.port.ts
│ ├── logger.port.ts
│ ├── config.port.ts
│ └── ...
├── services
│ ├── bootstrap.service.ts
│ ├── syncA.service.ts
│ └── ...
├── common
├── main.ts
폴더 구조부터 차차 설명하자면 다음과 같다:
[ Domains ]
- 도메인 엔티티와 외부의 소통하기 위한 Port 인터페이스들이 존재한다.
- 이 영역은 어떤 프레임워크도 어떤 어댑터도 알 수 없다.
[ Adapter ]
- 실제 외부 시스템들과 연결하는 부분들로 Port의 구현체들이다.
- in-bound: 애플리케이션이 동작할 수 있도록 이벤트가 접근하는 곳이다. (Primary / Driving Adapter라고도 한다.)
- out-bound: 애플리케이션이 의존하는 외부이며 이벤트를 내보내는 곳이다. (Secondary / Driven Adapter라고도 한다.)
- Infrastructure 영역이라고도 한다.
[ Services ]
- Port를 받아서 비즈니스의 핵심 흐름을 정의한 곳이다. (오케스트레이션이라 표현)
- 외부와의 연결 없이 순수하게 도메인 기능만 다룬다.
- Application 영역이라고도 한다.
[ main.ts / Bootstrap ]
- 애플리케이션 내부 클래스간의 의존성 주입 및 실행 초기화를 담당한다.
- Port 인터페이스에 실제 Adapter를 주입하고 서비스를 실행한다.
처음에 Port를 어디에 둬야 할지 정말 고민이 많았다. 여기서 내가 느낀 헥사고날 아키텍처의 핵심을 다시 생각해 보니 해답을 찾는데 어렵지 않았다.
본 아키텍처의 목표는 도메인의 독립성! -> 외부 시스템과 의존하지 않도록 하는 것!
그렇다면 도메인에서 외부와 통신이 필요할 때 정의하는 것이 "Port" 인터페이스인 것이다.
그러니 "Dmains" 폴더에 포함.
또한 폴더 용어에 대한 고민도 많이 했다. "Infrastructure ?" 혹은 "Application ?" 등 레이어드 아키텍처로 작업을 많이 해본 나에게는 이질감이 느껴졌다. 그래서 나는 좀 더 명료하고 축소적인 "Adapter"와 "Service"라는 폴더 명을 사용하였다. (주관적인 부분)
아, 그리고 UseCase라는 구성도 존재하나 내가 제작하는 애플리케이션의 비즈니스는 복잡하지 않고 정의되는 엔티티가 많지 않았기 때문에 과감하게 제거하였다.
(UseCase를 간단하게 설명하자면, 도메인의 기능을 사용해 사용자의 특정 요구나 시나리오를 처리하는 비즈니스 로직의 실행 단위라고 한다. Port와 동일하게 인터페이스로 작성하여 사용한다.)
3. 레이어드 아키텍처와의 차이점
전통적인 레이어드 아키텍처는 흔히 다음과 같은 구조를 갖는다.
Controller → Service → Repository → DB
각 계층이 아래 계층에 의존하며, 도메인 로직은 보통 Service에 섞여 있는 경우가 많다.
반면, 헥사고날 아키텍처는 의존성 방향을 반대로? 잡는다. (역전이니까 반대라는 표현이 맞는 거죠,,?)
즉, 도메인이 가장 중심에 있고, 외부에서 도메인을 향해 의존하도록 만든다.
Controller ← Port (interface) → Domain ← Port (interface) → Repository
역시 내가 헥사고날 아키텍처를 실무에 적용하면서 느낀 가장 큰 차이는 "의존성은 어디로 향하는가"에 있다.
- 레이어드 아키텍처: 각 계층 간의 의존성이 강하다. (외부 계층인 DB, 프레임워크 등에 강하게 의존한다.)
- 헥사고날 아키텍처: 도메인은 외부를 모른다. 즉, 뭐로 어떻게 만들든 Port만 부함 하면 되고 외부가 도메인을 알아야 한다.
4. 내가 느낀 장점
[ 테스트가 쉽다. ]
- Mock Adapter 만 만들어서 UseCase 단위로 테스트를 구성하면 된다.
[ 구조가 명확하다. ]
- 난 이 부분이 가장 마음에 들었다. "알림을 전송하는 책임", "데이터를 불러오는 책임" 등 명확하게 구조만 봐도 느낄 수 있다.
[ 동작을 위한 트리거 방식이 우연하다. ]
- 이 부분도 마음에 들었다. 현재 내 프로젝트에서는 setInterval을 사용하여 만들었지만, 이는 메시지 소비 방식, REST API 요청 방식 등으로 도메인에 영향이 없이 Adapter만 교체하면 된다.
- 지금 우리 조직처럼 레거시를 걷어내면서 신규 애플리케이션을 만들어가는 업무에서는 큰 장점이 되지 않을까 싶다.
[ 프레임워크 독립성이 커졌다. ]
프레임워크 의존적인 구조로 변화에 어려움 없이 핵심 비즈니스 로직은 프레임워크 없이 도작하도록 분리가 가능했다.
핵사고 날 아키텍처의 구조의 그림을 봤을 때, 마치 이상만 쫓아가는 구조가 아닐까 의심을 했다.
이전에 레이어드 아키텍처를 이용하여 API Gateway를 구현할 때 했던 고민들의 많은 부분들을 헥사고날 아키텍처가 해결해주고 있었고 이상인지 아닌지 맛보고 싶다는 생각이 들었었다.
한편 해당 아키텍처에 대한 부정적인 평가도 많았다. 약도 맞는 곳에 써야 된다라는 생각은 항상 가지고 있다. 하지만 난 만족스러웠다.
처음에는 낯설고, 초반 작업이 많다 느껴질 수도 있다. 하지만 복잡한 애플리케이션일수록 명확한 경계를 만들어주는 구조가 유지 보수에 있어 큰 도움을 줄 수 있다는 것을 많이 느꼈다.
새로운 프로젝트를 설계하거나, 레거시 개선을 할 기회가 있다면 이 구조를 또 고려해볼까 한다.
참고
- https://devocean.sk.com/blog/techBoardDetail.do?ID=165581
- https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)
'Backend > 개발 방법론 & 디자인 패턴' 카테고리의 다른 글
[Rate Limiting] Token Bucket 알고리즘 (2) | 2024.10.27 |
---|---|
API 디자인 패턴 (1) | 2023.12.13 |
DTO (Data Transfer Object)의 설계법 (0) | 2023.12.12 |
[Design Pattern] 싱글톤 패턴이란? (Singleton Pattern) (0) | 2023.03.11 |
[Methodologies] Monorepo(모노레포)란? (0) | 2023.02.24 |