티스토리 뷰

회사에서 최근 데이터를 블록체인에 저장하려고 하면서 관련된 새로운 API 서버와 연동을 해야하는 작업을 진행했었습니다. 기존 로직과 동일하긴 하지만 부분 부분 수정이 필요하였고 그러다보니 재개발 수준까지 가게 된 것 같습니다...

 

이쯤되니 잘 모르긴 해도 신규 개발에 맞는 아키텍처가 필요했고 클라이언트, 외부 서비스와 연동하면 자주 변경되는 요청, 응답 부분과 비즈니스 로직을 분리할 필요를 더 절실하게 느끼게 되었습니다.

 

그래서 새로운 아키텍처를 고민하던 중 헥사고날 아키텍처를 알게 되었고 이를 적용해본 과정에 대해 적어보려고 합니다.


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

사전적 의미로는 "육각형 건축물"을 뜻하는 헥사고날 아키텍처(=포트와 어댑터 아키텍처(Ports and Adapters Architecture))는 인터페이스나 기반 요소(infrastructure)의 변경에 영향을 받지 않는 핵심 코드를 만들고 이를 견고하게 관리하는 것이 목표입니다.

 

용어 정리

포트: 인터페이스를 의미합니다. 예를 들면 클래스의 메서드 시그니처나 Java의 인터페이스 등을 포트라고 합니다.

어댑터: 클라이언트에 제공해야 할 인터페이스를 따르면서도 내부 구현은 서버의 인터페이스로 위임하는 것을 말합니다.

 

조금 더 설명을 붙이자면 MVC를 사용하고 있다고 할 때 Service의 메서드를 사용하는 쪽은 컨트롤러이고 컨트롤러는 다시 HTTP를 통한 인터페이스를 클라이언트에게 제공합니다. 이를 그림으로 살펴보면 다음과 같습니다.

이때 서비스는 인터페이스를 제공하므로 포트입니다. (자세히 살펴보면 서비스가 제공하는 메서드가 포트!) 컨트롤러는 클라이언트의 요청을 받아 서비스의 메서드를 연결해주고 있기 때문에 어댑터입니다.

이렇게 외부에서 요청해야 동작하는 포트와 어댑터를 주요소(primary)라고 합니다.

 

한편 서비스의 구현체는 내부적으로 Repository 인터페이스를 사용합니다. 이를 그림으로 살펴보면 아래와 같습니다.

Repository는 인터페이스를 제공하므로 포트이고 이를 각 저장소에 맞게 구현한 구현체가 어댑터가 됩니다. 예를 들어 Redis와 프로토콜 통신을 하는 RedisRepository 구현체는 Repository 라는 인터페이스를 따르면서 내부적으로 Redis 프로토콜과 연결해주기 때문에 어댑터가 됩니다.

이렇게 애플리케이션이 호출하면 동작하는 포트와 어댑터를 부요소(Secondary)라고 합니다.

 

구성

위에서 설명한 내용을 바탕으로 각 요소들의 관계도를 살펴보면 다음과 같습니다.

위 그림을 살펴보면 어댑터가 애플리케이션과 직접 연결되지 않고 포트와 연결되어 있는 것을 확인할 수 있습니다.

 

여기서 포트는 변경이 잦은 어댑터와 애플리케이션의 결합도를 낮추는 역할을 합니다. 또한 애플리케이션은 도메인에 의존하지만 도메인은 애플리케이션이나 어댑터에 전혀 의존하지 않아 이것들이 변경되어도 핵심 로직인 도메인은 아무런 영향을 받지 않습니다.

 

어댑터를 살펴보면 주요소 쪽의 클라이언트는 각각의 어댑터를 통해 애플리케이션을 이용합니다. 클라이언트 단이 추가된다면 그에 맞는 어댑터를 생성하고 기존 포트와 연결하면 됩니다.

반면에 부요소 쪽에는 애플리케이션이 이용하는 기반 요소들이 있습니다. 주요소와 다르게 기반 요소의 포트와 어댑터는 일반적으로 1:1 관계입니다. 하나의 포트에 여러 어댑터가 있다거나 새로 추가될 일은 거의 없지만 기존에 사용하던 어댑터가 교체될 가능성은 충분합니다.

이렇게 어댑터가 추가될 때 포트가 애플리케이션과 도메인을 보호합니다.

 

장점

  1. 아키텍처 확장이 용이합니다.
  2. SOLID 원칙을 쉽게 적용할 수 있습니다.
  3. 모듈 일부를 배포하는 게 용이합니다.
  4. 테스트를 위해 모듈을 가짜로 바꿀 수 있으므로 테스트가 더 안정적이고 쉽습니다.
  5. 더 큰 비즈니스적 가치를 갖고 더 오래 지속되는 도메인 모델에 큰 관심을 둡니다.

 

적용 프로젝트 패키지 구조

테스트용으로 간단한 시나리오만 생성하여 다음과 같이 구성해보았습니다.

 

[시나리오]

1. 사용자가 미션을 등록할 수 있습니다.

2. 등록된 미션은 내부 DB와 외부 저장소(외부 API 호출을 통해)에 저장이 됩니다.

3. 사용자는 미션의 목록, 상세 조회를 할 수 있습니다.

 

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

.
├── common
│   ├── constant
│   ├── exception
│   └── utils
│
├── members
│
├── missions
│   ├── adapter
│   │   ├── constant
│   │   │   └── ContractApiUrl.java
│   │   ├── in
│   │   │   └── web
│   │   │       ├── MissionsRestController.java
│   │   │       └── model
│   │   │           ├── MissionAttachResponse.java
│   │   │           ├── MissionDetailResponse.java
│   │   │           └── MissionQuestionResponse.java
│   │   └── out
│   │       ├── persistence
│   │       │   ├── MissionCategoryJpaRepository.java
│   │       │   └── MissionsJpaRepository.java
│   │       └── web
│   │           ├── ContractApiClient.java
│   │           ├── DataApiClient.java
│   │           ├── exception
│   │           │   └── MissionsCreateException.java
│   │           └── model
│   │               ├── CommonResponse.java
│   │               ├── ...
│   │               └── MissionDetailResponseData.java
│   ├── domain
│   │   ├── MissionCategory.java
│   │   ├── ...
│   │   └── Missions.java
│   └── service
│       ├── MissionsCommandService.java
│       ├── MissionsQueryService.java
│       └── model
│           ├── MissionDetailDto.java
│           ├── ...
│           └── MissionsDto.java
└── ...

 

missions 패키지를 두고 그 아래 adapter, domain, service 패키지를 구성하였습니다.

  • adapter
    • adapter 역할을 하는 클래스들을 모아놓은 패키지입니다. (주로 Controller, Repository)
    • primary/secondary를 in(클라이언트 → 서비스)/out(서비스 → 외부 API, DB 등)으로 분리하였습니다.
    • in/out 안에는 다시 web(HTTP API를 사용하는 부분)과 persistance(DB 사용 부분)으로 분리하였습니다.
  • domain
    • 주요 로직을 담는 패키지입니다. (주로 Entity)
  • service
    • port 역할을 하는 클래스들을 모아놓은 패키지입니다. (주로 Service)
    • 별도 application 패키지를 두고 내부에 port(Service, Repository 인터페이스) 패키지와 그 구현체를 담아도 좋을 것 같지만 현재 인터페이스를 별도로 두지 않아 일단 service로 패키지를 구성하였습니다.
    • 인터페이스를 사용하지 않으면 어댑터와의 의존성에 문제가 있기는 하지만... 일단 간단한 구현을 위해 이렇게 작성하였습니다...🙏

Spring Data JPA를 사용하는 경우 인터페이스를 만들면 구현체는 자동으로 생성되기 때문에 port 쪽(application 패키지 내부)에 Repository를 생성하면 되고 별도 어댑터는 생성할 필요가 없습니다.

하지만 이번 프로젝트에서는 Repository를 adapter.out.persistance 패키지 내부에 생성하였습니다. 별도 어댑터 구성이 없기 때문에 DB를 사용하는 외부로의 호출 작업을 더 잘 표현하기 위해 패키지 구조를 조금 수정하였습니다...!

 

이 아키텍처를 적용하면서 의존성 감소를 위해 각 레이어에서 사용하는 모델들을 각각 별도로 생성하면서 클래스가 매우 많아지는 단점(?)이 있었습니다. 추후에 각각의 레이어 변동이 있다면 이렇게 모델을 분리한 것들이 장점이 될 것으로 생각하지만 필요에 따라 적절히 공유해서 사용해도 좋겠다고 생각했습니다. ㅎㅎ

 

헥사고날 아키텍처를 적용하면서 패키지 구조가 더 명확하고 깔끔해졌다고 생각해서 꾸준히 적용하며 개선해 나갈 생각입니다!

 

참고

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함