스프링부트

spring multi module 프로젝트 만들기

알쓸개잡 2023. 11. 19.

이번 포스팅은 Creating a Multi Module Project 가이드 문서의 내용을 정리 요약한 것이다.

Intellij IDE에서 Spring Boot로 다중 모듈 프로젝트를 생성하는 방법에 대해서 기록한다.

 

프로젝트에는 설정 파일의 내용을 주입받아 제공하는 라이브러리가 있고 해당 라이브러리를 사용하는 애플리케이션으로 2개의 프로젝트로 구성된다.

전체 소스 코드는 가이드 문서내에 github 링크가 포함되어 있다.

 

Root 프로젝트 생성

루트 프로젝트에는 두 개의 자식 프로젝트를 생성해야 한다. - library, application

library는 application 프로젝트에 종속된다.

 

Maven

Maven의 경우 <modules> 가 포함된 pom.xml을 root 프로젝트에 정의한다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-multi-module</artifactId>
    <version>0.1.0</version>
    <packaging>pom</packaging>

    <modules>
        <module>library</module>
        <module>application</module>
    </modules>

</project>

 

Gradle

Gradle의 경우 settings.gradle 에 프로젝트 설정을 정의한다.

rootProject.name = 'gs-multi-module'

include 'library'
include 'application'

선택사항으로 IDE가 루트 디렉토리를 식별하는데 도움이 되도록 빈 build.gradle을 root 프로젝트에 포함할 수 있다.

 

root 디렉토리로 사용할 디렉터리에 다음과 같은 하위 디렉터리 구조를 만든다.

└── library
└── application

 

 

Intellij에서 하위 프로젝트 생성

Intellij IDE에서 하위 프로젝트를 손쉽게 생성할 수 있다.

root 프로젝트에서 Cmd + n 단축키를 통해서 Module을 선택하여 하위 프로젝트를 생성할 수 있다.

Intellij 하위 프로젝트 생성

프로젝트 생성창을 통해서 프로젝트를 생성한다.

 

library 프로젝트 생성

library 프로젝트는 application 프로젝트가 사용할 라이브러리 역할을 한다.

library 디렉토리에 Maven 또는 Gradle 빌드 도구를 구성한다.

라이브러리는 애플리케이션으로 동작하지 않기 때문에 실행가능한 jar로 만들지 않아야 한다.

library 프로젝트의 디렉토리 구조는 다음과 같다.

└── src
    └── main
        └── java
            └── com
                └── example
                    └── multimodule
                        └── service

 

Maven (pom.xml)

library 프로젝트의 최종 pom.xml 은 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>library</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>library</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>17</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

</project>
  • Spring Boot 종속성 관리를 활용하기 위해서 spring-boot-starter-parent를 상위 프로젝트로 구성한다.
  • Spring initializer를 통해서 프로젝트를 생성하는 경우 기본적으로 실행가능한 jar를 빌드할 수 있는 스크립트가 포함되어 있으므로 pom.xml 에서 다음의 블록은 제거해야 한다.
<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
  </plugins>
</build>

 

root 프로젝트에 mvnw 파일, .mvn 디렉터리가 존재한다면 library 프로젝트에 mvnw,. mvn 은 제거해도 좋다.

 

Gradle (build.gradle)

library 프로젝트의 최종 build.gradle은 다음과 같다.

plugins {
	id 'org.springframework.boot' version '3.1.5' apply false
	id 'io.spring.dependency-management' version '1.1.3'
	id 'java'
}

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

java {
  sourceCompatibility = '17'
}

repositories {
	mavenCentral()
}

dependencyManagement {
	imports {
		mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
	}
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
  • 실행 가능한 jar로 만들지 않기 위해서 spring boot 플러그인은 비활성화 한다.
  • spring boot 종속성 관리를 유지하기 위해서 SpringBootPlugin.BOM_COORDINATES dependencyManagement를 추가한다.
  • 다음은 spring boot 플러그인은 비활성화하고 spring boot 종속성 관리는 유지하기 위한 스크립트다.
plugins {
	id 'org.springframework.boot' version '3.1.5' apply false
	id 'io.spring.dependency-management' version '1.1.3'
	...
}

dependencyManagement {
  imports {
    mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
  }
}
root 프로젝트에 gradlew 파일과, gradle 디렉터리가 존재한다면 library 프로젝트에 gradlew, gradle은 제거해도 좋다.

 

Service Component 생성

library는 애플리케이션에서 사용할 수 있는 MyService 클래스를 제공한다.

package com.example.multimodule.service;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Service;

@Service
@EnableConfigurationProperties(ServiceProperties.class)
public class MyService {

  private final ServiceProperties serviceProperties;

  public MyService(ServiceProperties serviceProperties) {
    this.serviceProperties = serviceProperties;
  }

  public String message() {
    return this.serviceProperties.getMessage();
  }
}

@EnableConfigurationProperties(ServiceProperties.class)를 통해서 ServiceProperties 클래스를 빈으로 생성하여 serviceProperties 멤버 변수에 주입받도록 하였다.

ServiceProperties 클래스는 service.message 속성을 주입받아 제공하는 ConfigurationProperties 역할을 하는 클래스다.

package com.example.multimodule.service;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("service")
public class ServiceProperties {

  /**
   * A message for the service.
   */
  private String message;

  public String getMessage() {
    return message;
  }

  public void setMessage(String message) {
    this.message = message;
  }
}

 

library 프로젝트의 MyService 클래스와 ServiceProperties 클래스를 library 내에서 Bean으로 생성하도록 하였다.
경우에 따라서는 library 내에서 MyService와 ServiceProperties를 일반 클래스로 정의하여 사용해야 할 수도 있는데 이런 경우에는 application 프로젝트 내에서 Configuration을 통해서 MyService와 ServiceProperties 클래스를 빈으로 등록하여 사용할 수도 있다.

 

 

application 프로젝트 생성

application 프로젝트는 library를 의존성으로 사용한다.

application 프로젝트의 디렉터리 구조는 다음과 같다.

└── src
    └── main
        └── java
            └── com
                └── example
                    └── multimodule
                        └── application

 

maven (pom.xml)

library 모듈의 클래스를 사용할 것이기 때문에 com.example:library:{version}을 의존성으로 추가할 것이다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.1.5</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>application</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>application</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>com.example</groupId>
			<artifactId>library</artifactId>
			<version>${project.version}</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

 

Gradle(build.gradle)

library 모듈의 기능을 사용할 것이기 때문에 library 프로젝트를 포함할 것이다.

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

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

java {
  sourceCompatibility = '17'
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation project(':library')
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

Application 코드

GET http://localhost:8080/ API를 호출하면 service.message 설정 값을 응답한다.

package com.example.multimodule.application;

import com.example.multimodule.service.MyService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication(scanBasePackages = "com.example.multimodule")
@RestController
public class DemoApplication {

  private final MyService myService;

  public DemoApplication(MyService myService) {
    this.myService = myService;
  }

  @GetMapping("/")
  public String home() {
    return myService.message();
  }

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

library의 MyService를 빈으로 주입받아야 하므로 @SpringBootApplication(scanBasePackages = "com.example.multimodule")로 설정하였다.

 

application.yml 설정은 다음과 같다.

service:
  message: "Hello multi module project"

 

테스트

다음 명령을 통해서 프로젝트를 빌드하고 실행한다. 프로젝트 root 디렉터리에서 실행한다.

# Gradle
$ ./gradlew build && ./gradlew :application:bootRun

# Maven
$ ./mvnw install && ./mvnw spring-boot:run -pl application

 

GET http://localhost:8080/ 을 호출하면 다음과 같은 응답을 받을 수 있다.

 

 

추가 사항

application 프로젝트에서 @Configuration을 통해서 library 클래스를 빈으로 등록하여 사용하기

  • library 프로젝트 내에서 MyService를 일반 클래스로 정의하고 application에서 @Configuration 을 통해 빈으로 사용할 수 있다.
  • ServiceProperties 클래스 파일을 단순 POJO 클래스로 정의하고 application 에서 설정 주입을 할 수 있다.

library의 MyService, ServiceProperties 클래스 수정

package com.example.spring.library.service;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class MyService {
	private final ServiceProperties properties;

	public String message() {
		return properties.getMessage();
	}
}

MyService 클래스를 일반 클래스로 정의하였다.

package com.example.spring.library.service;

import lombok.Getter;
import lombok.Setter;

public class ServiceProperties {
	@Getter
	@Setter
	private String message;
}

ServiceProperties 클래스를 단순 POJO 클래스로 정의하였다.

 

application 프로젝트의 @Configuration 클래스 추가

package com.example.spring.application.config;

import com.example.spring.library.service.MyService;
import com.example.spring.library.service.ServiceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class LibraryConfig {

	//ServiceProperties 클래스를 service.* 설정을 주입받는 빈으로 등록한다.
	@Bean
	@ConfigurationProperties(prefix = "service")
	public ServiceProperties serviceProperties() {
		return new ServiceProperties();
	}

	//ServiceProperties 클래스를 주입받은 MyService를 빈으로 등록한다.
	@Bean
	public MyService myService(ServiceProperties serviceProperties) {
		return new MyService(serviceProperties);
	}
}

혹은 ServiceProperties 클래스에 @ConfigurationProperties(prefix = "service") 애노테이션을 지정하고

LibraryConfig 클래스에서 @EnableConfigurationProperties(ServiceProperties.class) 애노테이션을 지정하면

LibraryConfig 클래스에서 serviceProperties 빈은 별도로 생성하지 않아도 된다.

 

application의 @SpringBootApplication에서 scanBasePackages 속성 제거

package com.example.spring.application;

import com.example.spring.library.service.MyService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
@RequiredArgsConstructor
public class Application {
	private final MyService myService;

	@GetMapping("/")
	public String message() {
		return myService.message();
	}

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

}

 

application 내에서 MyService, ServiceProperties 클래스를 빈으로 등록하여 사용하기 때문에 library의 MyService 패키지 경로를 component scan 할 필요가 없다.

 


관련링크

https://spring.io/guides/gs/multi-module/

 

Getting Started | Creating a Multi Module Project

You will want to write unit tests for your library components. If you provide re-usable Spring configuration as part of the library, you might also want to write an integration test, to make sure that the configuration works. To do that, you can use JUnit

spring.io

 

댓글

💲 추천 글