프로젝트 규모가 커지면서 고민한 멀티 모듈(Multi-Module) 아키텍처 분리 기준
하나의 거대한 Monolithic 서버가 무거워질 때, 도메인 주도 개발(DDD) 관점과 클린 아키텍처를 결합해 스프링 부트 프로젝트를 멀티 모듈로 쪼갠 회고입니다.
멀티 모듈, 왜 굳이 쪼개는가?
지금까지 작성해 온 API 예외 처리 전략, 공통 응답 구조, 통합 테스트 세팅 정도만 구축해 두면, "이 정도면 훌륭한 백엔드지"라며 만족하던 시기가 있었습니다.
하지만 운영 중인 서비스에 기능이 조금씩 덕지덕지 수개월 간 붙기 시작하면서 불편함이 감지되었습니다.
- 빌드 속도:
payment기능 하나를 살짝 수정했을 뿐인데 프로젝트 전체가 빌드되고 온갖 상관도 없는 도메인의 통합 테스트가 우르르 돕니다. - 의존성 늪(Spaghetti Dependencies): 어느 날 보니
OrderService가UserEntity와 외부Pay API클라이언트까지 몽땅 참조하고 있는, 스파게티 그릇이 펼쳐져 있었습니다. - 독립적 배포 불가: 외부 배치를 도는
Scrap로직과 사용자 반응을 실시간으로 쏘는API서버를 트래픽 상황에 맞게 따로 서버 증설할 방법이 막혀있었습니다.
이러한 Monolithic(단일 구조)의 한계를 깨고자, Gradle 기반의 멀티 모듈(Multi-Module) 아키텍처로 레이어를 발라낸 고군분투기를 나눕니다.
어떻게 찢을 것인가? 분리 기준의 고민
가장 유명한 우아한형제들의 '멀티 모듈 구성' 발표를 여러 번 돌려봤지만, 팀마다 컨벤션과 비즈니스 상황이 달라 제게 맞는 구조를 스스로 정의해야 했습니다.
수차례의 화이트보드 세션을 거쳐 저는 계층과 시스템의 역할에 따라 3단계 구조로 나누었습니다.
1단계: 가장 깊은 곳, Core (도메인)
module-core는 이 서비스의 심장입니다. 순수한 JPA Entity와 비즈니스 로직, 그리고 공통적으로 사용되는 유틸리티(Exception 핸들러 등)가 밀집되어 있습니다.
절대적으로 지킨 원칙은 "Core 모듈은 외부 모듈을 참조하지 않는다" 였습니다. 오직 Java, Spring Data JPA, Lombok 정도의 의존성만 허용했습니다.
2단계: 외부와의 소통, Infra (통신)
module-infra는 Redis 캐싱 제어, Sentry 설정, 외부 API 통신(OpenFeign 등) 모듈들을 뭉쳐두었습니다. 카카오톡 알림톡을 쏘거나 AWS S3에 이미지를 올리는 부차적인 기능이 이 레이어에 해당합니다. Core를 알고는 있지만 철저히 인터페이스(Interface) 로직으로 주입받게 하여 결합도를 끊어냈습니다.
3단계: 애플리케이션의 엔드포인트, API & Batch
가장 바깥 레이어로 실 사용자가 직접 닿는 영역입니다.
module-api: 외부 클라이언트나 프론트엔드가 호출하는 REST Controller, API Docs 생성 등이 위치. (Core와 Infra 참조)module-batch: 매일 새벽마다 쿠폰 만료 등의 통계를 내는 Spring Batch 모듈. (가벼워야 하므로 API는 배제하고 Core만 참조)
Gradle 세팅의 간소화
모듈을 분리하면서 제일 짜증났던 건 모듈마다 build.gradle에 dependencies { implementation 'org.springframework...' }를 수십 번 반복 작성하는 일이었습니다.
이를 해결하기 위해 최상단 루트(Root)의 build.gradle에서 subprojects 혹은 allprojects 선언을 통해 공통 설정(예: Java 버전, Lombok, 레포지토리)을 단 한 번만 명시하도록 통일했습니다.
subprojects {
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
}
// 특정 모듈에만 추가 의존성이 필요할 땐 해당 모듈 빌드 파일에 선언
project(':module-api') {
dependencies {
implementation project(':module-core')
implementation project(':module-infra')
implementation 'org.springframework.boot:spring-boot-starter-web'
}
}분리하고 보니 깨달은 점
초기 러닝 커브와 세팅에 들어간 며칠의 시간 투자는 뼈아팠지만, 멀티 모듈로 자리를 잡고 나니 제가 왜 이 짓(?)을 했는지 명확히 알게 되었습니다.
우선, 다른 모듈의 코드를 잘못 참조(import)하려는 순간 IDE(IntelliJ) 레벨에서 아예 컴파일 에러가 나면서 빨간 줄을 그어줍니다. 예전엔 컨벤션으로 "이 클래스는 저 클래스를 호출하면 안 돼"라고 구두로 약속했던 것들이, 구조적으로 시스템이 막아주니 신입 개발자가 합류해도 스파게티 코드가 생산될 리스크가 원천 차단된 셈입니다.
도메인을 격리(Isolation)하는 방법론. 이 경험 덕에 어떤 프로젝트를 시작하든 거대한 진흙덩이(Big Ball of Mud)를 피해 가는 단단한 지도를 손에 거머쥔 기분입니다.