🛠️ 실습 환경
OS: macOS Sonoma 14.0 (arm64)
IDE: IntelliJ 2023.2.2
Build: Gradle📝 실습 목표
스프링부트 멀티 모듈 프로젝트 구성하기
자바에서 모듈(Module)
은 독립적으로 배포될 수 있는 코드의 단위를 말합니다.
멀티 모듈(Multi-Module)
이란 이러한 코드 뭉치를 하나의 프로젝트 안에서 관리하는것을 의미합니다.
멀티 모듈 안에서 각각의 모듈은 서로를 향한 의존성을 가질 수 있습니다.
여러개의 모듈을 생성할 때 반드시 멀티 모듈 프로젝트를 생성해야하진 않지만 코드 중복 제거, 모듈간 의존성을 위해 저장소에 배포하지 않아도 되는 점 등 장점이 있어 소개하고자 합니다.
💬 좀 더 자세히 알고싶어요!
멀티 모듈 프로젝트에 대해 자세한 내용은 다른 포스팅을 통해 소개하고 이번엔 멀티모듈 프로젝트를 생성하고 모듈간 의존성을 설정하는 법을 다루겠습니다.
multi-module
로 설정하고 자바 버전은 17, 스프링 부트 버전은 3.1.x로 설정합니다.💬 Maven Repository
Maven Repository를 통해 의존성을 검색하고 가져올 수 있습니다.
(1)
src
디렉토리 제거
(2)build.gradle
수정
(3)settings.gradle
수정
multi-module
모듈을 루트 모듈이라고 하겠습니다.build.gradle
파일 내 스크립트를 다음과 같이 수정합니다.plugins {
id 'java'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
}
bootJar.enabled = false // 빌드시 현재 모듈(multi-module)의 .jar를 생성하지 않습니다.
repositories {
mavenCentral()
}
subprojects { // 모든 하위 모듈들에 이 설정을 적용합니다.
group 'com.example'
version '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies { // 모든 하위 모듈에 추가 될 의존성 목록입니다.
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
}
module-
이라는 접두사를 달아주었습니다.settings.gradle
을 열어 각 모듈이 잘 추가됐는지 확인해줍니다.rootProject.name = 'multi-module'
include 'module-common'
include 'module-rest'
include 'module-websocket'
src
디렉토리를 제거해줍니다.src
디렉토리는 자동으로 생성됩니다.src
디렉토리는 아무런 역할을 하지 못하기때문에 제거해도, 그대로 둬도 무방합니다.build.gradle
스크립트를 수정합니다.bootJar.enabled = false
jar.enabled = true
dependencies {
// 필요한 의존성 추가
...
}
💬 bootJar와 jar는 뭔가요?
스프링 부트 2.5.0
부터 빌드 기본 옵션으로bootJar
와jar
를 모두 생성하게 바뀌었습니다.
이전까지 특별한 설정 없이 모듈을 빌드하게되면bootJar
Task만 진행되고jar
Task는 스킵됐었습니다.
bootJar
로 생성된.jar
는java -jar {.jar-file-name}
명령어로 실행되는Executable Archive
지만jar
로 생성된.jar
는 실행되지 않는Plain Archive
입니다.
Plain Archive: 애플리케이션 실행에 필요한 의존성을 제외한 모든 리소스 파일과 빌드된 소스코드의 클래스 파일만 포함됩니다.
Executable Archive: 애플리케이션 실행에 필요한 의존성까지 모두 포함됩니다.
module-common
처럼 실행이 필요하지 않은 모듈은bootJar
를 비활성화 하고jar
만 활성화 해줍니다.
package com.example;
import org.springframework.stereotype.Component;
@Component
public class TestBean {
public void dependencyTest() {
System.out.println("성공적으로 로딩됐습니다.");
}
}
build.gradle
스크립트를 수정합니다.dependencies {
// 필요한 의존성 추가
...
implementation project(':module-common')
}
💬 Dependency에서
implementation
와api
차이가장 큰 차이점은 전이 의존성을 허용하는지 여부입니다.
자세한 내용은 다른 포스팅을 통해 다루겠습니다.
Main.java
파일을 제거하고 새로운 실행 클래스를 작성합니다.package com.example;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RestApplication {
// 의존성 확인을 위한 코드 - 시작
private final TestBean testBean;
@Autowired
public RestApplication(TestBean testBean) {
this.testBean = testBean;
}
@PostConstruct
public void dependencyTest() {
testBean.dependencyTest();
}
// 의존성 확인을 위한 코드 - 끝
public static void main(String[] args) {
SpringApplication.run(RestApplication.class, args);
}
}
build.gradle
스크립트를 수정합니다.dependencies {
// 필요한 의존성 추가
...
implementation project(':module-common')
}
module-rest
에서 처럼 실행클래스를 생성합니다.package com.example;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WebSocketApplication {
// 의존성 확인을 위한 코드 - 시작
private final TestBean testBean;
@Autowired
public WebSocketApplication(TestBean testBean) {
this.testBean = testBean;
}
@PostConstruct
public void dependencyTest() {
testBean.dependencyTest();
}
// 의존성 확인을 위한 코드 - 끝
public static void main(String[] args) {
SpringApplication.run(WebSocketApplication.class, args);
}
}
💬 If. 모듈마다 루트 디렉토리가 다르다면?
common
모듈에서TestBean
이/com/common
에 있고rest
모듈에서 실행 클래스가/com/rest
디렉토리에 있다면 실행클래스에@ComponentScan
애노테이션으로 스프링 빈을 찾아볼 디렉토리를 추가해줍니다.
자세한 내용은 다른 포스팅에서 다루겠습니다.
module-rest
와 module-websocket
은 module-common
을 향한 의존성을 가지게 설정했습니다.module-rest
와 module-websocket
의 실행클래스에 모듈간 의존성 확인을 위한 코드를 입력했습니다./Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=64627:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/neo/workspace/test/multi-module/module-websocket/build/classes/java/main:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/3.1.5/a14cd17b86261933929566775d80c65b9f7440fc/spring-boot-starter-3.1.5.jar:/Users/neo/workspace/test/multi-module/module-common/build/classes/java/main:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/3.1.5/42a5b2ee98f700fba8d8c88d4af7b23266f1de0f/spring-boot-autoconfigure-3.1.5.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/3.1.5/c188015a5a79f5df65e876dcfdef16148c45fe2c/spring-boot-3.1.5.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/3.1.5/8d8a91061baa4347d97a8fe15f3337d943badab/spring-boot-starter-logging-3.1.5.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/jakarta.annotation/jakarta.annotation-api/2.1.1/48b9bda22b091b1f48b13af03fe36db3be6e1ae3/jakarta.annotation-api-2.1.1.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/6.0.13/cd565c2408e37d2026822b871cd43e69da8ec40e/spring-core-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/1.33/2cd0a87ff7df953f810c344bdf2fe3340b954c69/snakeyaml-1.33.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/6.0.13/4c49af6dde7fce9602049f952b45ca29f30e2a37/spring-context-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.4.11/54450c0c783e896a1a6d88c043bd2f1daba1c382/logback-classic-1.4.11.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.20.0/d37f81f8978e2672bc32c82712ab4b3f66624adc/log4j-to-slf4j-2.20.0.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/2.0.9/9ef7c70b248185845f013f49a33ff9ca65b7975/jul-to-slf4j-2.0.9.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/6.0.13/91ea90f2de4c71dac3cff04882156b00cdca3e0d/spring-jcl-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/6.0.13/aae1a18033787c9d324322f4470b12264e773e83/spring-aop-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/6.0.13/5b205c9f2fb07c1367db144ce7ab305f94300604/spring-beans-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/6.0.13/2bedffa4a3850bbbb652a31c47671824b17fbe01/spring-expression-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.4.11/2f9f280219a9922a74200eaf7138c4c17fb87c0f/logback-core-1.4.11.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.9/7cf2726fdcfbc8610f9a71fb3ed639871f315340/slf4j-api-2.0.9.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.20.0/1fe6082e660daf07c689a89c94dc0f49c26b44bb/log4j-api-2.20.0.jar com.example.WebSocketApplication
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.5)
2023-11-14T02:43:23.817+09:00 INFO 38238 --- [ main] com.example.WebSocketApplication : Starting WebSocketApplication using Java 17.0.6 with PID 38238 (/Users/neo/workspace/test/multi-module/module-websocket/build/classes/java/main started by neo in /Users/neo/workspace/test/multi-module)
2023-11-14T02:43:23.819+09:00 INFO 38238 --- [ main] com.example.WebSocketApplication : No active profile set, falling back to 1 default profile: "default"
성공적으로 로딩됐습니다.
2023-11-14T02:43:24.211+09:00 INFO 38238 --- [ main] com.example.WebSocketApplication : Started WebSocketApplication in 0.616 seconds (process running for 1.154)
Process finished with exit code 0
/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home/bin/java -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -Dmanagement.endpoints.jmx.exposure.include=* -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=65244:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/neo/workspace/test/multi-module/module-rest/build/classes/java/main:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/3.1.5/a14cd17b86261933929566775d80c65b9f7440fc/spring-boot-starter-3.1.5.jar:/Users/neo/workspace/test/multi-module/module-common/build/classes/java/main:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/3.1.5/42a5b2ee98f700fba8d8c88d4af7b23266f1de0f/spring-boot-autoconfigure-3.1.5.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/3.1.5/c188015a5a79f5df65e876dcfdef16148c45fe2c/spring-boot-3.1.5.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/3.1.5/8d8a91061baa4347d97a8fe15f3337d943badab/spring-boot-starter-logging-3.1.5.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/jakarta.annotation/jakarta.annotation-api/2.1.1/48b9bda22b091b1f48b13af03fe36db3be6e1ae3/jakarta.annotation-api-2.1.1.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/6.0.13/cd565c2408e37d2026822b871cd43e69da8ec40e/spring-core-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/1.33/2cd0a87ff7df953f810c344bdf2fe3340b954c69/snakeyaml-1.33.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/6.0.13/4c49af6dde7fce9602049f952b45ca29f30e2a37/spring-context-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.4.11/54450c0c783e896a1a6d88c043bd2f1daba1c382/logback-classic-1.4.11.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.20.0/d37f81f8978e2672bc32c82712ab4b3f66624adc/log4j-to-slf4j-2.20.0.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/2.0.9/9ef7c70b248185845f013f49a33ff9ca65b7975/jul-to-slf4j-2.0.9.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/6.0.13/91ea90f2de4c71dac3cff04882156b00cdca3e0d/spring-jcl-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/6.0.13/aae1a18033787c9d324322f4470b12264e773e83/spring-aop-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/6.0.13/5b205c9f2fb07c1367db144ce7ab305f94300604/spring-beans-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/6.0.13/2bedffa4a3850bbbb652a31c47671824b17fbe01/spring-expression-6.0.13.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.4.11/2f9f280219a9922a74200eaf7138c4c17fb87c0f/logback-core-1.4.11.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.9/7cf2726fdcfbc8610f9a71fb3ed639871f315340/slf4j-api-2.0.9.jar:/Users/neo/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.20.0/1fe6082e660daf07c689a89c94dc0f49c26b44bb/log4j-api-2.20.0.jar com.example.RestApplication
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.5)
2023-11-14T02:45:55.327+09:00 INFO 38259 --- [ main] com.example.RestApplication : Starting RestApplication using Java 17.0.6 with PID 38259 (/Users/neo/workspace/test/multi-module/module-rest/build/classes/java/main started by neo in /Users/neo/workspace/test/multi-module)
2023-11-14T02:45:55.328+09:00 INFO 38259 --- [ main] com.example.RestApplication : No active profile set, falling back to 1 default profile: "default"
성공적으로 로딩됐습니다.
2023-11-14T02:45:55.556+09:00 INFO 38259 --- [ main] com.example.RestApplication : Started RestApplication in 0.426 seconds (process running for 0.97)
Process finished with exit code 0
간단한 멀티 모듈 프로젝트를 생성하고 모듈간 의존성을 설정하는 방법을 알아보았습니다.
혹시나 오류가 발생한다거나 결과가 올바르지 않다면 중간에 오탈자는 없었는지, 빠진건 없는지 한번 다시 확인해 주시고 이해가 안되는 부분이 있다면 댓글로 남겨주시면 확인하는 대로 답변 달겠습니다.
추가로 본문 중 "다른 포스팅에서 다루겠습니다."고 한 내용은 빠르게 추가해 링크로 연결해두겠습니다.
잘 읽었습니다. 좋은 정보 감사드립니다.