Hexagonal Architecture, 진짜 하실 건가요?

Hexagonal Architecture, 진짜 하실 건가요?

요약: Hexagonal Architecture를 도입했다가 제거한 경험을 통해 해당 아키텍처의 장단점과 실질적인 문제점을 다뤘습니다. 완벽해 보이는 Hexagonal Architecture도 상황에 따라 약점이 될 수 있음을 강조하며, 이를 적절히 활용하는 방법의 중요성을 전달합니다. 이 글은 실제 사례를 기반으로 아키텍처 도입을 고민하는 개발자들에게 유용한 교훈과 인사이트를 제공합니다.

💡 리뷰어 한줄평

bri.ghten Hexagonal Architecture에 대한 너무나 귀중한 경험을 공유한 글로, Hexagonal Architecture 도입을 고려할 때 필요한 부분들을 다시 한번 생각하게 되는 인사이트를 얻을 수 있습니다.

wade.hong 완벽할 거 같았던 Hexagonal Architecture도 은탄환은 아니었습니다. 그도 약점이 있고 잘 사용하지 않으면 독이 된다는 걸 다시 한번 깨닫게 해 주었습니다. Hexagonal Architecture에서 응용되는 개념을 자신의 상황에 맞게 잘 쓸 수 있다는 점을 배울 수 있습니다.

wonny.p 실제 마주했던 Hexaganal Architecture 문제들을 통해 다시 한번 Hexaganal Architecture에 대해 고찰하게 만드는 실감나는 글이었습니다.

시작하며

안녕하세요, 카카오페이 채널서버유닛 서버 개발자 도리입니다.

채널서버유닛에서는 유저들이 카카오페이의 다양한 서비스에 빠르고 쉽게 접근할 수 있도록 가장 최전방에서 여러 정보를 취합하여 보여주고, 많은 부서와 연동하여 유저에게 편의를 제공하는 서버를 개발하고 있는데요.

이번 글에서는 2023년 개편된 카카오페이의 대문, 카카오페이 새로운 홈 서버의 코드 아키텍처의 변화를 돌아보며, Hexagonal Architecture를 적용했다 다시 제거한 이유에 대해 풀어보려고 합니다.

Hexagonal Architecture가 무엇인지는 알고 계시지만, 도입을 망설이시는 분들께 도움이 되길 바랍니다.

카카오페이 홈 서버

프로젝트 소개

카카오페이는 두 가지 앱에서 접근할 수 있는 서비스입니다. 카카오톡에서도 접근할 수 있고, 카카오페이 앱을 통해서도 접근할 수 있죠. 유저들이 각 앱을 통해 접근하면 카카오페이 홈을 만나게 됩니다.

2023 개편 이전

개편 이전에는 카카오톡을 통해 접근했을 때 만나게 되는 UI와 카카오페이 앱을 통해 접근했을 때 만나게 되는 UI가 상이했습니다. 그리고 유저의 데이터 기반으로 보여주는 정보가 많이 없었습니다. 계좌 잔액, 결제 내역을 제외하곤 대부분 정적인 콘텐츠를 보여주는 영역이 많았습니다.

정적인 콘텐츠가 많았던 홈
정적인 콘텐츠가 많았던 홈

원래는 카카오톡에 특화된 기능과 카카오페이앱에 특화된 기능에 조금씩 차이가 있었기 때문에, 채널서버유닛에서는 각 접근 지점별로 마이크로서비스를 구성하여 서버를 관리했습니다.

카카오톡 / 카카오페이앱 각각 마이크로서비스로 존재하던 홈
카카오톡 / 카카오페이앱 각각 마이크로서비스로 존재하던 홈

2023 개편 이후

2023년 4월부터 점진적으로 배포하기 시작하여 2023년 6월, 카카오톡과 카카오페이 앱 모두 통일된 UI를 만날 수 있도록 전면적으로 카카오페이 홈이 개편되었습니다.

통합된 홈
통합된 홈

통합된 UI를 보여준다는 점과, 기존 두 가지 서버를 동시에 유지보수하는 것이 효율적이지 않다고 생각해 새로운 카카오페이 홈 마이크로서비스를 구성했습니다.

통합된 마이크로서비스
통합된 마이크로서비스

개편된 홈은 다음과 같은 특징을 가지고 있습니다.

Server Driven UI 적용

개편된 카카오페이 홈은 유저의 앱 버전과 무관하게 최대한 동일한 유저 경험을 제공할 수 있도록 Server Driven UI 기반으로 설계되었습니다. 클라이언트 개발자분들과 일관된 JSON 인터페이스를 정의하고, 해당 인터페이스에 맞춰서 응답을 할 경우 정의된 대로 UI가 보이도록 했습니다.

Server Driven UI 매핑 예시
Server Driven UI 매핑 예시

서버에서 가능한 모든 정보를 제어하도록 설계했고, 콘텐츠들의 순서나 노출 여부 그리고 각 콘텐츠에 노출되어야 하는 텍스트 등이 모두 서버를 통해 제어되고 있습니다. 당시에는 결제탭에서 활용하고 있는 클라이언트실의 SDU 시스템이 없었기 때문에 홈의 특성에 맞춰 직접 스펙을 정의하고 구현했습니다.1

실시간 유저 데이터 서빙

개편된 카카오페이 홈은 실시간 데이터를 기반으로 유저에게 이로운 정보를 보여줄 수 있게 설계되었습니다. 또한 홈의 전체 콘텐츠를 Server Driven UI로 구현하기 위해 단일 API로 구성했습니다. 따라서 단일 클라이언트 API 호출에 대해 수많은 연동 API 호출을 통해 정보를 조합해야 하는 서버입니다. 글을 쓰는 현재 연동되어있는 서버는 24개, 평균적으로 유저 1명의 홈을 그려주기 위한 API 호출은 50회에 육박합니다.

많은 연동을 가진 홈
많은 연동을 가진 홈

초기 아키텍처

레이어드 아키텍처 차용

com/kakaopay/home/

├── controller/
│   └── CardController.kt
├── service/
│   ├── MoneyCardService.kt
│   └── client/
│       ├── MoneyClient.kt
│       └── SecuritiesClient.kt
└── repository/

처음 서버를 개발할 때에는 연동을 하기 위해 협의해야 하는 지점도 많았고, 개발해야 하는 양도 많았기 때문에 레이어 단위로 패키징을 한 레이어드 아키텍처 기반으로 개발을 했습니다. 또한 종종 연동 서버에서 받아온 응답을 그대로 클라이언트까지 넘겨줘야 하는 경우도 있었는데, 이때에도 연동 서버의 응답 DTO를 그대로 클라이언트로 보내는 DTO에 재활용하기도 했습니다.
다소 아쉬운 부분이 있었지만, 익숙한 구조 속에서 빠르게 서비스를 개발할 수 있었습니다.

문제점

빠르게 개발을 한 이면에는 여러 문제점도 같이 산재하고 있었는데요.

먼저 프록시를 하는 경우 클라이언트의 UI까지 연동 API 응답에 의존하는 문제가 있었습니다. 앞서 말씀드렸다시피, Server Driven UI 기반으로 설계했기 때문에 클라이언트는 서버에서 응답하는 UI를 그대로 그리는 구조인데요.
연동 API 응답의 스펙이나 값이 바뀔 경우, 값을 검증할 기회 없이 클라이언트로 전달되어 UI가 망가지거나 노출되지 않는 문제가 발생했습니다.
또한 첫 서비스 오픈을 위한 개발이다 보니 연동 API 스펙에 있어 변화도 자주 발생했는데요, API 스펙이 한번 변할 때마다 해당 연동 API 응답을 다시 Server Driven UI 응답으로 변환하기 위해 많은 공수를 들여야 했습니다.

연동 API 인터페이스 도입

문제점을 해결하기 위해 가장 먼저 한 작업은 홈 연동 API 인터페이스라는 것을 정의한 것입니다.

보통 서버 간 연동이 필요한 경우는 다른 부서 실시간 정보를 불러와 아래와 같은 피드 형태로 변환하는 경우입니다.

피드 구조
피드 구조

개선 이전에는 기존에 존재하던 API의 응답 값을 홈 서버의 로직으로 처리하여 해당 카드 형태로 변환하고 있었습니다. 가령, 페이포인트 카드를 예로 들어 보면 아래와 같았습니다.

페이포인트 변환 예시
페이포인트 변환 예시

이 경우 연동 API 스펙이 변경되면 필연적으로 홈 서버에서도 반영을 위한 작업을 해야 합니다. 특히 전사적으로 큰 기술 변경이 있을 때마다 홈 서버는 연동하는 모든 부서의 응답 변경에 대응해야 하는 경우도 발생할 수 있습니다.

이를 해결하기 위해 연동 부서에서 특정 스펙에 맞춰 응답을 주면 클라이언트에 그대로 띄워줄 수 있는 API 스펙을 만들었습니다. 홈 서버에서는 어드민에 등록되어 있는 정보와 해당 연동 API 응답을 교차 검증, 활용하여 카드 응답을 생성하고, 실질적인 API 응답의 콘텐츠는 연동 부서에서 담당하도록 역할을 위임했습니다.

이렇게 책임이 분리되니, 다른 부서에서 보여주고 싶은 콘텐츠가 변경되어도 홈 서버의 작업 없이 연동 API 서버만 배포하면 되는 형태를 갖출 수 있었습니다.

연동 API 인터페이스 적용 예시
연동 API 인터페이스 적용 예시

Hexagonal Architecture 도입

도입 결정의 배경

기존 아키텍처는 다음과 같은 문제가 있다고 생각했습니다.

  • DTO에 대한 분리가 잘 이뤄져 있지 않았습니다. 연동 WebClient와 응답 Controller에서 같은 클래스를 사용하기도 했습니다.
  • 연동 API 스펙이 변경될 경우 핵심 비즈니스 로직까지 변경이 전파됩니다.

이것을 개선하기 위해 도입한 것은 Hexagonal Architecture였습니다.
홈 서버에서 다루는 연동 API는 다양하고 그 수가 많았습니다. 이를 각기 다른 out Port로 추상화하여, Hexagonal Architecture의 장점을 활용할 수 있을 것으로 기대했습니다.

구조와 구현 방식

구현 과정에서는 좀 더 엄격하게 의존성 방향을 강제할 수 있도록 구현했습니다. 이를 위해 멀티 모듈을 적용했는데요. 기존 코드는 상술한 바와 같이 레이어 단위로 패키징 된 단일 모듈 구조의 프로젝트였습니다.

단일 모듈 구조에서는 레이어 간 접근 제어를 관리하기 어렵다는 점, 그리고 Kotlin에서는 특히나 package private을 지원하지 않는다는 점 때문에 Hexagonal Architecture를 적용하며 멀티모듈로 분리했습니다.
분리한 패키지와 모듈의 구조는 다음과 같았습니다.

com/kakaopay/home/

├── api/
│   ├── adapter/
│   └── controller/
├── domain/
│   ├── port/
│   └── service/
└── infrastructure/
    ├── adapter/
    ├── repository/
    └── client/

각 모듈별 역할은 다음과 같이 배치했습니다.

  • api: domain의 in Port를 통해 반환받은 도메인 객체를 API 응답에 맞는 형태로 변환, Spring Boot 서버 구동 역시 담당
  • domain: Port와 해당 Port들을 활용하여 도메인 객체를 생성
  • infrastructure: 외부 API 연동 담당, 연동 API 응답을 out Port에서 지원하는 형태로 변환

카카오페이 홈에 적용한 Hexagonal Architecture
카카오페이 홈에 적용한 Hexagonal Architecture

초기에 느낀, 기대했던 이점

Hexagonal Architecture 적용 이후에는 연동 부서에서 API 응답을 변경하거나 혹은 연동해야 하는 API 자체가 달라지더라도 infrastructure 모듈만 수정하여 in Port 인터페이스에만 맞추면 기존 도메인 로직은 변경할 필요 없이 로직을 지켜낼 수 있었습니다. 도메인 로직의 재사용성을 높이면서도, 외부 세계의 변동에 대해 빠르게 대응할 수 있을 것이라는 기대감에 부풀었죠.

운영 과정에서 드러난 문제점

연동 API 인터페이스와의 모순

상술한 대로 연동 부서의 변경에 직접적인 영향을 받지 않기 위해 연동 인터페이스라는 API 스펙을 정의하여 운영해오고 있습니다. 그리고 기존에 연동 인터페이스가 아닌 API 연동에 대해서는 신규 추가를 Deprecate하고, 만약 기능 변경이 필요하면 연동 인터페이스를 준수하여 구현하실 수 있게 커뮤니케이션하고 있습니다. 이 덕분에 홈 서버는 이미 연동 부서에서 노출하고 싶은 콘텐츠가 변동된다고 하더라도 별도의 작업 없이 대응할 수 있는 구조가 되었습니다.

애초에 연동 부서의 변경으로부터 독립될 수 있는 방법론을 API 응답 스펙 정의를 통해 해결하다 보니 코드 레벨의 아키텍처를 통해 변경에 쉽게 대응하는 것의 효용이 떨어진다고 느꼈습니다.

모호한 레이어 간의 경계

처음 Hexagonal Architecture를 적용해야겠다고 고려할 때, 도메인에 대한 정의는 크게 고려하지 않았습니다. Hexagonal Architecture는 구조적으로 Port와 Adapter를 활용해 코드를 구성하고, 핵심 비즈니스 로직이 외부의 의존성에 신경 쓰지 않을 수 있도록 의존성 역전을 적용하는 것이 핵심이라고 생각했기 때문입니다.

실제로 적용 초기에는 핵심 비즈니스 로직을 변경하지 않고도 연동 부서의 변경에 대응하는 등 아키텍처의 장점을 체감할 수 있었습니다.

하지만 도메인에 대해 강하게 정의하지 않다 보니 결론적으로 Port, 특히 out Port의 경계가 모호해지기 시작했습니다.

out Port에선 어떤 정보를 어떤 기준으로 받아와야 할까요? Hexagonal Architecture 적용 초기에는 기존에 사용하던 WebClient의 응답을 기준으로 Port를 작성했습니다. 그리고 이후에 외부 연동이 변경될 때마다 해당 Port의 인터페이스에 Adapter의 로직을 바꿔가며 Port의 변경을 일으키지 않으려고 노력했습니다.

지켜낸 Port의 인터페이스는 과연 최선의 인터페이스일까요? 어쩌면 더 간결한 형태가 될 수도 더 최적화될 수도 있을 것입니다. 하지만 Port와 핵심 비즈니스 로직을 지켜낸다는 것에 집중하여 작업을 하면 이러한 Port의 개편은 일단 제쳐두고 작업을 하게 됩니다. Port의 변경은 나중으로 미루는 것이죠.

이 부분에서 미스매치가 발생하는 경우가 많았습니다. 예를 들어, 기존의 Port와 핵심 비즈니스 로직이 애초에 연동 API의 스펙을 지원하기 위해 비효율적으로 되어 있었던 경우, 연동 부서에서 저희를 위해 효율적인 API를 구성해 주더라도 그 이점을 활용하지 못하는 경우가 발생했습니다.

이런 부분을 피해 가기 위해선 애초에 Port의 인터페이스부터 변경해야 했고, 결론적으로 외부의 변경이 비즈니스 로직에는 영향을 주지 않는다는 Hexagonal Architecture의 이점을 누리지 못하는 경우가 자주 생겼습니다.

애매한 도메인 모델

이런 문제를 피하기 위해선 도메인에 대한 정의가 필수적이라는 결론에 다다르게 되었습니다. 우리 서비스에서 다루는 도메인에 의해 Port의 인터페이스가 정의된다면 이러한 혼동을 피할 수 있을 것이라는 생각이 들었기 때문입니다.

그렇다면 홈의 도메인은 무엇이라고 생각해야 할까요? 모델의 관점에서 생각했을 때에는 개인화된 유저의 실시간 정보로 생각해야 할 것입니다. 이 부분에서 또 쉽지 않은 부분이 생겼는데요, 홈 서버의 대부분 비즈니스 로직은 외부 연동 서버로부터 실시간 정보를 받아와서 Server Driven UI에 맞춰 렌더링 하는 것이 대부분이었기 때문입니다. 이 로직은 개인화된 유저의 실시간 정보를 ‘활용’해서 잘 ‘보여주는’ 것에 집중하는 로직이라고 볼 수 있습니다. 애초에 우리가 핵심 비즈니스 로직이라고 생각하고 작성했던 부분들이 도메인 로직과는 거리가 멀고, 오히려 in Port의 로직에 가까워지는 것이죠.

도메인 모델을 정의하는 것도 그를 통해 경계를 정리하는 것도 어려웠습니다.

결국 외부에 의존

위에서 말한 내용들을 보면 홈 서버는 다른 서비스로부터 실시간 유저 데이터를 조회하여 가치 있는 형태로 변환해 보여주는 것에 집중하고 있습니다. 따라서 외부와의 의존성이 어쩔 수 없이 굉장히 큰 서버입니다.

위에서 정리한 도메인에 대한 기준으로 볼 때는 저희가 개발해야 하는 코드도 도메인 로직에 몰려 있다기 보단, 외부의 정보를 렌더링에 맞는 DSL로 변환하는 데에 크게 집중하고 있죠.

외부에 대한 의존성이 애초에 큰 서버에 연동이 많다는 이유로 Hexagonal Architecture를 적용하는 것은 오히려 아키텍처의 장점을 잘 가져가지 못한다는 생각이 들었습니다.

신규 기능 개발 시의 비효율성

연동 인터페이스의 적극적인 도입에 힘입어 연동 API의 변경이 홈 서버의 작업으로 이어지는 빈도는 급격히 줄어갔습니다. 반면 홈 서버의 신규 기능 개발 빈도는 점점 증가했습니다.

신규 기능의 개발은 곧 전체 레이어에 대한 변경을 의미했습니다. 다음의 변경이 대부분 필수적으로 발생했습니다.

  • 도메인 레이어에 로직 추가/변경
  • 도메인 레이어에서 사용할 수 있는, 사용할 out/in Port 인터페이스 추가/변경
  • 해당 Port들의 인터페이스에 맞춘 Adapter 개발

이 Port에 값을 제공해 줄 클라이언트로부터 값을 받아 처리하려면 다음의 과정을 거쳐야 했습니다.

변환 구조도
변환 구조도

신규 기능을 개발할 때에는 이런 구조를 가진 여러 파일에 Domain -> Port -> Adapter 순으로 작업을 해야 했습니다. 또한 각각의 레이어가 가진 DTO, 그리고 매핑 로직까지 모두 작업을 해야 했죠.

카카오페이 홈 서버는 개편을 진행한 2023년 기준, 연동 서버 23개 대상으로 40개 이상의 API를 호출하고 있었습니다. 그리고 각각의 케이스마다 다른 기능이 사용되었기 때문에 40개 이상의 out Port가 존재했는데요.
신규 기능을 빠르게 개발해야 하는 상황에서 도메인 레이어부터 시작해서 다수의 out Port, DTO, 그리고 매핑 로직을 수정해야 했습니다. 이러한 작업은 시간이 지나 Port가 많아질수록 점점 더 큰 비용이 되어갔습니다.

단일 in Port

카카오페이 홈 서버는 현재로선 HTTP API를 제공하는 컴포넌트 하나만 구동이 되고 있습니다. 다시 말해 도메인이 있다고 하더라도 해당 모듈을 재사용하는 모듈이 없다는 것이죠. 물론 처음 Hexagonal Architecture를 적용할 때에는 같은 도메인 로직을 활용하는 유저의 데이터를 미리 적재하는 Consumer 컴포넌트가 추가될 수도 있겠다는 생각을 했습니다. 하지만 운영하는 시간이 길어질수록 만약 Consumer 컴포넌트를 만든다고 하더라도 Consumer에서 활용해야 하는 로직과 API 서버에서 활용해야 하는 로직이 각자의 책임에 따라 크게 차이가 날 것이라는 생각이 들었습니다.

따라서 도메인 모듈의 재사용성이 사실상 크게 떨어지는 것이고, 모듈을 재사용하지 못한다면 Hexagonal Architecture를 적용하는 의미도 크게 떨어진다고 판단했습니다.

팀 온보딩 및 코드 관리 비용 증가

팀에 새로운 동료가 생길 때마다 홈 서버의 아키텍처를 설명하는 부분도 점점 어려워졌습니다. Hexagonal Architecture라는 설명으로는 부족할 정도로 하는 기능에 비해 코드의 사이즈가 비대해진 나머지 하나의 기능이 어떻게 동작하는지 설명하기 위해 많은 비용을 들여야 했습니다.

또한 Hexagonal Architecture에 대해 팀원들이 이해한 방향이 조금씩 다른 경우가 많아서 구조적인 강제성을 띄어야 할 아키텍처가 오히려 더 많은 다양성을 만들어냈습니다. 결과적으로 안 그래도 많아진 파일에 산발적으로 로직이 흩어지며 관리 비용이 증가했습니다.

Hexagonal Architecture 제거

제거를 결심한 이유

여러 이유를 종합해 본 결과, 카카오페이 홈 서버의 현재 상황에서는 Hexagonal Architecture가 제공하는 구조적 장점보다, 운영과 개발 과정에서의 비효율성이 더 크게 느껴졌습니다. 앞으로도 여러 변화가 예상되고, 유저에게 더 나은 가치를 제공하기 위해서라면 어떤 형태로 변화될지 예상할 수 없는 프로젝트였기에 Hexagonal Architecture를 제거하기로 결정했습니다.

제거 과정

제거 과정에서는 다음의 부분들을 고려했습니다.

관심사 단위 패키징

최초 개발 시 적용했던 레이어 단위 패키징으로 돌아가는 것이 아니라, 관심사 단위 패키징을 적용하기로 했습니다. Hexagonal Architecture에서 도메인 모델로 인식했던 것들을 기준으로 패키징을 적용하여 로직을 담당하는 코드를 더 빨리 탐색할 수 있게 했습니다.

단일 모듈 구조로 회귀

관심사 단위 패키징을 적용하면서 Controller, Service, Infrastructure 관련 의존성을 하나의 패키지 하위에 위치하는 것이 자연스럽겠다는 생각을 했습니다. 따라서 멀티 모듈을 단일 모듈로 합치는 작업을 가장 먼저 수행했습니다. 기존 Hexagonal Architecture를 적용하면서도 Component Scan을 지원하기 위해 Annotation만 gradle 의존성으로 가지고 있었던 부분들이 자연히 해결되었고, 의존성 간 버전이 어긋나는 것 역시 자연히 해결되었습니다.

Port와 DTO 처리 방식 재설계

Port를 통해 받아오던 DTO 로직을 통합하고 매핑 로직을 제거하는 작업은 굉장히 크기가 큰 작업이었습니다.
의존성의 관점으로 볼 때, 기존 로직에서 사용하는 필드들은 모두 Port의 인터페이스에 정의되어 있는 DTO에 있을 것입니다. 그리고 Port에 맞춰 매핑하기 위한 WebClient의 DTO는 JSON 이름을 기준으로 작성되어 있을 것입니다.
이 DTO들과 Adapter에서의 변환을 통합하기 위해 Port의 DTO를 기준으로 가장 필요한 필드들만 남기고, WebClient의 DTO를 기준으로 필드의 이름을 설정했습니다. 그리고 기존에 있던 변환 로직의 경우 Port를 사용하던 Service가 처리하도록 했습니다.

통상적으로 많이 사용하는 접근이지만, Port와 Adapter, 그리고 Adapter가 사용하는 WebClient로 책임이 분리되어 있던 것을 다시 통합하는 과정이다 보니 이러한 기준이 필요했습니다.

Controller와 Infrastructure 간 DTO 분리 유지

Hexagonal Architecture를 적용했을 때 느꼈던 엄격한 레이어 간 DTO 분리는 시스템의 안정성을 높이는 데에 크게 기여했음이 틀림없었습니다.

따라서 클라이언트에 연동 API의 응답을 그대로 응답하더라도 Controller와 Infrastructure에서 사용하는 DTO를 분리하는 것은 엄격히 유지했습니다. 또한 데이터베이스 접근과 연동 API에서 활용되는 Image, Link 등의 응답에 대해서도 공통으로 활용하지 않고 모두 분리했습니다. 중복 코드로 볼 수도 있지만, 조직의 관점에서 각각 서로 다른 조직과 협업하는 것이기 때문에 벽을 치듯 최대한 분리했습니다. 이를 위해 응답에 대한 DTO 클래스를 Namespace로 인식하고 Nested Class를 활용하여 혼동을 최소화했습니다.

제거 후의 변화

Hexagonal Architecture를 제거한 이후 현재까지의 패키지 구조는 아래와 같습니다.

com/kakaopay/home/

├── cards/
│   ├── controller/
│   └── service/
└── card/
    ├── money/
    │   ├── service/
    │   └── infrastructure/
    ├── shortcut/
    │   ├── service/
    │   └── infrastructure/
    └── feed/
        ├── point/
        │   ├── service/
        │   └── infrastructure/
        ├── money/
        │   ├── service/
        │   └── infrastructure/
        └── mydata/
            ├── service/
            └── infrastructure/

관심사별로 구분되어 있는 레이어드 아키텍처의 패키지 구조를 따라가고 있습니다. 큰 변화가 없는 것으로 느껴지실 수도 있지만, PR 기준 8000줄 이상의 코드가 줄어들었습니다. 줄어든 코드의 양만큼 로직을 파악하기 편해졌고, 파악하기 편해진 만큼 신규 기능을 추가하는 데에 걸리는 시간도 줄어들었습니다.

Hexagonal Architecture 제거 이후 곧바로 Controller부터 Infrastructure까지 전체 레이어에 추가 개발을 해야 하는 과제를 진행했는데요. 카카오페이 증권과 협업이 필요한 과제여서 컴플라이언스 이슈, 네트워크 프록시 이슈 등 다양한 이슈 아래에서 개발 기간이 굉장히 촉박했음에도 불구하고 필요한 부분만 효율적으로 변경하여 무리 없이 기능을 릴리즈할 수 있었습니다.

Hexagonal Architecture에 적합한 프로젝트

카카오페이 홈보다 Hexagonal Architecture가 적합하다고 생각하는 프로젝트의 특징은 다음과 같습니다.

도메인 모델을 확실하게 정의할 수 있는 서비스

도메인 모델이 명확하지 않은 경우, Hexagonal Architecture의 핵심 요소인 Port와 Adapter의 경계를 설정하는 데에서 어려움이 발생할 수 있습니다.

따라서 하나의 프로젝트 내에서 관리해야 하는 도메인 모델을 명확히 정의하는 것이 Hexagonal Architecture 적용의 시작이 되어야 한다고 생각합니다. 더불어 Hexagonal Architecture에 있어 DDD가 필수는 아니지만, 그에 준하게 필요하다는 생각 역시 들었습니다.

외부 의존성이 많지 않은 서비스

의존성이 많지 않다는 것은 깊이에 대한 부분이 아니라 넓이에 대한 부분입니다. 로직의 대부분이 연동 API에 의해 동작하는 서비스일 경우 결론적으로 Port와 Adapter에 로직이 과중되는 경우가 많아질 확률이 높다고 생각합니다. 따라서 외부에 대한 의존성보다 코어 로직이 풍부하게 존재하는 서비스일 때 의미를 가지게 된다고 생각합니다.

이는 위에서 말한 도메인 모델을 확실하게 정의할 수 있단 의미와 거의 비슷한 것 같기도 합니다.

코어 모듈을 사용하는 모듈이 2개 이상인 서비스

코어 모듈을 2개 이상의 서비스가 사용하지 않으면 out Port에 해당하는 부분만 추상화하는 것이 더 나은 방향이 될 수 있다고 생각합니다. 코어 모듈의 로직을 재사용할 수 있도록 해당 모듈을 사용하는 컴포넌트 모듈이 2개 이상인 서비스 일 때 Hexagonal Architecture를 고려해봐야 한다고 생각합니다.

Hexagonal Architecture 적용을 고민하는 분들에게

Hexagonal Architecture 역시도 상황 상 어울리는 프로젝트와 그렇지 않은 프로젝트가 있음은 확실한 것 같습니다.

구조적인 이점만을 보고 적용하기엔 추후 더 많은 고민과 헤쳐나가야 하는 부분이 많은 아키텍처이니, 그것의 기준이 될 수 있는 도메인적인 이해도가 높은 상황에서 적용하시는 것을 추천드립니다.

마치며

위의 여러가지 이유를 들어 카카오페이 홈 서버에서는 Hexagonal Architecture를 제거했습니다.

은탄환이 없다는 말, 소프트웨어를 개발하면 어느 상황에서든 적용되는 말이라고 생각하는데요.

Hexagonal Architecture는 좋은 총과 저격수 그리고 알맞은 환경이 주어지면 은탄환에 가까울 수 있겠지만, 적어도 카카오페이 홈에 있어서는 은탄환이 아니었습니다.

Hexagonal Architecture를 적용할지 고민이 되신다면 카카오페이 홈의 제 사례를 보고 한번쯤 재고하는 데에 도움이 되었으면 좋겠습니다.

감사합니다.

각주

Footnotes

  1. 결제탭의 SDU 시스템과 아키텍처에 대해선 이 글을 참조해주세요!

dory.m
dory.m

카카오페이 서버 개발자 도리입니다. 설계 관점에서 나무보다 숲을 보는 것을 더 즐기지만, 숲을 구성하는 나무들의 중요성을 역시 잊지 않고자 노력하고 있습니다.