[미래를 담아낸 뼈대 1/7] 컴파일이 지키는 아키텍처

우리가 풀고 있는 문제는 "대용량 트래픽"이 아닙니다
개발자 커뮤니티에서 가장 눈길을 끄는 키워드는 "대용량 트래픽"과 "복잡한 마이크로서비스"입니다. 초당 수십만 건의 요청을 처리하고, 수백 개의 서비스를 오케스트레이션하는 이야기. 솔직히 매력적이죠. 기술 블로그를 쓰거나 발표를 할 때도, 이 숫자들이 앞에 오면 주목도가 확 달라집니다.
하지만 모든 서비스가 그런 문제를 풀고 있는 건 아닙니다. 그리고 솔직히, 모든 서비스가 그런 문제를 풀어야 하는 것도 아닙니다. flex는 HR SaaS입니다. 출퇴근 시간에 트래픽이 몰리긴 하지만, 초당 수십만 건의 요청이 들어오는 서비스는 아닙니다. 트래픽이 우리의 핵심 도전은 아니라는 뜻입니다.
그렇다면 우리가 풀고 있는 어려운 문제는 무엇일까요?
flex의 제품 라인업을 보면 감이 옵니다. 인사 정보, 인사 관리, 근무, 휴가, 워크플로우, 급여, 채용, 평가, 목표 관리, 캘린더, 인사이트, 비용 관리까지 — 많은 도메인 모듈이 하나의 제품 안에 존재합니다. 그리고 이 도메인들은 독립적으로 존재하지 않습니다. 급여를 계산하려면 근무 시간 데이터가 필요하고, 평가를 진행하려면 구성원 정보와 조직 구조를 알아야 하고, 인사이트는 모든 도메인의 데이터를 종합해야 의미 있는 분석을 제공할 수 있습니다.
여기에 권한의 복잡성이 더해집니다. "구성원 A가 급여팀 소속이면서, B부서의 매니저이고, C프로젝트의 열람자일 때, 이 사람이 D의 급여 명세서를 볼 수 있는가?" 이 질문 하나에 답하려면 조직 구성원 여부, 하위조직 관계, 유저 그룹, 오브젝트 간 관계, 심지어 2차 인증 여부까지 다양한 도메인의 정보를 동원해야 합니다.
트래픽은 서버를 늘리면 됩니다. 하지만 9개 이상의 도메인을 개발하는 여러 팀이 같은 규칙 안에서 코드를 짜고, 도메인 간 데이터 정합성을 유지하며, 복잡한 권한 체계를 일관되게 적용하는 건 — 서버를 아무리 늘려도 풀 수 없는 문제입니다. 이건 아키텍처로만 풀 수 있습니다.
Hexagonal Modular Monolith — MSA도 모놀리스도 아닌 길
flex 백엔드는 Hexagonal Modular Monolith라는 구조를 선택했습니다.
익숙한 선택지부터 짚어보겠습니다. 전통적인 모놀리스는 모든 코드가 하나의 덩어리에 있습니다. 시작은 빠르지만, 코드가 커지면서 도메인 간 경계가 무너지고, 한 곳을 고치면 다른 곳이 깨지는 상황이 반복됩니다. 그래서 많은 팀이 MSA(마이크로서비스)로 넘어갑니다. 서비스를 작게 쪼개서 독립적으로 배포하자는 거죠. 하지만 MSA는 서비스 간 통신, 분산 트랜잭션, 배포 파이프라인 관리 등 운영 비용이 급격히 올라갑니다. 9개 이상의 도메인이 서로 데이터를 참조해야 하는 HR SaaS에서, 모든 도메인을 독립 서비스로 분리하는 건 득보다 실이 큰 선택이었습니다.
flex가 선택한 Hexagonal Modular Monolith는 이 둘의 장점을 취합니다. 하나의 배포 단위 안에 있지만, 각 도메인은 명확한 모듈 경계를 가집니다. 그리고 각 모듈은 헥사고날 아키텍처(Ports & Adapters)를 따릅니다. 도메인 로직이 중심에 있고, 외부와의 통신은 Port(인터페이스)와 Adapter(구현체)를 통해서만 이루어집니다. 이 말은 도메인 로직이 데이터베이스나 외부 API 같은 인프라에 직접 의존하지 않는다는 뜻이고, 모듈 간의 결합도 Port를 통해서만 이루어진다는 뜻입니다.
flex에는 `flex-skeleton`이라는 헥사고날 도메인 모듈 템플릿이 있습니다. 새로운 도메인을 시작할 때 이 스켈레톤을 기반으로 모듈을 만들면, Port/Adapter의 디렉토리 구조, 의존성 방향, 계층 분리가 자동으로 잡힙니다. "어떻게 구조를 잡아야 하지?"라는 고민을 처음부터 하지 않아도 됩니다.
단일 런타임 — 모든 도메인이 하나의 JVM
모든 도메인이 같은 프로세스에서 함수 호출로 통신합니다. DB는 같은 인스턴스를 공유하되, 도메인별로 논리적 스키마가 격리되어 있습니다.
부분 분 리 — 필요한 만큼만 떼어내기
Recruiting만 별도 런타임으로 분리했습니다. Core HR ↔ Payroll은 여전히 함수 호출이고, Recruiting과의 통신만 REST/Kafka Adapter로 교체됩니다. Domain + Port 코드는 변경 0줄.
완전 분리 — 각 도메인이 독립 런타임
모든 도메인을 독립 런타임으로 분리해도, 바뀌는 건 Adapter뿐입니다. 안쪽의 Domain + Port는 세 그림 모 두 동일합니다. 이것이 Hexagonal Architecture의 힘입니다.
아키텍처를 문서가 아니라 빌드가 지킨다
구조를 정하는 건 첫 번째 단계일 뿐입니다. 진짜 어려운 건 그 구조를 유지하는 겁니다. 아키텍처 가이드 문서를 아무리 잘 써도, 마감에 쫓기면 사람은 지름길을 택합니다. "이번만 이렇게 하자"가 쌓이면, 어느새 가이드 문서는 현실과 동떨어진 이상향이 됩니다.
flex는 이 문제를 Gradle Convention Plugin으로 풀었습니다. 하나둘이 아닙니다.
'name-policy-plugin'은 멀티모듈 프로젝트에서 모듈의 group name을 계층 구조에 맞게 자동으로 해결합니다. Gradle은 기본적으로 'group:module' 조합이 충돌하면 문제가 생기는데, 200개 넘는 모듈이 '{도메인}:{레이어}' 형태로 구성될 때 이 충돌을 방지합니다. 'module-hierarchy-settings-plugin'은 이 대규모 모듈 선언을 계층적 DSL로 작성할 수 있게 해서, 'settings.gradle.kts'가 곧 프로젝트의 아키텍처 지도 역 할을 합니다.
'build-recipe'는 모듈의 타입(예: 'kotlin-boot-mvc-application', 'kotlin-boot-jdbc-repository')에 따라 표준화된 빌드 설정을 자동으로 적용합니다. 개별 모듈의 'build.gradle.kts'에서 의존성이나 플러그인을 직접 나열할 필요가 없습니다. 'gradle.properties'에 'type=kotlin-boot-mvc-application'이라고 한 줄 선언하면, Spring Boot, Web, Security, 테스트 프레임워크 등이 자동으로 구성됩니다.
'version-management'는 사내 라이브러리와 외부 라이브러리의 버전을 3단계 우선순위 시스템으로 중앙 관리합니다. 개별 모듈에서 버전을 직접 명시하면 오히려 관리에서 벗어나기 때문에, 버전 없이 의존성만 선언하는 것이 올바른 사용법입니다. 버전은 플러그인이 결정합니다.
'publish-dependency-validator-plugin'은 한 단계 더 나아갑니다. 외부에 퍼블리시되는 라이브러리 모듈이 내부 전용 모듈에 의존하고 있으면, 빌드를 막습니다. 이 의존성이 빠져나가면 소비자 프로젝트에서 빌드가 깨지기 때문에, "나중에 알게 되는" 문제를 "지금 막는" 것입니다.
이 플러그인들이 하는 일의 본질은, 아키텍처 규칙을 컴파일 타임에 물리적으로 강제하는 것입니다. 어떤 모듈이 허용되지 않은 방향으로 다른 모듈에 의존하려고 하면, 코드가 빌드되지 않습니다. 문서에 "이 방향으로 의존하면 안 됩니다"라고 쓰는 대신, 빌드 자체가 실패합니다. 가이드 문서는 잊힐 수 있지만, 빌드가 깨지는 건 무시할 수 없습니다.
왜 우리 팀에서 이게 가능했을까
여기서 한 가지 의문이 들 수 있습니다. "이런 도구를 만든 별도의 플랫폼 팀이 있었나요?"
아닙니다. flex-skeleton이나 Gradle Convention Plugin 같은 도구들은 별도의 플랫폼 조직이 위에서 내려준 것이 아닙니다. flex의 엔지니어들은 스쿼드 체제에서 각자의 도메인 목표에 집중하면서도, "이 구조가 다른 팀에도 통하는가"를 함께 고민했습니다. 반복되는 문제를 보면서, "이건 공통으로 뽑을 수 있겠다"는 판단이 자연스럽게 나왔고, 그 판단을 실행에 옮겼습니다.
물론 이 진화를 플랫폼 관점에서 이끈 사람은 있었습니다. 하지만 결국 전체 엔지니어가 함께 채택하고 발전시킨 결과물입니다. 누군가 "이렇게 하세요"라고 지시한 게 아니라, 엔지니어들이 "이건 이래야 한다"고 스스로 판단하고 움직인 겁니다.
이 일관성이 다음 이야기의 전제가 됩니다. 모든 모듈이 같은 구조라는 건, 공통 인프라가 "기대한 대로" 동작한다는 뜻이기 때문입니다.

