본문 바로가기
Framework/Spring Boot

[Spring Boot] 멀티 모듈(Multi-Module) 아키텍처: 거대해진 모놀리스의 분리

반응형

작은 쇼핑몰로 시작했던 우리의 스프링 부트 프로젝트가 3년 만에 거대해졌습니다. 처음에는 src/main/java 폴더 하나에 컨트롤러(API), 도메인(Entity), 그리고 새벽에 도는 배치(Batch) 코드까지 몽땅 때려 넣고 행복하게 개발했습니다. 그런데 어느 날, 배치 팀 개발자가 User 엔티티에 필드를 하나 추가하고 배포를 했더니, 갑자기 API 팀의 로그인 서버가 런타임 에러를 뿜으며 죽어버립니다. "아니, 나는 배치 코드만 고쳤는데 왜 API 서버가 죽어?!" 하나의 거대한 프로젝트(모놀리식) 안에서 모든 코드가 뒤엉켜버린 스파게티 의존성의 저주입니다. 그렇다고 당장 38단계에서 배운 MSA(마이크로서비스)로 완전히 찢자니 서버 인프라 비용과 관리의 압박이 너무 심합니다. "하나의 프로젝트(레포지토리) 안에서, 공통으로 쓰는 핵심 도메인만 따로 떼어내고, API와 배치는 서로의 코드를 절대 못 보게 격리(분리)할 수는 없을까?" 이 우아한 절충안이 바로 Gradle 멀티 모듈(Multi-Module) 아키텍처입니다.

이번 [Spring Boot 실무 완벽 가이드] 시리즈에서는 "스타트업에서 중견기업으로 넘어가는 필수 관문!" 멀티 모듈 설계와 의존성 주입 실무 가이드에 대하여 다뤄보겠습니다.

🧱 1. 스파게티 코드의 종식: 역할별 모듈 분리

스프링 부트 프로젝트 최상단 폴더에 3개의 하위 폴더(모듈)를 새로 만듭니다.

멀티 모듈 개념도
  • module-core (코어 모듈): 모든 서버가 공통으로 알아야 하는 JPA 엔티티(User, Product 등), 공통 예외 처리, 공통 유틸리티 클래스만 들어갑니다. 이 모듈은 다른 어떤 모듈도 쳐다보지(의존하지) 않습니다. 오직 순수한 비즈니스 로직만 가집니다.
  • module-api (API 서버 모듈): 프론트엔드와 통신하는 @RestController들이 들어갑니다. Tomcat을 띄우는 메인 실행 클래스가 여기 있습니다. 이 모듈은 오직 module-core만 가져다 씁니다(의존).
  • module-batch (배치 서버 모듈): 새벽에 도는 대용량 배치(Job, Step) 로직만 들어갑니다. 당연히 module-core만 가져다 씁니다.

가장 중요한 핵심은 "module-api와 module-batch는 서로의 존재를 모른다(의존성 없음)"는 것입니다. 따라서 배치 개발자가 module-batch에서 아무리 똥을 싸도, API 서버는 단 1%의 영향도 받지 않고 평화롭게 돌아갑니다!


⚙️ 2. Gradle 설정: 의존성(Dependency) 조립하기

루트 폴더의 settings.gradlebuild.gradle을 수정하여 모듈 간의 계급과 관계를 정의합니다.

// settings.gradle (루트)
rootProject.name = 'my-shopping-mall'
include 'module-core', 'module-api', 'module-batch'
// build.gradle (module-api 폴더 내부)
dependencies {
    // 🌟 핵심: API 모듈은 Core 모듈을 자신의 프로젝트 내부 라이브러리처럼 땡겨옵니다!
    implementation project(':module-core')
    
    // API 서버 구동을 위한 웹 의존성 추가
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

이제 API 모듈에서 module-core에 만들어둔 User.java 엔티티를 import하여 자연스럽게 사용할 수 있습니다.


💀 3. 멀티 모듈의 함정: Bean 스캔 지옥

설정을 끝내고 module-api를 실행했는데 "User 엔티티를 못 찾겠어!" 또는 "Core에 있는 빈(Bean) 주입 실패!" 에러가 터집니다. 왜 그럴까요?
스프링 부트의 @SpringBootApplication은 기본적으로 자기가 속한 패키지(예: com.myapp.api)와 그 하위만 뒤져서 빈을 등록합니다. 그런데 Core 모듈의 패키지가 com.myapp.core라면? 스프링이 Core 쪽은 아예 쳐다보지도 않게 됩니다.

해결책: API 모듈의 메인 클래스에서 빈 스캔 범위를 최상위 공통 패키지(com.myapp)로 강제로 넓혀주어야 합니다.

// module-api 의 메인 클래스
@SpringBootApplication(scanBasePackages = {"com.myapp.core", "com.myapp.api"})
@EntityScan(basePackages = "com.myapp.core") // JPA 엔티티 스캔 범위 지정
@EnableJpaRepositories(basePackages = "com.myapp.core") // Repository 스캔 범위 지정
public class ApiApplication { ... }

🎯 4. 마무리 및 다음 단계

지금까지 거대한 하나의 덩어리를 우아한 레고 블록 단위로 찢어내고, 팀별(API팀, 배치팀)로 서로 간섭 없이 독립적인 개발을 가능하게 만드는 MSA 이전 단계의 필수 클린 아키텍처, Gradle 멀티 모듈 설계에 대해 상세히 다루어 보았습니다. 이제 소스코드를 고칠 때 덜덜 떨지 않아도 됩니다.

백엔드 소스코드는 모듈로 예쁘게 분리했습니다. 그런데 데이터베이스(DB)는 어떨까요? 개발 서버에서는 컬럼 이름이 user_name인데, 운영 서버에서는 name으로 되어 있어서 오늘 밤 배포가 장렬하게 실패했습니다. 소스코드는 Git으로 완벽하게 버전 관리가 되는데, DB 스키마(DDL)는 슬랙(Slack)으로 "DB 담당자님, 컬럼 하나 추가해 주세요~"라고 주먹구구식으로 소통하다 보니 싱크(Sync)가 안 맞는 것입니다. "자바 코드처럼, DB 스키마(DDL) 변경 내역도 형상 관리(버전 컨트롤) 툴에 넣어서 서버가 뜰 때 자동으로 DB를 최신 상태로 맞춰줄 수는 없을까?" 이어지는 47단계 포스팅에서는 "DB 배포 사고 0%에 도전한다!" 데이터베이스 마이그레이션의 절대 표준, Flyway(플라이웨이) 아키텍처 완벽 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!

반응형