"또 아키텍처를 바꾸자고..?" 솔직히 이 말, 개발자라면 한 번쯤 속으로 해봤을 거라 생각한다. 나도 예외는 아니었다.
중복의 늪과 아키텍처 실험
우리팀은 백엔드 개발자 총 4명으로, 관리하는 API Application만 20개가 넘는다.
각 Spring Boot API Application마다 같은 스키마를 참조하는 Entity나 DTO, Repository 로직이 JDK 버전이나 Spring Boot 버전만 다를 뿐 사실상 복붙 수준으로 중복되어 있었다.
이 문제를 해결하기 위해 처음엔 "중복된 Entity와 DTO, Repository를 공통 라이브러리로 분리해서 Nexus Repository에 배포하자!"라고 생각했다. 하지만 실제로 진행해보니 예상치 못한 문제들이 발생했다.
Entity, Repository를 공통화하니 각 서비스의 도메인 경계가 모호해졌고, 한 서비스에서 Entity를 수정할 때마다 다른 모든 서비스에 영향을 미치게 되었다. 또한 공통 라이브러리의 버전 관리가 생각보다 복잡했다.
DTO 공통화도 마찬가지였다. API 스펙이 변경될 때마다 의존성 지옥에 빠졌고, 서비스별로 요구사항이 달라서 불필요한 필드들이 계속 추가되었다. 가장 큰 문제는 각 서비스의 독립적인 배포와 개발 사이클이 방해받는다는 점이었다. 결국 "공통화"보다는 "표준화"가 더 나은 접근이었다는 것을 깨달았다.
찍어먹어본 MSA(마이크로서비스 아키텍처)
표준화를 진행하며 각각의 서비스가 가진 특징을 살려보겠다고 OpenFeign(내부 API 연동), Eureka(Service Discovery), Spring Cloud Gateway(API 게이트웨이), Spring Cloud Config(분산 환경 설정 관리) 이런 기술들도 팀에서 열심히 공부했다.
"각 API Application을 작은 서비스로 쪼개서, 필요한 부분만 내부적으로 통신하면 중복도 자연스럽게 줄고, 서비스별로 관리도 쉬워지지 않을까?" 이런 논리였다.
실제로 우리가 제공하는 여러 서비스 중 한 도메인(예를 들어 '회원' 관련)만 따로 떼서 3개의 어플리케이션을 Multimodule로 구성하고, 각각의 Service를 독립적으로 띄운 후 Eureka에 등록해서 OpenFeign으로 내부 통신하는 '작은 MSA' 환경을 만들어봤다.
처음엔 "오, 이거 되는 거 아냐?" 실제로 내부에서 서비스간 REST 통신도 잘 되고, 각 모듈을 따로 띄웠다 내렸다 하면서 Eureka 대시보드에서 Service Discovery도 테스트해봤다.
그렇데, 문제는 그 다음이었다. 막상 조금만 복잡한 비즈니스 로직이나 실제 운영 환경으로 생각해보면 여러 문제들이 한꺼번에 터졌다. 중복 로직을 공유하는 패키지 관리가 생각보다 어려웠고, 공통 Entity/Repository/DTO의 버전 관리 복잡성도 여전했다. 무엇보다 서비스간 통신 오버헤드와 장애 전파 문제가 심각했다. 한 서비스에 문제가 생기면 연쇄적으로 다른 서비스들까지 영향을 받았다. 그리고 배포, 롤백, 테스트 등 현실적인 운영 관리 포인트들이 오히려 더 복잡해졌다.
"중복은 계속 생기는데, 이젠 라이브러리 의존성이나 버전 관리가 오히려 더 까다로워진 것 같고, 서비스가 많아지면 모니터링이나 장애 대응도 결국 또 다른 관리 포인트가 되더라."
OpenFeign, Eureka, Spring Cloud 모두 공부와 PoC(Proof of Concept) 단계에선 신기하고 재밌었지만 "이걸 우리가 지금 '우리 서비스 현실'에 바로 가져갈 수 있을까?" 의문이 남았다.
그래서, 처음 고민이 뭐였지?
사실 우리 팀의 아키텍처 실험은 여기서 끝이 아니었다.
MSA에 대한 기대와 환상, 그리고 실전에서 겪은 복잡함을 온몸으로 경험하고 나니, "이 많은 서비스들과 공통 로직을 효율적으로 관리할 수 있는, 우리만의 확실한 기준이 필요하다"는 이야기가 자연스럽게 나왔다.
이때 팀원들 사이에서 처음부터 다시 진지하게 논의한 것은 "우리가 진짜로 해결하고 싶은 게 '물리적인 서비스 분리(MSA)'냐, 아니면 각자 맡은 코드의 책임과 관심사가 명확하게 분리되는 구조냐"는 본질적인 질문이었다.
MSA 실험 과정에서 깨달은 것은, 단순히 애플리케이션을 여러 개로 나눈다고 해서 '관심사의 분리'가 저절로 따라오는 게 아니라는 점이었다. 공통 Entity 하나 고치려 해도 각 서비스마다 버전 관리가 따로라 실수하기 쉽고, DTO나 공통 로직을 라이브러리로 관리해도 API 응답이 달라지거나, 한 쪽에만 버그가 생기면 트러블슈팅도 배로 힘들었다.
이런 경험 끝에, 진짜 우리가 추구해야 할 건 서비스의 갯수나 네트워크 통신 방식이 아니라,코드와 책임, 의존성, 변경의 파급 효과 자체를 '명확하게 구획하는 것'이라는 결론에 이르렀다.
그래서 다시 "관심사의 분리"라는 고전적인 개발 원칙을 붙잡았고, 이걸 현실적으로 실현해줄 수 있는 방법을 찾아보니 자연스럽게 헥사고날 아키텍처(Hexagonal Architecture)에 눈길이 갔다.
헥사고날 아키텍처를 진지하게 적용해보자는 논의가 이어졌다. 처음엔 생소할 수도 있었지만, 실제로 프로젝트에서 마주쳤던 문제들을 하나씩 대입해보면 헥사고날 아키텍처가 준 '구조적 이점'이 분명하게 보이기 시작했다.
우리가 정의한 헥사고날 아키텍처
헥사고날 아키텍처를 실제로 적용하면서 가장 먼저 고민했던 것은 "8개의 API 애플리케이션을 어떻게 일관성 있게 구조화할 것인가"였다. 단순히 이론적인 레이어 분리가 아니라, 실제 개발자 4명이 매일 코드를 작성하고 리뷰하면서도 헷갈리지 않을 구조를 만들어야 했다.
멀티모듈을 사용한 물리적 분리
처음엔 하나의 애플리케이션 안에서 패키지로만 분리하려 했지만, 곧 한계를 느꼈다. 개발자들이 여전히 도메인 레이어에서 JPA Entity를 import하거나, 인프라 레이어의 구현체를 직접 참조하는 실수를 반복했다.
그래서 아예 물리적으로 모듈을 분리했다:
- module-domain: 순수 비즈니스 로직만
- module-application: 유스케이스와 포트 정의
- module-adapter: 인바운드/아웃바운드 어댑터
이렇게 하니 컴파일 단계에서 의존성 위반이 바로 걸렸다. 도메인 레이어에서 JPA Entity를 import하려 하면 아예 빌드가 실패하는 구조가 된 것이다.
포트/어댑터 - 추상화의 진정한 힘
가장 극적인 변화는 외부 연동 부분이었다. 기존에는 서비스 클래스에서 직접 JPA Repository를 호출하고, 암호화 유틸을 직접 사용했다. 요구사항이 바뀔 때마다 비즈니스 로직과 기술 구현이 뒤섞여서 수정 범위를 파악하기 어려웠다.
이제는 애플리케이션 레이어에서 EncryptionPort, RepositoryPort라는 추상 인터페이스만 사용한다. 실제 구현은 인프라 레이어의 어댑터에 숨어 있다.
예를 들어, 개인정보 암호화 방식을 AES에서 다른 방식으로 바꾸더라도 비즈니스 로직은 전혀 건드리지 않는다. EncryptionPort의 구현체만 교체하면 된다
코드 리뷰의 질적 변화
가장 결정적인 변화는 코드 리뷰 관점이 달라진 것이다. 예전엔 "이 코드가 동작하나?"에 집중했다면, 이제는 "이 코드가 올바른 레이어에 있나?"를 먼저 본다.
- "이 validation 로직이 왜 Controller에 있지? Domain으로 내려야 하는 거 아닌가?"
- "JPA Entity의 변환 로직이 Service에 있네. Adapter로 옮겨야겠다."
- "이 business rule이 Infrastructure layer에 있으니 Domain으로 올려야 한다."
팀원들이 자연스럽게 "관심사의 분리"를 체크하는 습관이 생겼다. 권한 체크, 예외 처리, 데이터 변환까지 모두 "이건 어느 레이어 책임인가?"를 명확하게 정할 수 있게 된 것이다.
실제 운영에서의 효과
헥사고날 아키텍처 형식의 애플리케이션을 관리하면서 가장 큰 변화는 "예측 가능성"이었다. 새로운 기능을 추가할 때도, 버그를 수정할 때도 "어디를 봐야 하는지" 바로 알 수 있다.
DB 스키마가 바뀌면 JPA Entity와 Repository만 보면 되고, 비즈니스 규칙이 바뀌면 Domain과 Application layer만 보면 된다.
Boolean 타입이나 암호화 처리 같은 기술적 규칙도 더 이상 "팀의 고민거리"가 아니라 "각 레이어에서 자연스럽게 따르는 명확한 규칙"이 되었다.
결국 우리는 헥사고날 아키텍처를 통해 단순히 "깨끗한 코드"를 얻은 게 아니라, "예측 가능하고 유지보수 가능한 시스템"을 만들 수 있었다.
우리는 헥사고날 아키텍처를 선택했다.
결국, 우리는 헥사고날 아키텍처를 선택했다. 그리고 그 선택은 지금도 여전히 발전 중이다.
우리는 여러 기술 스택을 직접 손에 익혀가며, 실제로 MSA식 내부 통신 구조도 실험했다. 중복된 코드와 서비스 분리의 어려움도, 그리고 수많은 회고와 토론을 반복하면서 우리에게 가장 현실적인 해답이 무엇일지 고민했다. 그렇게 내린 결론이 바로 “이제부터 우리 서비스의 개발과 설계를 헥사고날 아키텍처를 기준으로 통일하자”였다.
더는 각 서비스마다 어설프게 코드를 나누거나, 중복 라이브러리 때문에 복잡하게 얽히는 대신, 이제는 레이어별 역할과 책임을 명확히 구분하고, 데이터 변환, 권한 처리, 예외 처리, 네이밍 컨벤션까지 팀 전체가 합의한 원칙 아래서 표준화하고 있다.
그리고 무엇보다 중요한 것은, 이렇게 만들어진 명확한 아키텍처 기준이 우리 팀의 성장 기반이 되었다는 점이다. 이 기준 위에 실제 구현 경험과 시행착오가 차곡차곡 쌓이면서, 팀원 누구나 코드의 의도와 역할을 쉽게 이해할 수 있고, 새로운 멤버가 들어와도 자연스럽게 우리의 문화를 익혀갈 수 있게 되었다.
하지만 여기서 모든 게 끝난 것은 아니다. 실제 운영을 하다 보면 아직도 해결해야 할 고민이 많다.
멀티모듈 구조에서 빌드 시간이 점점 길어지는 문제, 도메인 로직과 애플리케이션 로직의 경계가 애매해지는 상황, 새로운 요구사항이 들어왔을 때 기존 아키텍처 안에서 이를 어떻게 자연스럽게 녹여낼지에 대한 고민 등은 여전히 반복되고 있다.
예를 들어, 최근에는 “이 validation은 정말 도메인 규칙에 들어가야 할까, 아니면 단순히 입력값 검증에 불과한 걸까?” 같은 고민이 자주 생긴다. 또, 복잡한 비즈니스 로직이 여러 도메인에 걸쳐 분산될 때 책임 분리를 어떻게 해야 할지도 팀 내에서 계속 시행착오를 겪는 중이다.
그래서 우리 아키텍처는 지금 이 순간에도 ‘진화 중’이다. 매일 점심시간 끝나기 10분 전, 누가 시키지 않아도 자리로 모여서 “아키텍처 관점에서 애매하거나 고민이 컸던 사례”를 서로 공유한다. 그 자리에서 새로운 기준이나 규칙을 제안하고, 이미 정한 규칙이라도 필요하면 기꺼이 보완해 나간다.
BooleanYn 타입 처리, 암호화 규칙 등도 마찬가지다. 처음엔 꽤 완벽하다고 생각했지만, 실제로 써보니 예외 케이스가 등장하고(예를 들어 BooleanYn을 도메인에서 직접 참조했는데 애그리거트 루트 참조규칙에 위반된다 생각하여 JpaConverter를 도입해서 사용중),
그때마다 규칙을 다듬거나 더 나은 방식으로 바꿔가고 있다.
이제는 “완벽한 아키텍처를 찾았다”는 생각 대신, “우리 팀만의 기준을 만들고 그 기준을 계속 발전시켜가는 문화”를 갖게 된 것이 가장 큰 변화이자 성과라고 느낀다.
예전에는 “이건 대체 어디다 둬야 하지?”라며 막막해했다면, 지금은 “지금 기준에선 이렇게 하는 게 맞아. 만약 문제가 보이면 다음 회의에서 기준을 더 보완하자”라는 유연하고 실용적인 태도로 접근할 수 있게 됐다.
헥사고날 아키텍처는 우리 팀의 최종 목적지가 아니라, 더 나은 팀으로 성장하기 위한 새로운 출발점이 되어주었다.
처음엔 “또 아키텍처를 바꾸자고..?“라는 말에 부담부터 느꼈던 내가, 지금은 오히려 “이건 이렇게 한 번 바꿔볼까요?”라며 변화를 즐기고 있다.
'생활 로그 > 회고록' 카테고리의 다른 글
JPA에서 N+1 문제를 어떻게 해결하고 계신가요? (4) | 2025.07.04 |
---|---|
나는 이제, 기술자가 되고 싶다 (7) | 2025.07.04 |
댓글