1. Introduction
📌 Topic
헥사고날 아키텍처를 따로 깊게 배운 적이 없는데, 요새 내가 즐겨쓰는 구조가 헥사고날 아키텍처라고 책이 말해주더라.
띠옹, 나는 그 유명한 육각형 그림 말고는 본 적도 없는데 이게 무슨 일이지.
좋은 소프트웨어 설계에 대해 끊임없이 고민해본 개발자라면, 아마 나와 비슷한 경험을 겪어봤을 이가 적지 않을 것이다.
Atomic design pattern, FSD, MVC, MVVM, 헥사고날 아키텍처, SOA, DDD 같은 용어 따위에 매몰되지 않아도, 여러 이슈들을 부딪혀가며 깎고 깎고 깎다보면 다들 비슷한 형태로 귀결된다.
이 정도까지 오면 선호도에 따른 차이가 대부분인데, 이건 원칙을 어떻게 해석하고 적용하냐의 차이에서 비롯하는 듯하다.
가끔 보면 이게 경전 해석이 달라서 종교 전쟁 발발하는 거랑 뭐가 다른가 싶다.
📌 Before We Begin
헥사고날이나, 여러 아키텍처 방법론을 가져와서 "이걸 적용해보려 하는데 어떤가요?"라는 질문을 종종 받는데, 공부해보고 싶은 거면 자유롭게 하면 되고, 다른 할 일이 넘쳐나는데 하는 건 권장하지 않는 주의다.
애초에 이런 아이디어들은 소프트웨어가 점점 비대해지면서 유연하고 확장 가능한 구조를 갖추기 위해 나온 일종의 Best Practice들인데, CRUD만 왕창 찍어내다가 몇 개월 후 폐기시켜버릴 소프트웨어에 굳이 그런 게 필요할까.
유연성이나 유지보수 같은데 고민할 시간을 버리는 게 문제 해결 경험 소스 뽑아내고 싶은 취준생 입장에서도 훨씬 낫다.
경험이 부족할 수록 완벽한 아키텍처에 대한 환상을 품게 된다. (내가 그러했듯)
3 layer architecture보다는 헥사고날 아키텍처가 더 낫고, MVC보다는 MVVM이 더 좋으며, 단일 모듈보다는 멀티 모듈, 모놀리식보다는 MSA가 더 완벽하다는 착각을 하곤 한다.
그리고 이런 완벽한 구조(?)의 소프트웨어는 개발 속도를 증진시키고, 개발자로서의 역량을 과시하는데 지대한 공헌을 할 것이라 스스로를 기망하지만, 실상은 조금 다르다.
구조가 복잡해질 수록 따라오지 못하는 팀원은 늘어나 개발 속도가 지연되기도 하고, 배포 전략과 프로세스는 점점 복잡해져서 관리 비용이 증가하는 등.
처음 예상과는 달리 오히려 단점이 장점을 넘어서는 일도 허다하다.
그리고 경영진 입장에서 좋은 소프트웨어를 만들기 위한 개발팀의 노력을 마냥 좋은 시선으로 보긴 어려울 것이라는 입장이다.
그들에게 소프트웨어란 돈을 벌어다 주는 장치인데, 아키텍처를 개선하는 작업은 새로운 가치를 창출하는 행위는 아니기 때문이다.
물론 중·장기적으로 봤을 때 도움이 되긴 하겠지만, 이를 위해 인력을 분산시켜야 하며, 릴리즈 텀이 늘어져서 단기적으로 큰 피해가 발생할 수 있다는 점을 감안하면, 기술적 성공에 매몰된 엔지니어들의 한계라고 치부하는 것을 이상하다고 보기 어렵다.
여기서 끝마치면 아키텍처 회의론자처럼 보일 듯한데, 이렇게 말은 했지만 나도 좋은 소프트웨어를 만들려는 이들을 좋아한다.
그러나 당신이 진정 엔지니어라면, 제 수준에 맞는 아키텍처를 고려하는 것까지도 당신의 책무임을 잊어선 안 된다.
코딩은 아름답다는 측면에서 예술과 비슷하지만, 근본적으로 예술과는 다르다.
단순히 아름답다는 이유로 과도한 아키텍처를 도입하는 것 또한 오버 엔지니어링이다.
지금부터 할 얘기는 엔지니어로서의 책무를 잊었던 과거의 자아 성찰이다.
(백/프론트 구분을 짓지 않고 시간 흐름 순으로 나열해둔 터라 혼란스러울 수 있다.)
2. Experience
📌 MVC

다들 개발을 시작하면 인프런 영상을 많이 봐서 그런지, Controller, Service, Repository는 기본적으로 나누고 시작하는 것 같다.
하지만 나는 취미로 코딩하던 시작한데다, 웹 개발을 독학으로 시작했던 터라 View 하나에 모든 것을 집어넣었었다.
html에 모든 로직을 집어넣고 github으로 배포하면 이렇게 만들 수 있다.

머리가 조금 굵어지면 js를 html에서 분리할 수 있다는 엄청난 사실을 알 수 있는데, 알고 쓴 건 아니지만 사실상 Controller 정도는 분리한 셈이다.
정말 장족의 발전이라 할 수 있다.

이후 django로 본격적으로 개발을 시작하면서 처음으로 MVC 패턴이라는 것을 배웠었다.
사실상 DB 테이블과 1:1로 매핑된 데이터 객체나 다름 없었고, 여전히 모든 비즈니스 로직은 Controller가 가지고 있었다.
📌 Divide Views
1달 정도 후에 DRF(Django REST Framework)라는 것을 배우면서, view는 react로 분리하는 작업을 수행했다.

frontend, backend라는 개념조차 처음 들어보던 때였고, js를 분리한 게 개념적으로나마 service를 분리한 것을 몰랐던 때라, 프론트엔드는 다시금 page 단위의 view로 모든 것이 묶이게 되었다.
이렇게 개발하다보니, 이럴 거면 바닐라 js 쓰지 뭐하러 react를 써야 하는지 이해가 안 갔었다.

그런데 당시 다른 분이 UI 작업을 하기 위해 설계를 하는 방식이 독특했었다.
하나의 View를 한 곳에 몽땅 구현하는 것이 아니라, View를 구성하는 영역을 잘게 쪼개어 분할하는 식으로 구현을 하고 있던 것이다.
당시에 알고리즘 문제 푸는 맛에 중독되어 있던 터라, '어라, 이렇게 하면 tree 구조로 만들어서 중복 node 제거가 되겠는데?'라는 아이디어로 이어졌었다.

그러나 겉모습만 보고 자신감있게 도입을 했던 패턴은 금새 지옥으로 돌변했다.
하나의 Page를 의미하는 View와 View를 구성하는 각각의 Presentation이 무분별하게 API를 호출하기 시작했다.
다행인 점은 해당 프로젝트는 오래 안 가 폐기되는 덕에 문제가 있었다는 사실조차 몰랐다는 것이고,
불행인 점은 빨리 얻어맞고 배울 수 있었던 내용을 놓쳤었다는 것이다.
웃긴 건, 위 구조의 문제점을 알게 된 건 웹 개발이 아니라, 이후로 알고리즘을 더 딥하게 공부하면서 알게 되었었다.

View가 state를 갖는다는 옥에 티는 잠시 넘어가고, 가장 큰 문제는 각 Presentation이 경계를 무시하고 API를 직접 호출하여 state를 가지고 있다는 점이었다.
View의 중복 Presentation을 memoization하고 싶었다면, 각 Presentation은 referential transparent function이어야 했다.
쉽게 말해, input이 고정되어 있을 때, 언제나 output이 동일한 꼴로 만들었어야 했다는 것이다.
📌 3 Layer Architecture

이후 Spring Boot로 넘어오면서, 처음으로 3 Layer Architecture라는 것을 접했다.
왜 하는 건지는 모르겠고, 그냥 이렇게 분리하라길래 분리한 게 전부였다.
Controller와 Repository의 목적은 얼추 이해가 되었으나, Service가 담당하는 Business Logic이라는 건 당췌 이해가 안 갔었다.
그래서 그냥 Controller는 router같은 거고, DB랑 통신할 때는 Repository 쓰고, Entity는 여전히 테이블 정보 가져오는 값 객체이며, 나머지 로직은 몽땅 Service로 쑤셔넣으면 된다고 이해했었다.
📌 DTO (Data Transfer Object)

DTO라는 게 무엇인지는 진즉 알고 있었지만, Entity 몽땅 넘겨주는 것이 편한데 굳이 왜 써야 하는지는 모르는 상태였다.
그래서 사용자 Entity처럼 보안에 문제가 있을 법한 것들은 제외하고, 나머지는 서버에서 Entity를 통채로 넘겨줘버렸다.
이 쯤에는 앱 개발이 진행되고 있었다. (웹 프론트 개발은 중단된 상태)
모바일 단에서는 MVVM 패턴으로 구현하기로 이야기가 끝난 상태였다. (사실 글 쓸 때 MVC로 착각하고 있었다. Controller로 죄다 써버렸는데, 사진 다 고치기 귀찮아서 냅뒀다.)
다만 해당 팀에서 서버의 응답을 매번 Map 타입으로 받은 후에 문자열로 필드 값을 key로 입력해 여기저기서 추출하고 있었기에, "API 응답을 DTO로 받는 것은 어떻냐"라는 말을 해서 개선을 했었다.
문제는 이전처럼 필요한 시점에 필요한 데이터만 추출하는 게 아니라 API 응답 스펙과 완전히 일치하는 DTO가 등장하면서, 내가 멋대로 API 응답 스펙을 변경하면 앱 장애로 이어지는 상황이 되었다.
그래서 서버의 응답도 DTO로 수정을 함으로써 Entity의 수정이 곧장 API 응답에 반영되지 않도록 막고, API 버저닝을 통해 문제를 해결했다.
그러나 Controller는 단순히 routing만 하는 역할이라 단정지었던 시기였고, 의존 방향성의 중요성도 몰랐던 때라, Entity와 DTO 매핑을 Service 단에서 수행해주고 있었다.
📌 Two Depth Service

2023년 9월에 "Service Layer 분리에 대하여"라는 포스트를 작성했었다.
여느 학생들이 그렇듯, 나 또한 entity를 기준으로 모든 패키지를 분리해둔 상태였다.
그러나 이 방식은 Service가 하나의 entity만으로 비즈니스 로직을 완성할 수 없을 때 문제가 되었는데, 예를 들어 게시글과 댓글을 함께 조회할 때는 PostService가 PostRepository와 CommentRepository를 모두 의존해야만 했다.
위 방식은 싫었으나, 그렇다고 대등한 계층의 Service 간 의존을 허용하면 순환 참조의 우려가 있었다.
그래서 controller는 feature를 기준으로 분리하고, repository는 table을 기준으로 분리한 뒤, 이를 조율(orchestration)하는 service 로직을 만들어 2-depth 계층으로 구성했었다.
1depth Service에도 비즈니스 로직이 포함되어 있었고, Controller는 여전히 N개의 DomainService를 의존하고 있었기에 Facade Pattern과는 다른 상황이었다.
📌 Multi Module Migration

기능을 개발하면서 점차 구조가 복잡해지기 시작했다.
Batch 애플리케이션이 등장하면서 중복 코드가 나오기도 하고, web 컴포넌트도 코드량이 너무 많아졌다.
그래서 멀티 모듈로 마이그레이션 하겠다는 결정을 내렸는데, 그 탓에 "프로젝트 멀티 모듈화 고찰"이라는 포스트가 탄생하게 되었다.
그러나 지금 돌이켜보면 이건 잘못된 판단이었다.
아키텍처가 복잡하고 더러웠던 것은 싱글 모듈에서도 충분히 개선할 수 있었던 사안이었다.
멀티 모듈은 하나의 구현 방식일 뿐, 아키텍처와는 별개의 내용이었으니까.
하지만 그런 사실을 몰랐던 때라 그냥 했다.
멀티 모듈로 만들겠다는 동기 자체는 틀려먹었을지라도, 이 작업을 하면서 온갖 아키텍처 개괄을 독파했었고, 본격적으로 좋은 아키텍처가 무엇인지 볼 수 있는 시야가 트이는 계기로 작동했었다.
프로젝트 입장에선 실패지만, 개인 학습 목적으로는 온갖 피나는 경험을 다 할 수 있었달까.

우선 web -> domain 모듈로 의존성 방향이 강제되면서, Service가 DTO를 의존하는 구조를 끊어냈다.
그리고 진정한 Facade 패턴을 구현해보겠다고 UseCase를 클래스를 만들어, 메서드만으로 호출 Service를 구분하도록 만들었다.
UseCase의 책임을 최대한 빼앗고 싶어 Mapper를 만들어 DTO <-> Entity 변환을 하도록 구성했다. (그런데 귀찮아서 DTO -> Entity는 그냥 DTO가 알아서 하도록 만드는 게 일상다반사)
물론, 위 방식은 2 depth Service 구조의 1 depth 역할을 UseCase가 떠맡았을 뿐이라 기존의 문제점을 그대로 가지고 있었지만, 이 때까지는 모르고 살았었다.

아무튼 외부 인프라와 통신하는 곳은 Infrastructure 모듈로 분리하여 중복을 제거한 것은 확실한 성과였다.
여기서 가장 오점은 사용자 측이 최종 응답을 반환하기 위해 Domain과 Infrastructure를 각각 호출하여 조율하는 역할을 떠맡고 있다는 점이다.
이런 설계 미스의 주 원인은 2가지 때문이었다.
- JPA 설정 복잡도로 인해 Repository를 Infrastructure로 분리하기 어려움.
- 애초에 세부 사항인 JPA가 아키텍처 발전을 막는 아이러니한 상황이다.
- 당시 기술적 역량이 부족했던 터라, JPA의 entity scan이나, JPA 기능을 확장해놓은 클래스들을 Infrastructure로 분리할 아이디어를 떠올리지 못함.
- UseCase가 기존 조율자 역할의 Service 책임을 떠맡으면서, 당연히 UseCase에서 이를 조율하는 것이 옳다고 판단함.
- DomainService를 그대로 Facade Pattern으로 분리한 셈인데, 이 때도 아직 비즈니스 규칙이 뭔지 제대로 정의하지 못하던 때라 틀린 것조차 인식하지 못한 상태.
젠장, 지금 보니 끔찍하기 그지없는 구조다.
📌 Test Case
테스트 케이스를 작성하는 개발자가 멋있어 보인다는 이유 하나만으로 공부를 시작했었다.
그 와중에 통합 테스트는 간지가 안 난다는 이유로 Controller와 Service, Repository에서의 Unit/Slice Test 등을 가지각색으로 만들어놨었다. (테스트 커버리지가 중요한 게 아니라, 그냥 멋져 보여서 계속 했었다.)

문제는 UseCase였다.
Controller와 UseCase는 1:1 매핑 관계이므로 딱히 문제가 없었지만, UseCase와 Service가 문제였다.
사실 UseCase는 테스트 대상이 아니지만...나의 잘못된 설계로 인해, 비즈니스 규칙을 검증하기 위해서는 언제나 UseCase와 Service를 함께 검사해야만 했다.
UseCase를 테스트 하다보니, AuthUseCase가 signUp 메서드에서 SignUpService를 Mocking하고 테스트를 작성해놨는데, signOut 메서드를 추가하고 SignOutService를 추가하면 로그인 테스트 mocking 실패로 테스트가 깨져버렸다.

그렇게 탄생한 글이 "Service Layer 분리에 대하여 (2)"인데, UseCase를 완전히 routing 역할만 수행하도록 책임을 빼앗고, 조율자 역할의 Service를 다시 부활시켰다.
덕분에 테스트는 더 이상 막힘이 없어졌지만, 사실 비즈니스 규칙을 담은 모든 DomainService가 web 컴포넌트에 담겨있는 건 문제가 있다.
이 사실을 알게 되는 건 또 한참 후의 이야기..
📌 MVVM + Clean Architecture


이 때쯤 프론트 측에서도 대대적인 아키텍처 개선 작업이 이루어졌다.
연어처럼 플젝을 이탈했다가 돌아온 웹프팀은 FSD 아키텍처를 도입해보고 있었고, 모바일 팀은 그럴 계획은 없는 상황이었다.
24년 6월 쯤, 우리 프로젝트는 선택의 기로에 놓이게 된 적이 있었다.
iOS 앱 심사 탈락 사유에 맞추기 위해 프로젝트 근간을 이루는 비즈니스 규칙을 뜯어 고칠 것이냐, 아니면 SNS 기능을 덧붙여 탈락 사유를 회피하는 도박수를 둘 것이냐.
전자를 택하기엔 프로젝트 초반에 작성해둔 코드는 내 것도 엉망이었기에 당췌 그럴 엄두가 나질 않았었다.
그래서 합의 하에 후자를 택하긴 했으나, 기존 모바일 팀의 아키텍처는 없다고 봐도 무방한 수준이었기에 문제가 심각했다.
비즈니스 로직이 View와 Controller 전반에 흩어져 있었고, 덕분에 기능 하나를 추가/수정하면 여러 기능이 갑자기 동작을 하지 않는 경우도 있었다.
이런 구조로 stateful한 웹소켓 통신을 다룬다..? 안 된다. 지옥길이 훤히 보였다.
전진도 후퇴도 못하는 상황이었기에, 제자리에서 잠시 재정비를 하기로 결정했다.

비록 겉핡기지만 MVVM + Clean Architecture를 공부해 앱을 제작해본 경험을 바탕으로 모바일 개발 팀에 가르친 후, 채팅 도메인 쪽이라도 잘 구분된 아키텍처를 반영하고자 했다.
왜 굳이 MVVM이었냐, 원래 모바일 팀에서 MVVM 적용해서 진행 중이었기 때문이다.

여튼 View는 stateless한 상태로 만들고, ViewModel과 UseCase의 책임을 잘 분산해서 만들면 된다고 알려주고, 구현까지는 간섭하지 않아서 지금도 어떤 상태인지는 잘 모른다.
그런데 적어도 구조를 명확하게 나누고, 책임을 구분해놨더니, 이전처럼 기능 조금 수정한다고 벌벌 떠는 일은 완전히 사라졌다.
2달 정도를 소요해서 채팅 기능을 만들 기반을 다졌었다.
📌 Domain Service's Responsibilities

Domain Service가 web 모듈에 위치하는 게 거슬리기 시작했었다.
핵심 도메인 규칙은 Entity로 수행하고, 애플리케이션 특화 비즈니스 규칙은 Service가 처리하도록 만드는 것을 선호하는 편인데, 정책과 수준의 차이가 있을 뿐 모두 비즈니스 규칙이라고 생각했다.
그런데 현재 구조는 비즈니스 규칙 구현을 위해 Service가 핵심 비즈니스 규칙을 담은 Entity를 넘겨주는 것이 강제되면서, 마음만 먹으면 하위 모듈에서 비즈니스 규칙을 깨트리는 코드를 작성하는 것도 가능했다.
그리고 이러한 비즈니스 로직 유출은 기능 수정을 위해 서칭을 하는 과정에서 "대체 이 기능의 비즈니스 로직은 어디 있는 거야?"라는 상황이 빈번하게 발생했고, 이는 곧 코드의 응집성이 떨어졌음을 의미한다고 판단했다.
그렇게 "도메인 비즈니스 규칙과 멀티 모듈 아키텍처 설계"와 "다중 인프라스트럭처 도메인 모듈 분리를 위한 리팩토링"이라는 포스트가 탄생했다.

DomainService를 일단 Domain 모듈로 옮기고 보니, 더 이상 Domain을 의존하는 모듈에 Entity를 넘겨줄 이유가 없어졌다.
아니, 오히려 Entity를 넘겼다가 하위 모듈에서 실수로 chaining으로 lazy loading되는 객체 조회를 시도하는 등의 오류가 발생할 여지가 있었기에, Service는 언제나 요청에 대한 완성된 응답을 DTO에 담아 반환하도록 만들었다. (망할 JPA)
그렇게 Command/Result 패턴을 고착화시켰다.
그러나 여전히 하위 모듈이 infrastructure을 사용해야 하는 경우가 있었다.
예를 들어, "사용자가 채팅 메시지를 전송하면, 메시지를 저장하고 MQ로 보낸다"는 Domain Service의 역할일 수 있겠지만, "채팅 메시지 요청이 들어올 때마다 로그 수집기에 로그를 전송한다"는 비즈니스 규칙이라고 봐야할까?
단순 시스템 스펙을 위한 규칙이라고 봐야하는 것은 아닐까?
당시에는 이 질문에 대한 매끄러운 답을 내기도 어려웠고, domain 영역에서 모두 처리하자니 모듈 의존 구조가 크게 바뀌어야 하는 작업이라 그냥 넘겨버렸다.
그 다음 문제는 Domain 모듈의 책임 과다였다.
RDB와 NoSQL이 한데 뒤섞이면서 구성 설정하는 것도 번잡스러워지고, 애초에 이런 인프라 정보를 domain 모듈이 알고 있다는 것 자체가 마음에 들지 않았다.
안타깝게도 나는 JPA를 사용 중이었고, Infrastructure에 위치했어야 할 DB 설정들을 의존성 역전 원칙을 준수해가며 세팅하기엔 개발 비용이 너무 컸다.
따라서 적어도 domain 모듈의 infra 의존도만이라도 낮추기 위해 모듈을 분리하기로 했다.
그래서 우아한 기술 블로그에서 나온 것처럼 "하나의 모듈은 하나의 infrastructur만 책임지도록 모듈을 작성"하는 것을 원칙으로 rdb와 nosql 모듈을 분리, domain service에서 필요한 모듈을 불러와 사용하도록 분리시켰다.
여담이지만, 난 이 방법이 전혀 만족스럽지 않았다.

의견이 많이 갈리는 부분이긴 하지만, 난 핵심 비지니스 규칙을 Entity가 표현하도록 구현하는 것을 선호하는 사람이다.
Martin Fawler가 이야기한 Anemic Domain Model을 알고 쓴 건 아니지만, Service에서 모든 비즈니스 로직을 처리하도록 권장하는 것은 객체 지향 설계의 근본 사상에 완전히 위배되는 안티 패턴으로 생각하는 입장이다.
그렇기에 내 코드는 Entity가 비즈니스의 기본 지식, 상황 정보, 규칙을 나타내고, Service는 SW가 해야할 작업을 명시한 후 Domain 객체들에게 작업을 위임하도록 했었는데, 위와 같은 분리는 핵심 비즈니스 규칙이 Domain Service 영역을 이탈하는 아이러니가 발생하게 된다.
이걸 알고 있었음에도 개선하지 못한 이유는 JPA 설정 복잡성이 큰 부분을 차지했었다.
📌 To Go Back to the Beginning
이러한 고민들을 하고 나니, 신규 프로젝트를 하면서 첫 설계 관점이 많이 달라지게 되었다.
"기술이나 트렌드가 내 아키텍처를 결정하도록 방임하지 말고, 내 수요에 의해 기술이 결정되도록 만들겠다."
이 관점이 트이고 나서야, 비로소 1년 전에 시청했던 2023 인프콘 "우리는 이렇게 모듈을 나눴어요" 영상에서 나온 말이 온전히 이해가 되었다.
개발을 시작할 때 무엇부터 해야할까?
테이블 설계, 캐시 AOP 등 모두 중요하지만 가장 중요한 것은 아닐 수도 있다.
가장 중요한 것은 핵심 비즈니스 로직과 유즈 케이스다.
가장 먼저 내가 만들 서비스의 요구 사항을 분석한 후, 구현해야 할 스펙을 문서로 정리했다.
어떤 프레임워크를 사용할지, 사용자에게 어떻게 보여줄지, 어떤 db와 추가 infra를 사용할 지, 더 이상 아무것도 중요하지 않았다.
정확히는 모두 세부 사항이기에 지금 고려할 이유가 없었다.
web이나 mobile이 아닐 수도 있고, MySQL이 아니거나, 애초에 db가 필요가 없을 수도 있었으며, 더 나아가 framework가 필요한지도 확실치 않은 상황이었으니까.
순수 코틀린 프로젝트를 생성한 후, 기능을 구현하기 위한 Service 파일을 생성했다.
input에 대한 output을 출력하는 함수를 만들다보니, 반드시 저장해서 관리해야 하는 데이터들도 있고, 소프트웨어 종속적이지 않은 규칙들도 존재하고, 내 서비스의 가치를 만들어줄 로직들이 분리되어 보이기 시작했다.
그래서 이들을 순수 함수로써 분리하고, Entity는 필요할 때마다 필드와 메서드를 추가해가며 개발을 속행했다.
내심 사용하고 싶었던 RDB, NoSQL, Store, MQ 같은 것들이 정말 많았지만, 그러한 세부 사항은 애써 뒤로 미뤘다.
인프라가 필요할 때마다 interface를 만들고, 메모리 상에서 동작하는 간단한 구현체만 붙여두면서 비즈니스 로직에 초점을 맞추어 진행했다.
일단 동작하는 무언가를 보고 싶었던 욕구가 모든 것을 압도했다.
위에서 언급한 web 모듈이 특수한 이유로 infra 모듈을 의존해야 할 필요성도 조금 다르게 해석하기 시작했다.
이 또한 우리 시스템, 즉 애플리케이션 스펙을 맞추기 위한 정책(policy)이라면, 응당 domain에 속해야 한다고 생각했다.
이렇게 개발하다보니 어떠한 형태가 되었는가?
그 유명한 헥사고날 아키텍처와 유사한 형태가 만들어졌다.

나중에서야 Rober C. Martine의 Clean Architecture를 읽고 헥사고날 아키텍처 내용을 찾아보니, 지금 내가 하는 방식이랑 똑같은 이야기를 하고 있었다.
하지만 중요한 건 헥사고날 아키텍처를 만들었다는 사실이 아니라, 왜 이렇게 되었는지 설명할 수 있다는 것이다.
각 레이어의 분리, 의존성의 방향, DomainService가 interface가 아닌 이유 등.
판에 틀어박힌 "확장성과 유연성을 위해서요"라는 답변이 아니라, 진정 이 구조가 어떤 측면에서 이로웠고 내 SW에 적합한지 설명할 수 있으며, 더 나아가 위 설계가 적절치 않은 때가 언제인지도 설명 가능하다.
이야, 겉모습에 홀려 따라하기만 하던 내가 자신있게 이런 말을 뱉을 수 있는 날이 오다니.
덜 맞아서 이럴 확률이 높다.
이 방식을 기존 시스템에 적용했다면, 다음과 같은 아키텍처로 변모하지 않았을까 싶다.

지적할 여지가 다분한 설계지만, 직접 해본 것도 아니고 구상일 뿐이라서 반박을 받을 수가 없다.
+ 예전에는 JPA 사용하면 위 구조로 만드는 게 불가능할 것이라 생각했는데, JPA가 애초에 인터페이스 모음집이라서 domain 의존성에 추가해주고, infrastructure 패키지 규칙만 잘 준수하면 가능할 것 같긴 하다.
📌 Deployment vs Architecture
단일 레포 싱글 모듈에서도 좋은 소프트웨어를 만들 수 있다.
MSA 가능성을 염두하기 위해 SOA를 따른다거나, 코드를 깔끔하게 만들기 위해 멀티 모듈을 선택했었지만, 전자는 순서가 바뀌었고 후자는 아키텍처와 하등 상관없는 내용이었다.
배포 수준 또한 세부 결정 사항이며, 처음부터 잘 설계된 SW를 만들었다면 패키지 몇 개 분리하고, 설정을 조금 더하는 것만으로도 monolithic에서 MSA로의 전환이 가능했어야 한다.
이건 훈수 목적의 내용이 아니라, 멀티 모듈로 분리했다가 피를 본 경험을 공유하려는 목적이다.

모듈을 분리한 이유는 코드의 심미성과 관심사 별 컴포넌트를 분리하기 위한 목적이었다.
즉, 필요에 의해서 이미 분리되어있던 애플리케이션들의 유지보수성을 높이기 위해 멀티 모듈을 구성한 게 아니라, 모놀리식으로 잘 동작하던 녀석들을 원칙대로 분리하기 위한 잘못된 접근이었다.
그 결과가 어땠는가?
- 객체 지향 근본 원칙을 모두 이해했다고 착각한 상태로 잘못된 코드를 작성했었는데, 문제가 발견되었을 때 싱글 모듈일 때보다 작업 비용이 커졌다. (오버 엔지니어링보다 언더 엔지니어링이 낫다는 걸 이때 깨달음)
- 팀원들이 이해하지 못해서 간단한 기능 하나도 구현하는데 오래 걸림.
- application 레벨 모듈을 모두 각각 배포해야 하므로 인프라 관리 비용의 증가.
- 모놀리식이면 함수 호출로 끝날 것을 MQ도 도입해야 하고, sync 틀어지는 것도 해결해야 하는 등 side effect 비용 증대.
- 그보다 돈이 너무 많이 듦..
개인 공부 목적으로라면 얻은 게 정말 많았지만, 프로젝트 입장으로 봤을 때는 실패였다.
패키지 단위만 잘 나눠두고 모놀리식으로 빠르게 개발 서버 띄워가며 작업했으면, 최소 2~3개월은 개발 기간을 단축시킬 수 있었을 것이라 분석하고 있다.
모듈을 분리하는 것을 아키텍처 관점에서 중요하다고 오해해서 발생한 문제였다.
3. Conclusion
📌 Where We Are Now
5년 전 HTML에 모든 걸 집어넣던 나는, 이제 "이 코드는 왜 여기 존재하는가?"를 끊임없이 묻는 한 명의 개발자가 되었다.
그 사이에 수많은 시행착오를 겪었고, 끊임없이 더 나은 소프트웨어를 만들기 위해 노력했다.
처음에는 궁극적으로 완벽한 아키텍처가 존재할 것이라는 믿음 때문이었다.
소문 자자한 아키텍처나 인프라 구조를 직접 구현해내면, 개발자로서의 역량이 높아졌다고 볼 수 있을 것이라 생각했다.
그러나 실상은 달랐다.
공부를 위해 여러가지 시도를 해본 것은 좋았다.
실제로 그 과정에서 값진 경험을 많이 쌓았고, 그 덕에 이러한 교훈들을 얻을 수 있었으니까.
하지만 "지금 내 학습이 프로젝트 성공보다 우선인가?"라는 질문에 눈을 돌리지 않고 다시 마주해본다면, 이는 분명한 엔지니어의 책무를 망각한 행위였다.
팀은 따라오지 못했고, 아키텍처와 배포 수준을 분리하여 생각하지 못해 프로세스는 복잡해졌고, 비용은 증가했다.
개인으로서는 성공이었지만, 프로젝트는 실패했다.
그렇다면 우리는 어떤 아키텍처를 설계해야 하는가.
지난 시행착오를 통해 알게 된 점은 이 질문 자체가 잘못되었다는 것이다.
그러니 질문은 다음과 같이 바뀌어야 한다.
현재 상황에서 어떤 원칙들을 따라야 좋은 패턴의 발견으로 이어질 수 있는가.
질문을 바꾸면 아키텍처를 따르는 것이 아니라, 문제를 이해하고 올바른 원칙을 적용함으로써 결과적으로 최선의 아키텍처를 발견하게 될 수 있다.
여기서부터가 시작이다.
계속 코딩하고, 질문하고, 성장해야 한다.
문제를 해결하면서 해결책 뒤에 숨은 원칙들을 배우고, 번지르르한 이름 따위에 현혹되지 말고 그 본질을 파악한다.
Clean Architecture, 헥사고날, SOA같은 하나의 형태가 아니라, 그 기저에 깔려있는 핵심 가치가 무엇인지를 아는 것이 보다 중요하기 때문이다.
이러한 과정이 보다 진정한 엔지니어로서의 자질을 키우는데 도움이 된다고 믿는다.