백엔드 개발일지

[Spring] 멀티 모듈 프로젝트 도입하기

jisu0708 2023. 11. 20. 16:21

1. 단일 모듈 프로젝트의 문제점

장기 프로젝트의 경우, 시간이 지날수록 기능이 추가되고 무거워집니다. 그러면서 하나로 운영하던 서비스가 클라이언트 서비스와 운영 시스템으로 나뉘는 등의 분리가 일어나는데 여기서 각각의 프로그램에 있는 도메인의 동일성이 깨질수 있다는 문제점이 생깁니다.

 

예를 들어 하나로 운영하던 프로젝트에서 클라이언트 api와 운영 api 두가지로 나뉘었을 때, 회원에 대한 도메인은 각각의 프로젝트에서 동일해야 합니다. 하지만 두 개의 프로젝트는 별개로 관리가 되고 어느 한 쪽에서 도메인을 수정하면 다른 쪽에서도 도메인을 수정해야 합니다.

이 과정에서 어느 한 쪽에서 수정이 되지 않으면 도메인이 달라지는 경우가 생길 수 있으며 결국 프로젝트가 개발자에 대한 의존성이 높아지게 됩니다.

 

 

 

2. 멀티 모듈 프로젝트란?

프로그래밍에서 ‘모듈’은 프로그램을 구성하는 시스템을 기능 단위의 독립적인 부분으로 분리한 것을 의미합니다. 다시 말해  독립적으로 배포될 수 있는 코드의 단위입니다.

 

그리고 하나의 프로젝트 안에 독립적인 모듈을 여러 담은 구조를 멀티 모듈 프로젝트라 하고 이렇게 모듈들을 분산시키면 높은 응집도와 낮은 결합도를 가지게 되어 유지보수에 용이합니다. 또한 위에서 언급한 서로 독립된 프로젝트에서 발생하던 도메인의 동일성 보장 문제 또한 해결 있습니다.

 

 

 

3. 멀티 모듈 프로젝트 적용하기

1) 멀티 프로젝트 구조

이러한 멀티 모듈의 장점때문에 최근에 시작한 프로젝트에서도 멀티 모듈을 도입하였습니다.

* 해당 포스트에 대한 코드 내용은 여기서 확인할 수 있습니다.

(https://github.com/depromeet/14th-team5-BE/pull/2)

 

server: 루트 프로젝트
├── config: 프로젝트 설정 관리
│   ├── build
│   ├── build.gradle
│   └── src
├── member: Member 도메인
│   ├── build
│   ├── build.gradle
│   └── src
├── build
├── build.gradle
├── gradle
│   └── wrapper
├── gradlew
├── gradlew.bat
└── settings.gradle

 

멀티 모듈 프로젝트를 구성할 때 기능 별 혹은 도메인 별로 모듈을 나누어 구성할 수 있는데 이전에 진행한 프로젝트들을 도메인 별로 나누어 구성해왔고 이 방식이 팀원들 모두가 익숙하여 위와 같이 구성했습니다. 

(현재는 프로젝트 기획 초기 단계라 member(회원) 도메인만 존재한다는 가정 하에 설계했습니다.)

  • config 모듈은 런타임 환경 및 프로젝트에서 필요한 설정들을 관리하며 메인 런처를 가집니다
  • member 모듈은 회원 도메인과 관련한 패키지 및 코드를 가집니다

 

 

 

2) 멀티 모듈 프로젝트 생성

① 프로젝트 생성 및 모듈 추가

 

 

https://start.spring.io 에서 프로젝트를 구성하면 처음에는 위와 같이 구성됩니다.

(SpringBoot 3.1.5, Java 17, Gradle 기준입니다)

루트 프로젝트(server)는 여러 모듈을 담을 프로젝트이므로 src 폴더는 필요하지 않으므로 삭제합니다.

 

 

 

 

루트 프로젝트를 클릭한 채 필요한 모듈들을 생성합니다.

 

주의해야 할 점은 루트 아래의 하위 모듈들(ex. config, member...)은 같은 패키지 아래에 있어야 합니다.

만약 config라는 이름으로 모듈을 생성하면 package name도 자동으로 com.프로젝트이름.config로 설정되는데 member와 같은 패키지 아래에 있도록 하기 위해 표시한 부분인 'config'는 없애 모두 'com.프로젝트이름'으로 설정합니다. 

(물론 하위 모듈들이 다른 패키지 내에 존재해도 되지만 그럴 경우, 스캔할 패키지를 자동으로 찾지 못해서 @ComponentScan으로 직접 패키지 경로를 설정해줘야 합니다)

 

 

 

여기까지 왔다면 루트 프로젝트(server) 아래에 config, member 모듈이 존재하게 됩니다.

 

 

그리고 이 모듈 안에서도 settings.gradle, .gradle과 같은 필요하지 않는 파일들은 삭제하고 src 폴더, build.gradle만 남겨줍니다.

 

 

 

프로젝트 세팅

  • root/settings.gradle
rootProject.name = 'server'

include 'config' //추가
include 'member' //추가

 

기존에는 rootProject 설정만 되어있겠지만 include를 통해 하위 모듈까지 포함하도록 구성해줍니다.

 

 

  • root/build.gradle
plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.5'
	id 'io.spring.dependency-management' version '1.1.3'
}

repositories {
	mavenCentral()
}

// subprojects 안에는 하위 모듈에서 공통으로 적용할 사항들을 적어줍니다
subprojects {
	group = 'com.oing'
	version = '0.0.1-SNAPSHOT'
	sourceCompatibility = '17'

	apply plugin: 'java'
	apply plugin: 'org.springframework.boot'
	apply plugin: 'io.spring.dependency-management'

	configurations {
		compileOnly {
			extendsFrom annotationProcessor
		}
	}

	dependencies {
        	// 하위 모듈에서 공통으로 사용할 라이브러리
        	// ex. lombok, JPA...
		implementation 'org.springframework.boot:spring-boot-starter-web'
		implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
		compileOnly 'org.projectlombok:lombok'
		annotationProcessor 'org.projectlombok:lombok'
		testImplementation 'org.springframework.boot:spring-boot-starter-test'
	}
}

tasks.named('bootJar') {
	enabled = false // 루트 프로젝트에서는 중복된 실행 가능한 JAR 파일을 생성하는 것을 방지
}

tasks.named('jar') {
	enabled = true // 각 모듈의 라이브러리 JAR 파일을 생성하기 위해 킴
}

tasks.named('test') {
	useJUnitPlatform()
}

 

루트 프로젝트의 build.gradle 파일 안에서는 subprojects를 통해 하위 모듈에서 공통적으로 사용할 의존성에 대해 선언합니다.

멀티 모듈을 도입하는 이유가 모듈 간 의존성을 최소화하기 위해서이므로 공통으로 사용할 의존성에 대해서만 선언을 해줍니다.

이제 하위 모듈에서는 subprojects에 적힌 의존성들을 따로 명시하지 않아도 자동으로 적용됩니다.

 

 

  • root/config/build.gradle
plugins {
    id 'java'
}

// config 모듈의 build.gradle 파일
repositories {
    mavenCentral()
}

dependencies {
    // member 모듈에 대한 의존성을 선언
    implementation project(':member')
	
    // 데이터베이스 마이그레이션 의존성, 런타임에서만 사용하는 의존성
    implementation 'org.flywaydb:flyway-core'
    implementation 'org.flywaydb:flyway-mysql'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'
}

tasks.named('bootJar') {
    enabled = true // 실행 가능한 JAR 파일 생성을 활성화
}

tasks.named('jar') {
    enabled = true
}

tasks.named('test') {
    useJUnitPlatform()
}

 

프로젝트 환경 설정을 담당하고 메인 런처를 가지는 config 모듈의 build.gradle 파일입니다.

실제 런타임 구동 시에는 필요하지만, 앞의 추상화 계층이 따로 있거나 application.yaml에서 설정하는 의존성들은 config의 build.gradle에 추가합니다.

또한, config 모듈이 메인 런처를 가지므로 실행 가능한 JAR 파일을 생성할 수 있도록 활성화합니다.

 

 

  • root/member/build.gradle
repositories {
    mavenCentral()
}

dependencies {

}

tasks.named('bootJar') {
    enabled = false
}

tasks.named('jar') {
    enabled = true
}

 

member 도메인을 관리하는 member 모듈의 build.gradle 파일입니다.

이 파일도 필요한 의존성에 대해서만 명시해줘야 하지만 프로젝트 기획 초기라 member에서만 사용하는 기술이 확정되지 않아 아직 명시하지 않고 기본 설정만 해두었습니다.

 

 

  • gradle 리프레쉬를 통해 하위 모듈 추가

 

마지막으로 루트 프로젝트 아래에 하위 모듈들이 잘 구성되도록 gradle 리프레시를 합니다. 사진과 같이 루트 프로젝트 아래에 하위 모듈이 잘 위치해있다면 성공입니다.

 

 

 

root/build.gradle에서 subprojects에 선언한 의존성들(ex. JPA, lombok...) 또한 config, member 모듈에서도 적용됨을 알 수 있습니다. 👍