개발끄적

MSA 환경에서 살아남기 위한 멀티모듈 구조

누구세연 2025. 3. 22. 22:03

실제 MSA 환경에서 하나의 DB를 여러 서비스가 동시에 접근하면서, 각 레포지토리에서 중복된 방식으로 데이터를 다루는 일이 잦아졌습니다.
이로 인해 데이터 관리가 복잡해지고, 리팩토링 또한 점점 어려워지는 문제를 경험하게 되었습니다..😥

“이런 구조를 조금 더 효율적으로 개선할 수는 없을까?”라는 고민을 하던 중 멀티모듈 구조를 알게 되었고, 이를 실무에 적용해 보면서 얻은 경험과 개념들을 정리해보고자 합니다.

 

 

왜 멀티모듈 구조를 사용하는가?

1. 단일 모듈 구조의 한계

하나의 프로젝트에 모든 도메인과 기능을 넣는 단일 모듈 방식은 규모가 작을 때는 문제가 없지만, 기능이 점점 많아지면서 관리가 어려워지고 코드 의존성도 높아지게 됩니다. 특히 대규모 서비스에서는 유지보수가 어려워지고, 모듈 간 책임이 불명확해지며 테스트 환경 구성도 복잡해집니다.

2. 멀티모듈 구조란?

멀티모듈은 하나의 프로젝트를 여러 하위 모듈로 나누어 각자의 책임을 갖게 하는 구조입니다. 이 구조는 모듈 간 명확한 경계를 두어, 도메인 중심 설계(DDD)나 MSA 환경에서도 효율적인 서비스 구성이 가능합니다.

특히 domain 모듈은 인프라에 대한 정보를 전혀 알지 못하게 설계하고, infra 모듈에서 domain의 인터페이스를 구현하는 구조를 따르는 것이 중요합니다.

이를 통해 의존성 역전(DIP) 원칙을 자연스럽게 적용할 수 있으며, domain → infra 방향의 의존성을 피할 수 있습니다.


멀티모듈 구성 및 구조 예시

멀티모듈은 각 책임을 분리하여 개발, 테스트, 유지보수가 쉬운 구조를 만드는 데 목적이 있습니다.

📁 기본 구성 및 역할

  • domain: 핵심 도메인 모델 및 인터페이스 정의. 기술 스택에 대한 의존이 없고, 가장 순수한 계층입니다.
  • application: 비즈니스 로직을 구현하는 서비스 계층. 도메인 인터페이스를 조합하여 흐름을 구성합니다.
  • infra: 외부 시스템 연동 구현체(DB, Redis, Kafka 등). domain의 인터페이스를 구현합니다.
  • api: 컨트롤러와 외부 요청 진입 지점. application의 로직을 호출합니다.
  • common: 공통 예외, 응답 포맷, 유틸 클래스 등 재사용 가능한 요소를 포함합니다.

📂 프로젝트 구조 예시

my-multi-module-project/
 ┣ 📂 domain         ← 도메인 모델 & 인터페이스
 ┣ 📂 application    ← 비즈니스 서비스 로직 (UseCase 등)
 ┣ 📂 infra          ← 외부 시스템 연동 구현체 (DB, Redis 등)
 ┣ 📂 api            ← 컨트롤러 계층 (사용자 요청 처리)
 ┣ 📂 common         ← 공통 유틸, 에러 핸들링, 상수 등

🔁 각 모듈 간 순환 참조를 방지하고 단방향 의존성(api → application → domain, infra → domain)을 유지하는 것이 핵심입니다.

✅ 구조 내 테스트 전략 & 팁

  • domain: 단위 테스트 중심 (비즈니스 로직 테스트)
  • application: 서비스 흐름 테스트 (Mock 도메인 인터페이스 활용)
  • infra: 외부 연동 Mock 또는 TestContainer
  • api: 통합 테스트 또는 WebMvcTest 활용

공통 테스트 유틸 또는 설정은 test-support라는 별도 모듈로 분리하면 효율적입니다.


멀티모듈 적용 방법

간단한 예시로 어떻게 멀티 모듈을 적용할 수 있을지 작성해 보겠습니다.

1️⃣ 루트 프로젝트 생성

먼저 루트 프로젝트 디렉토리를 하나 생성합니다. 이 프로젝트가 모든 하위 모듈을 관리하는 최상위 디렉토리가 됩니다.

my-multi-module-project/
├── build.gradle (루트)
├── settings.gradle
├── domain/
├── application/
├── api/
├── infra/
└── common/

2️⃣ settings.gradle 설정

각 하위 모듈을 프로젝트에 등록합니다.

rootProject.name = 'my-multi-module-project'
include 'domain', 'application', 'infra', 'api', 'common'

3️⃣ 각 모듈 디렉토리 구조 생성

src 디렉토리는 루트 프로젝트가 아닌 각 하위 모듈에서 생성해 줍니다.

domain/
├── src/main/java
├── src/main/resources
└── build.gradle

api, common, infra 모듈도 동일한 구조로 생성합니다.

4️⃣ 각 모듈의 build.gradle 작성 예시

domain/build.gradle

plugins {
    id 'java'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
}
  • 도메인 계층에서 JPA와 관련된 기능만 사용하며, 다른 모듈을 참조하지 않습니다.

application/build.gradle

plugins {
    id 'java'
}

dependencies {
    implementation project(':domain')
}
  • 비즈니스 흐름을 구성하는 서비스 계층으로, 도메인만 참조합니다.

common/build.gradle

plugins {
    id 'java'
}

dependencies {
    implementation 'org.apache.commons:commons-lang3:3.12.0'
}
  • 공통 유틸, 상수, 예외 클래스 등을 포함하는 모듈입니다. 모든 모듈에서 참조 가능.

infra/build.gradle

plugins {
    id 'java'
}

dependencies {
    implementation project(':domain')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.kafka:spring-kafka'
}
  • 인프라와 관련된 DB, Redis, Kafka 등 외부 리소스를 다루며 도메인 로직의 구현체를 포함합니다.

common/build.gradle

plugins {
    id 'java'
}

dependencies {
    implementation 'org.apache.commons:commons-lang3:3.12.0'
}
  • 공통 유틸, 상수, 예외 클래스 등을 포함하는 모듈입니다. 모든 모듈에서 참조 가능.

api/build.gradle

plugins {
    id 'org.springframework.boot' version '3.1.0'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

dependencies {
    implementation project(':application')
    implementation project(':infra')
    implementation project(':common')
    implementation 'org.springframework.boot:spring-boot-starter-web'
}
  • 실제 실행 주체이며, 외부와 연결되는 모든 컨트롤러 및 서비스가 이곳에 존재합니다.

5️⃣ 루트 build.gradle 작성 예시

buildscript {
    ext {
        springBootVersion = '3.1.0'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}"
    }
}

allprojects {
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'

    repositories {
        mavenCentral()
    }
}

subprojects {
    apply plugin: 'java'
    sourceCompatibility = '17'
    targetCompatibility = '17'
}
  • 루트에서는 공통 설정과 버전 정보를 관리하고, 각 모듈에 중복 설정을 줄이기 위해 subprojects 블록을 사용합니다.

6️⃣ 메인 애플리케이션 클래스 작성 (api 모듈)

@SpringBootApplication
public class ApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class, args);
    }
}

이 구성을 통해 각 모듈은 독립성과 책임을 갖고 나눠지며, 유지보수성과 테스트 효율이 크게 향상됩니다. 🚀


멀티모듈 적용 경험

✅ 적용 배경

  • 여러 레포지토리에서 같은 DB를 접근하다 보니 도메인 중복, 로직 중복, 관리 포인트 증가 등의 문제가 발생했습니다.
  • 이를 해결하기 위해 도메인 중심으로 코드를 분리하고, 각 서비스가 필요한 기능만 가져다 쓰는 구조가 필요했습니다.

✅ 적용 효과

  • 도메인별 코드 책임이 명확해져서 유지보수와 테스트가 훨씬 쉬워졌습니다.
  • 불필요한 의존성이 줄고, 공통 로직은 common 모듈로 일원화되어 중복 코드가 감소했습니다.
  • 도메인 수정 시 영향 범위를 예측하기 쉬워졌고, MSA 구조 내에서도 모듈 재활용성이 좋아졌습니다.

실무에서 마주친 어려움과 팁

❗️ 도메인 간 의존성 순환 문제

도메인끼리 직접 참조하면 순환 참조 문제가 생길 수 있습니다. 이를 방지하려면 구조를 명확히 나누고, 공통 인터페이스로 분리하거나 이벤트 기반 연동을 고려해야 합니다.

❗️ 테스트 환경 복잡도 증가

각 모듈별로 별도 테스트 설정이 필요할 수 있습니다. 테스트 설정을 공통화하거나, test 모듈을 따로 두는 방식도 고려해 볼 수 있습니다.

✅ 추천 팁

  • 공통 응답/에러 포맷은 common 모듈에 통합
  • 모듈 간 의존성은 최소화 + 단방향으로 유지
  • 도메인 모듈은 다른 어떤 모듈도 참조하지 않도록 설계 (의존성 역전의 원칙 적용)

 

 

 

멀티모듈 구조는 프로젝트가 커질수록 강력한 아키텍처 전략이 될 수 있습니다. 초반엔 약간의 설정과 고민이 필요하지만, 유지보수성과 확장성을 생각하면 충분히 가치 있는 구조입니다.
이 글에서는 멀티모듈의 구조 설계와 실무 적용 경험을 바탕으로 구성 요소, 설정 방법, 주의할 점을 정리해 봤습니다. 앞으로 도메인 분리와 모듈화가 필요한 프로젝트를 계획 중이라면, 멀티모듈 구조를 꼭 한번 고려해 보시길 추천드립니다. 🙌