[Spring] 스프링부트로 멀티 모듈 프로젝트 진행하기

neo-the-cow·2023년 11월 13일
7

Spring

목록 보기
1/2
post-thumbnail

0. 들어가기 전에

  • 스프링부트 프로젝트를 멀티 모듈 프로젝트로 만들어봅니다.
  • 실습 환경과 목표는 다음과 같습니다.
  • 공부하며 기록하는탓에 오개념이 있을 수 있습니다. 댓글, 이메일로 알려주시면 더 공부하고 잘 반영하겠습니다.

🛠️ 실습 환경

OS: macOS Sonoma 14.0 (arm64)
IDE: IntelliJ 2023.2.2
Build: Gradle

📝 실습 목표

스프링부트 멀티 모듈 프로젝트 구성하기

0.1 멀티 모듈이란?

자바에서 모듈(Module)은 독립적으로 배포될 수 있는 코드의 단위를 말합니다.
멀티 모듈(Multi-Module)이란 이러한 코드 뭉치를 하나의 프로젝트 안에서 관리하는것을 의미합니다.
멀티 모듈 안에서 각각의 모듈은 서로를 향한 의존성을 가질 수 있습니다.
여러개의 모듈을 생성할 때 반드시 멀티 모듈 프로젝트를 생성해야하진 않지만 코드 중복 제거, 모듈간 의존성을 위해 저장소에 배포하지 않아도 되는 점 등 장점이 있어 소개하고자 합니다.

💬 좀 더 자세히 알고싶어요!

멀티 모듈 프로젝트에 대해 자세한 내용은 다른 포스팅을 통해 소개하고 이번엔 멀티모듈 프로젝트를 생성하고 모듈간 의존성을 설정하는 법을 다루겠습니다.

1. 프로젝트 시작 하기

1.1 프로젝트 생성

  • 새로운 프로젝트를 생성해줍니다.
  • 프로젝트 이름은 multi-module로 설정하고 자바 버전은 17, 스프링 부트 버전은 3.1.x로 설정합니다.
  • 당장 사용 할 의존성은 정해지지 않았으므로 별도의 설정없이 프로젝트를 생성해주면 됩니다

💬 Maven Repository

Maven Repository를 통해 의존성을 검색하고 가져올 수 있습니다.

1.2 멀티 모듈 프로젝트를 위한 설정

1.2.1 수정 포인트 확인

  • 확인할 포인트는 총 3부분 입니다.
  • 다음 세가지 부분을 잘 기억했다가 포스팅을 따라오며 확인하면 됩니다.

    (1) src 디렉토리 제거
    (2) build.gradle 수정
    (3) settings.gradle 수정

1.2.2 빌드 스크립트 수정

  • 지금부터 multi-module 모듈을 루트 모듈이라고 하겠습니다.
  • build.gradle 파일 내 스크립트를 다음과 같이 수정합니다.
  • 더 좋은 방법, 가독성 좋은 방법이 있다면 적용하셔도 됩니다.
  • Groovy 문법을 잘 이해하고 있는게 아니라 다음과 같이 설정했고 세부 사항은 주석으로 작성하겠습니다.
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()
    }
}

1.2.3 모듈 추가 하기

  • 위와같이 하나의 공통 모듈에 의존하는 형태로 구성해보겠습니다.




  • 각 모듈을 추가하면 다음과 같이 모듈이 생성됩니다.
  • 모듈의 네이밍은 특별한 규칙이 없습니다만 가독성을 위해 module-이라는 접두사를 달아주었습니다.

1.2.4 프로젝트 세팅 스크립트 확인

  • 루트 모듈 내 새로운 모듈이 생성됐다면 settings.gradle을 열어 각 모듈이 잘 추가됐는지 확인해줍니다.
rootProject.name = 'multi-module'
include 'module-common'
include 'module-rest'
include 'module-websocket'

1.2.5 루트 모듈 내 src 디렉토리 제거

  • 루트 모듈 내 src 디렉토리를 제거해줍니다.
  • 사실 새 모듈을 추가할 때 마다 루트 모듈의 src 디렉토리는 자동으로 생성됩니다.
  • 하지만 루트모듈은 하위 모듈을 담는 그릇처럼 사용되고 루트 모듈 내 src 디렉토리는 아무런 역할을 하지 못하기때문에 제거해도, 그대로 둬도 무방합니다.

1.3 각 모듈별 설정

1.3.1 module-common

1.3.1.1 빌드 스크립트 수정

  • build.gradle 스크립트를 수정합니다.
bootJar.enabled = false

jar.enabled = true

dependencies {
	// 필요한 의존성 추가
	...
}

💬 bootJar와 jar는 뭔가요?

스프링 부트 2.5.0 부터 빌드 기본 옵션으로 bootJarjar를 모두 생성하게 바뀌었습니다.
이전까지 특별한 설정 없이 모듈을 빌드하게되면 bootJar Task만 진행되고 jar Task는 스킵됐었습니다.
bootJar로 생성된 .jarjava -jar {.jar-file-name} 명령어로 실행되는 Executable Archive지만 jar로 생성된 .jar는 실행되지 않는 Plain Archive입니다.
Plain Archive: 애플리케이션 실행에 필요한 의존성을 제외한 모든 리소스 파일과 빌드된 소스코드의 클래스 파일만 포함됩니다.
Executable Archive: 애플리케이션 실행에 필요한 의존성까지 모두 포함됩니다.
module-common처럼 실행이 필요하지 않은 모듈은 bootJar를 비활성화 하고 jar만 활성화 해줍니다.

1.3.1.2 테스트를 위한 빈 등록

package com.example;

import org.springframework.stereotype.Component;

@Component
public class TestBean {
    
    public void dependencyTest() {
        System.out.println("성공적으로 로딩됐습니다.");
    }

}

1.3.2 module-rest

1.3.2.1 빌드 스크립트 수정

  • build.gradle 스크립트를 수정합니다.
dependencies {
	// 필요한 의존성 추가
	...
    implementation project(':module-common')
}

💬 Dependency에서 implementationapi 차이

가장 큰 차이점은 전이 의존성을 허용하는지 여부입니다.
자세한 내용은 다른 포스팅을 통해 다루겠습니다.

1.3.2.2 메인 클래스 작성

  • 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);
    }

}

1.3.3 module-websocket

1.3.3.1 빌드 스크립트 수정

  • build.gradle 스크립트를 수정합니다.
dependencies {
	// 필요한 의존성 추가
	...
    implementation project(':module-common')
}

1.3.3.2 메인 클래스 작성

  • 앞서 살펴본 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애노테이션으로 스프링 빈을 찾아볼 디렉토리를 추가해줍니다.
자세한 내용은 다른 포스팅에서 다루겠습니다.

1.4 모듈간 의존성 확인하기

  • 앞서 module-restmodule-websocketmodule-common을 향한 의존성을 가지게 설정했습니다.
  • module-restmodule-websocket의 실행클래스에 모듈간 의존성 확인을 위한 코드를 입력했습니다.
  • 메인 메서드를 실행해 콘솔창에 찍힌 로그로 확인해봅니다.

1.4.1 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

1.4.2 module-rest 콘솔 로그

/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

2. 맺음말

간단한 멀티 모듈 프로젝트를 생성하고 모듈간 의존성을 설정하는 방법을 알아보았습니다.
혹시나 오류가 발생한다거나 결과가 올바르지 않다면 중간에 오탈자는 없었는지, 빠진건 없는지 한번 다시 확인해 주시고 이해가 안되는 부분이 있다면 댓글로 남겨주시면 확인하는 대로 답변 달겠습니다.

추가로 본문 중 "다른 포스팅에서 다루겠습니다."고 한 내용은 빠르게 추가해 링크로 연결해두겠습니다.

profile
Hi. I'm Neo

8개의 댓글

comment-user-thumbnail
2023년 11월 13일

잘 읽었습니다. 좋은 정보 감사드립니다.

1개의 답글
comment-user-thumbnail
2024년 9월 8일

좋은 내용 잘 읽었습니다.

1개의 답글
comment-user-thumbnail
2024년 9월 11일

감사합니다 최고에요

1개의 답글
comment-user-thumbnail
2024년 9월 19일

첫 멀티모듈 프로젝트 해보는데 큰 도움이 되었습니다! 감사해요!

1개의 답글