Guide_Creating a Multi Module Project

Dev.Hammy·2023년 12월 8일
0

Spring Guides

목록 보기
1/46

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

이 가이드에서는 Spring Boot를 사용하여 다중 모듈 프로젝트를 만드는 방법을 보여줍니다. 프로젝트에는 라이브러리 jar와 라이브러리를 사용하는 기본 애플리케이션이 있습니다. 또한 이를 사용하여 라이브러리(즉, 애플리케이션이 아닌 jar 파일)를 자체적으로 빌드하는 방법을 확인할 수도 있습니다.

What You Will Build

You will set up a library jar that exposes a service for simple “Hello, World” messages and then include the service in a web application that uses the library as a dependency.

프로젝트 생성

sdk java 17
language java
build gradle-groovy
gradle Wrapper 8.5
group 'guides'
project name 'multi-module'

build.gradle [root]

plugins {
    id 'java'
}

group = 'guides'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

setting.gradle [root]

rootProject.name = 'multi-module'

include 'library'
include 'application'

rootProject.name만 있던 setting.gradle 파일에 include할 디렉터리 이름 두 개를 추가합니다.

루트 디렉터리 아래 library 디렉터리 추가

모듈이 아닌 디렉터리를 추가합니다. 가이드와는 달리 저는 경로를 'library/src/main/java/guides/multimodule/service'라고 지정했습니다.
library 디렉터리의 루트 즉, src와 같은 깊이에 build.gradle을 추가합니다.

build.gradle [library]

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

group = 'guides'
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'

//튜토리얼에는 위 두개의 의존성만 기재하고 있지만 스프링 3에서는 junit4가 아닌 junit5를 사용하므로 아래 두개의 의존성과 test {use JUnitPlatform()} 도 그대로 기재합니다.

    testImplementation platform('org.junit:junit-bom:5.9.1')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
    useJUnitPlatform()
}

주어진 Gradle 스크립트는 Spring Boot 플러그인을 적용하지 않도록 설정되어 있습니다. apply false로 지정된 부분에서 Spring Boot 플러그인을 적용하지 않도록 설정하고 있습니다. 이 설정 때문에 해당 Gradle 빌드 스크립트를 실행하여 실행 가능한 JAR 파일을 생성하는 기능이 비활성화될 수 있습니다.

Spring Boot 플러그인은 org.springframework.boot 플러그인 ID로 선언되며, 이것이 JAR 파일을 만들기 위한 많은 기능을 활성화합니다. 따라서 해당 설정을 비활성화하면 Spring Boot의 기능을 사용할 수 없게 됩니다.

예를 들어, bootJar 또는 jar 태스크를 사용하여 실행 가능한 JAR 파일을 생성하는 등의 작업이 Spring Boot 플러그인을 통해 수행될 수 있습니다. 설정에서 apply false로 Spring Boot 플러그인을 적용하지 않으면 이러한 태스크가 활성화되지 않을 수 있습니다.

디렉터리의 마지막 레벨을 보면 main 클래스가 생성되지 않은 것을 확인할 수 있습니다.

BOM(Bill of Materials)

Maven과 Gradle의 BOM(Bill of Materials)은 프로젝트에서 사용하는 종속성의 버전을 관리하는 방법입니다.

  1. Maven BOM:

    • Maven 프로젝트에서 사용하는 방법으로, POM 파일에 종속성을 정의하고, 이러한 종속성들의 버전을 일관되게 유지할 수 있도록 BOM을 사용합니다.
    • Maven BOM은 일련의 종속성 그룹을 정의하고, 해당 그룹에 속하는 모든 종속성의 버전을 명시합니다. 이를 통해 프로젝트 전체에서 일관된 버전을 사용할 수 있습니다.
  2. Gradle Native BOM:

    • Gradle 빌드 시스템에서 사용하는 방법으로, Maven BOM과 유사한 개념을 가지고 있습니다. Gradle에서도 BOM을 활용하여 종속성의 버전을 관리할 수 있습니다.
    • Gradle Native BOM은 Gradle의 특정 플러그인이나 기능에서 사용하는 종속성의 버전을 관리하는 데 사용됩니다. Maven BOM과 유사한 목적으로 사용되지만 Gradle의 문법과 구조에 맞게 적용됩니다.

이러한 BOM은 종속성 버전을 일관되게 유지하고, 프로젝트 전체에서 특정 종속성 그룹의 버전을 간편하게 업데이트하거나 변경할 수 있도록 도와줍니다. Maven과 Gradle은 각각의 빌드 도구에서 사용되는 BOM의 개념을 지원하며, 프로젝트의 특정 요구 사항에 따라 선택하여 활용할 수 있습니다.


https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/

https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#managing-dependencies.dependency-management-plugin.using-in-isolation

3.1.2. Spring Boot의 종속성 관리를 독립적으로 사용하기

Spring Boot의 종속성 관리는 해당 프로젝트에 Spring Boot 플러그인을 적용하지 않고도 사용할 수 있습니다. SpringBootPlugin 클래스는 BOM의 그룹 ID, artifact ID, 또는 버전을 알 필요 없이 BOM을 가져올 수 있는 BOM_COORDINATES 상수를 제공합니다.

먼저, 프로젝트를 Spring Boot 플러그인에 의존하도록 구성하되 적용하지 않습니다.

plugins {
    id 'org.springframework.boot' version '3.2.0' apply false
}

Spring Boot 플러그인의 종속성 관리 플러그인 의존성은 의존성 관리 플러그인을 선언하지 않고도 사용할 수 있게 합니다. 또한, Spring Boot가 사용하는 의존성 관리 플러그인과 동일한 버전을 자동으로 사용하게 됩니다.

의존성 관리 플러그인을 적용한 다음 Spring Boot의 bom을 가져올 수 있도록 구성합니다.

apply plugin: 'io.spring.dependency-management'

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

위 코드는 조금 어색합니다. 그 이유는 우리가 의존성 관리 플러그인을 적용하는 명령형 방식을 사용하기 때문입니다.

코드를 좀 더 간결하게 만들기 위해 루트 부모 프로젝트에서 플러그인을 적용하거나 Spring Boot 플러그인과 같은 방식으로 plugins 블록을 사용할 수 있습니다. 이 방법의 단점은 의존성 관리 플러그인의 버전을 명시적으로 지정해야 한다는 것입니다.

plugins {
    java
    id("org.springframework.boot") version "3.2.0" apply false
    id("io.spring.dependency-management") version "1.1.4"
}

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

3.2. Gradle의 BOM 지원을 통한 종속성 관리

Gradle은 bom을 사용하여 프로젝트의 버전을 관리할 수 있도록 해줍니다. platform 또는 enforcedPlatform 종속성으로 선언하여 bom을 사용할 수 있습니다. platform 종속성은 bom의 버전을 권장사항으로 취급하며, 종속성 그래프의 다른 버전 및 제약 사항이 해당 bom에 선언된 버전 이외의 버전을 사용할 수 있도록 합니다. enforcedPlatform 종속성은 bom에 선언된 버전을 요구 사항으로 취급하며, 종속성 그래프에서 해당 버전 이외의 모든 버전을 무시합니다.

SpringBootPlugin 클래스는 BOM_COORDINATES 상수를 제공하여 Spring Boot의 bom에 대한 의존성을 선언할 수 있도록 합니다. 버전이나 그룹 ID, artifact ID를 알 필요 없이 사용할 수 있습니다.

dependencies {
    implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
}

platform 또는 enforced platform은 선언된 구성(Configuration)에서만 버전을 제약하며, 선언된 구성에서 확장된 구성으로만 적용됩니다. 따라서 하나 이상의 구성에서 동일한 종속성을 선언해야 할수도 있습니다.

3.2.1. 관리되는 버전 사용자 정의
Gradle의 bom 지원을 사용할 때 spring-boot-dependencies에서 프로퍼티를 사용하여 버전을 제어할 수 없습니다. 대신 Gradle이 제공하는 메커니즘 중 하나인 해결 전략을 사용해야 합니다. 그 중 하나인 해결 전략을 사용하여 org.slf4j 그룹의 모듈들의 버전을 제어하는 방법은 다음과 같습니다.

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if (details.requested.group == 'org.slf4j') {
            details.useVersion '1.7.20'
        }
    }
}

각 Spring Boot 릴리즈는 특정 집합의 서드파티 종속성과 설계, 테스트되었습니다. 버전을 재정의하면 호환성 문제가 발생할 수 있으므로 신중하게 사용해야 합니다.


library 디렉터리 : MyService, ServiceProperties 클래스 생성

library는 application이 사용할 MyService.java를 제공할 것입니다.


package guides.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();
  }
}

MyService.java가 사용할 ServiceProperties 클래스도 같은 깊이에 생성합니다.

package guides.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;
    }
}

이런 식으로 할 필요는 없습니다. 라이브러리는 Spring 기능이 아닌 순수 Java API만 제공할 수 있습니다. 이 경우 라이브러리를 사용하는 애플리케이션은 구성 자체를 제공해야 합니다.

@ConfigurationProperties는 스프링 부트에서 외부 설정 파일의 값을 자바 빈에 바인딩하기 위해 사용됩니다. 이를 통해 외부 설정을 해당 클래스의 인스턴스로 자동 매핑할 수 있습니다. 보통 @ConfigurationProperties를 사용할 때는 @EnableConfigurationProperties를 사용하여 해당 클래스를 활성화합니다.

@ConfigurationProperties("service")는 속성 파일(application.properties)이나 환경 변수에서 service.*와 일치하는 속성을 찾습니다. 이 코드에서는ServiceProperties 클래스의 필드와 매핑하려고 시도합니다. 만약 service.*와 매치되는 속성이 없다면 그 필드들은 디폴트 값(null 또는 primitive type의 default 값)으로 초기화됩니다.

그러나 위 코드에서는 MyService 클래스의 생성자를 통해 ServiceProperties를 주입받고 있기 때문에, 외부 프로퍼티를 바로 사용하고 있습니다. 따라서 @ConfigurationProperties를 사용하지 않고 의존성 주입을 통해 값을 가져올 수 있습니다. 이러한 경우 @ConfigurationProperties@EnableConfigurationProperties를 사용할 필요는 없습니다.

참고

1. @Import

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Indicates one or more <em>component classes</em> to import &mdash; typically
 * {@link Configuration @Configuration} classes.
 *
 * <p>Provides functionality equivalent to the {@code <import/>} element in Spring XML.
 * Allows for importing {@code @Configuration} classes, {@link ImportSelector} and
 * {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component
 * classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).
 *
 * <p>{@code @Bean} definitions declared in imported {@code @Configuration} classes should be
 * accessed by using {@link org.springframework.beans.factory.annotation.Autowired @Autowired}
 * injection. Either the bean itself can be autowired, or the configuration class instance
 * declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly
 * navigation between {@code @Configuration} class methods.
 *
 * <p>May be declared at the class level or as a meta-annotation.
 *
 * <p>If XML or other non-{@code @Configuration} bean definition resources need to be
 * imported, use the {@link ImportResource @ImportResource} annotation instead.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.0
 * @see Configuration
 * @see ImportSelector
 * @see ImportBeanDefinitionRegistrar
 * @see ImportResource
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * {@link Configuration @Configuration}, {@link ImportSelector},
	 * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
	 */
	Class<?>[] value();

}

해당 코드는 @Import 어노테이션을 정의하고 있습니다. 이 어노테이션은 하나 이상의 컴포넌트 클래스를 가져오는 데 사용됩니다. 주로 @Configuration 클래스들을 가져오는 데 사용되며, Spring XML의 <import/> 요소와 유사한 기능을 제공합니다.

@Import 어노테이션을 통해 다음을 가져올 수 있습니다:

  • @Configuration 클래스들
  • ImportSelector
  • ImportBeanDefinitionRegistrar 구현체
  • 4.2부터는 일반 컴포넌트 클래스들 (AnnotationConfigApplicationContext#register와 유사)

이 어노테이션은 클래스 수준이거나 메타 어노테이션으로 사용될 수 있습니다.

가져온 @Configuration 클래스에서 선언된 @Bean 정의는 @Autowired 주입을 통해 접근할 수 있습니다. 주입할 빈 자체를 @Autowired로 설정할 수 있거나, 빈을 선언하는 구성 클래스 인스턴스를 @Autowired로 설정할 수도 있습니다. 후자의 방법을 사용하면 @Configuration 클래스 메서드 간에 명시적이고 IDE에서 쉽게 이동할 수 있습니다.

XML 또는 다른 비 @Configuration 빈 정의 리소스를 가져와야 하는 경우에는 @ImportResource 어노테이션을 대신 사용해야 합니다.

2. ImportBeanDefinitionRegistrar.java

package org.springframework.context.annotation;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.type.AnnotationMetadata;

/**
 * Interface to be implemented by types that register additional bean definitions when
 * processing @{@link Configuration} classes. Useful when operating at the bean definition
 * level (as opposed to {@code @Bean} method/instance level) is desired or necessary.
 *
 * <p>Along with {@code @Configuration} and {@link ImportSelector}, classes of this type
 * may be provided to the @{@link Import} annotation (or may also be returned from an
 * {@code ImportSelector}).
 *
 * <p>An {@link ImportBeanDefinitionRegistrar} may implement any of the following
 * {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective
 * methods will be called prior to {@link #registerBeanDefinitions}:
 * <ul>
 * <li>{@link org.springframework.context.EnvironmentAware EnvironmentAware}</li>
 * <li>{@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware}
 * <li>{@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware}
 * <li>{@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
 * </ul>
 *
 * <p>Alternatively, the class may provide a single constructor with one or more of
 * the following supported parameter types:
 * <ul>
 * <li>{@link org.springframework.core.env.Environment Environment}</li>
 * <li>{@link org.springframework.beans.factory.BeanFactory BeanFactory}</li>
 * <li>{@link java.lang.ClassLoader ClassLoader}</li>
 * <li>{@link org.springframework.core.io.ResourceLoader ResourceLoader}</li>
 * </ul>
 *
 * <p>See implementations and associated unit tests for usage examples.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.1
 * @see Import
 * @see ImportSelector
 * @see Configuration
 */
public interface ImportBeanDefinitionRegistrar {

	/**
	 * Register bean definitions as necessary based on the given annotation metadata of
	 * the importing {@code @Configuration} class.
	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
	 * registered here, due to lifecycle constraints related to {@code @Configuration}
	 * class processing.
	 * <p>The default implementation delegates to
	 * {@link #registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry)}.
	 * @param importingClassMetadata annotation metadata of the importing class
	 * @param registry current bean definition registry
	 * @param importBeanNameGenerator the bean name generator strategy for imported beans:
	 * {@link ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR} by default, or a
	 * user-provided one if {@link ConfigurationClassPostProcessor#setBeanNameGenerator}
	 * has been set. In the latter case, the passed-in strategy will be the same used for
	 * component scanning in the containing application context (otherwise, the default
	 * component-scan naming strategy is {@link AnnotationBeanNameGenerator#INSTANCE}).
	 * @since 5.2
	 * @see ConfigurationClassPostProcessor#IMPORT_BEAN_NAME_GENERATOR
	 * @see ConfigurationClassPostProcessor#setBeanNameGenerator
	 */
	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator) {

		registerBeanDefinitions(importingClassMetadata, registry);
	}

	/**
	 * Register bean definitions as necessary based on the given annotation metadata of
	 * the importing {@code @Configuration} class.
	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
	 * registered here, due to lifecycle constraints related to {@code @Configuration}
	 * class processing.
	 * <p>The default implementation is empty.
	 * @param importingClassMetadata annotation metadata of the importing class
	 * @param registry current bean definition registry
	 */
	default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}

}

해당 코드는 ImportBeanDefinitionRegistrar 인터페이스를 정의하고 있습니다. 이 인터페이스는 @Configuration 클래스를 처리할 때 추가적인 빈 정의를 등록하는 데 사용됩니다. 주로 빈 정의 수준에서 작업해야 하는 경우에 유용합니다.

이 인터페이스를 구현하는 클래스들은 @Import 어노테이션에서 제공하거나 ImportSelector에서 반환될 수 있습니다.

ImportBeanDefinitionRegistrar는 다음의 Aware 인터페이스 중 하나 이상을 구현할 수 있습니다:

  • EnvironmentAware
  • BeanFactoryAware
  • BeanClassLoaderAware
  • ResourceLoaderAware

또는 다음과 같은 단일 생성자를 제공할 수 있습니다:

  • Environment
  • BeanFactory
  • ClassLoader
  • ResourceLoader

registerBeanDefinitions 메서드를 구현하여 @Configuration 클래스를 가져온다면 해당하는 어노테이션 메타데이터를 기반으로 빈 정의를 등록할 수 있습니다. 단, BeanDefinitionRegistryPostProcessor 유형의 빈은 여기서 등록할 수 없으며, 이는 @Configuration 클래스 처리와 관련된 라이프사이클 제약 때문입니다.

3. @EnableConfigurationProperties.java

package org.springframework.boot.context.properties;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

/**
 * Enable support for {@link ConfigurationProperties @ConfigurationProperties} annotated
 * beans. {@code @ConfigurationProperties} beans can be registered in the standard way
 * (for example using {@link Bean @Bean} methods) or, for convenience, can be specified
 * directly on this annotation.
 *
 * @author Dave Syer
 * @since 1.0.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {

	/**
	 * The bean name of the configuration properties validator.
	 * @since 2.2.0
	 */
	String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

	/**
	 * Convenient way to quickly register
	 * {@link ConfigurationProperties @ConfigurationProperties} annotated beans with
	 * Spring. Standard Spring Beans will also be scanned regardless of this value.
	 * @return {@code @ConfigurationProperties} annotated beans to register
	 */
	Class<?>[] value() default {};

}

이 코드는 @EnableConfigurationProperties라는 어노테이션의 정의를 보여줍니다. 이 어노테이션은 @ConfigurationProperties로 주석이 달린 빈을 지원하는 데 사용됩니다. @ConfigurationProperties로 주석이 달린 빈은 표준 방식(예: @Bean 메서드를 사용)으로 등록되거나 이 어노테이션에 직접 지정할 수 있습니다.

주요 속성은 다음과 같습니다:

  • value(): 이 속성은 @ConfigurationProperties로 주석이 달린 빈을 빠르게 Spring에 등록하는 편리한 방법입니다. 이 값을 설정하면 해당 클래스에 @ConfigurationProperties가 달린 빈을 자동으로 스캔하고 등록합니다. 또한, 이 속성을 설정하지 않아도 표준 Spring Beans은 스캔되어 등록됩니다.

4. @ConfigurationProperties.java


package org.springframework.boot.context.properties;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Indexed;

/**
 * Annotation for externalized configuration. Add this to a class definition or a
 * {@code @Bean} method in a {@code @Configuration} class if you want to bind and validate
 * some external Properties (e.g. from a .properties file).
 * <p>
 * Binding is either performed by calling setters on the annotated class or, if
 * {@link ConstructorBinding @ConstructorBinding} is in use, by binding to the constructor
 * parameters.
 * <p>
 * Note that contrary to {@code @Value}, SpEL expressions are not evaluated since property
 * values are externalized.
 *
 * @author Dave Syer
 * @since 1.0.0
 * @see ConfigurationPropertiesScan
 * @see ConstructorBinding
 * @see ConfigurationPropertiesBindingPostProcessor
 * @see EnableConfigurationProperties
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {

	/**
	 * The prefix of the properties that are valid to bind to this object. Synonym for
	 * {@link #prefix()}. A valid prefix is defined by one or more words separated with
	 * dots (e.g. {@code "acme.system.feature"}).
	 * @return the prefix of the properties to bind
	 */
	@AliasFor("prefix")
	String value() default "";

	/**
	 * The prefix of the properties that are valid to bind to this object. Synonym for
	 * {@link #value()}. A valid prefix is defined by one or more words separated with
	 * dots (e.g. {@code "acme.system.feature"}).
	 * @return the prefix of the properties to bind
	 */
	@AliasFor("value")
	String prefix() default "";

	/**
	 * Flag to indicate that when binding to this object invalid fields should be ignored.
	 * Invalid means invalid according to the binder that is used, and usually this means
	 * fields of the wrong type (or that cannot be coerced into the correct type).
	 * @return the flag value (default false)
	 */
	boolean ignoreInvalidFields() default false;

	/**
	 * Flag to indicate that when binding to this object unknown fields should be ignored.
	 * An unknown field could be a sign of a mistake in the Properties.
	 * @return the flag value (default true)
	 */
	boolean ignoreUnknownFields() default true;

}

이 코드는 @ConfigurationProperties라는 어노테이션의 정의를 보여줍니다. 이 어노테이션은 외부 구성 파일(예: .properties 파일)에서 속성을 바인딩하고 유효성을 검사하기 위해 클래스 정의나 @Configuration 클래스의 @Bean 메서드에 추가할 수 있는 어노테이션입니다.

해당 어노테이션은 다음과 같은 속성을 가지고 있습니다:

  • value(): 해당 객체에 바인딩할 유효한 속성들의 접두사(prefix)를 정의합니다. 이는 prefix() 메서드와 동의어입니다. 유효한 접두사는 점으로 구분된 하나 이상의 단어로 정의됩니다. (예: "acme.system.feature")
  • prefix(): value() 메서드와 동일한 기능을 수행하며, 해당 객체에 바인딩할 유효한 속성들의 접두사(prefix)를 정의합니다.
  • ignoreInvalidFields(): 해당 객체에 바인딩할 때 유효하지 않은 필드를 무시할지 여부를 지정합니다. 유효하지 않은 필드란, 일반적으로 올바른 형식이 아닌 필드(또는 올바른 형식으로 변환할 수 없는 필드)를 의미합니다. (기본값은 false)
  • ignoreUnknownFields(): 해당 객체에 바인딩할 때 알 수 없는 필드를 무시할지 여부를 지정합니다. 알 수 없는 필드란 속성 파일에 오타 등의 실수로 나타난 필드를 의미합니다. (기본값은 true)

Service 테스트

library/src/test/java/guides.multimodule.service 디렉터리 생성

이전 목록에서는 @SpringBootTest 주석의 기본 속성을 사용하여 테스트용 service.message를 구성했습니다. 라이브러리를 사용하는 애플리케이션과 런타임 시 충돌이 발생할 수 있으므로 application.properties를 라이브러리에 저장하지 않는 것이 좋습니다(클래스 경로에서 단 하나의 application.properties만 로드됨). application.properties를 테스트 클래스 경로에 넣을 수 있지만 jar에는 포함시키지 않을 수 있습니다(예를 들어 src/test/resources에 배치).

라이브러리 구성 요소에 대한 단위 테스트를 작성하고 싶을 것입니다. 재사용 가능한 Spring 구성을 라이브러리의 일부로 제공하는 경우 구성이 작동하는지 확인하기 위해 통합 테스트를 작성할 수도 있습니다. 이를 위해 JUnit과 @SpringBootTest 주석을 사용할 수 있습니다.

MyServiceTest 클래스를 생성하고 아래 코드를 삽입하세요

package guides.multimodule.service;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest("service.message=Hello")
public class MyServiceTest {

    @Autowired
    private MyService myService;

    @Test
    public void contextLoads() {
        assertThat(myService.message()).isNotNull();
    }

    @SpringBootApplication
    static class TestConfiguration {
    }

}

이 코드는 스프링 부트의 테스트 기능을 사용하여 MyService 클래스를 테스트하는 코드입니다.

  1. @SpringBootTest("service.message=Hello"): @SpringBootTest 어노테이션은 테스트를 위해 스프링 애플리케이션 컨텍스트를 설정합니다. 여기서 "service.message=Hello"는 외부 속성으로 설정되는 내용입니다. 이것은 테스트 환경에서 service.message 속성에 Hello 값을 주입하는 역할을 합니다.

  2. private MyService myService;: MyService를 주입받을 필드입니다. @Autowired 어노테이션을 사용하여 스프링이 MyService 빈을 주입하도록 지시합니다.

  3. @Test: JUnit의 테스트 메서드를 나타내는 어노테이션입니다.

  4. assertThat(myService.message()).isNotNull();: myService.message()를 호출하여 반환된 값이 null이 아닌지를 검증합니다.

  5. @SpringBootApplication static class TestConfiguration { }: 테스트에서 사용하는 임시 스프링 부트 애플리케이션 컨텍스트를 정의하는 내부 정적 클래스입니다. @SpringBootApplication은 스프링 부트 애플리케이션을 정의할 때 사용하는 메타 어노테이션으로, 여기서는 테스트 환경을 구성하기 위해 사용되었습니다.

이 코드는 MyService 클래스의 message() 메서드를 호출하고, 반환된 값이 null이 아닌지를 확인하는 단순한 테스트를 수행합니다.

MyService를 찾지 못해서 빨간 글씨로 표시되고 있습니다.

이제 루트 디렉터리의 build.gradle, src 디렉터리는 삭제합니다. 삭제 후 library 디렉터리의 build.gradle에서 다시 빌드 작업을 진행합니다.

성공적으로 resolve 되는 것을 확인할 수 있습니다.

테스트 실행 시 위와 같은 에러가 난다면 스프링 부트 버전에 맞는 테스트 의존성을 사용하고 있는지 확인합니다. build.gradle을 튜토리얼에서 명시한대로 작성했다면 제가 작성한 build.gradle을 참고하여 JUnit5로 변경합니다.

이번엔 테스트 및 빌드에 성공합니다.

루트 디렉터리 아래 application 디렉터리 추가

'application/src/main/java/guides/multimodule/application' 경로를 생성하고 build.gradle을 추가한다.

build.gradle [application]


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

group = 'guides'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}


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

test {
    useJUnitPlatform()
}

의존성에 implementation project(':library') 로 앞서 만든 library 디렉터리를 추가했습니다.

application 디렉터리 : DemoApplication 클래스 생성

경로의 끝에 DemoApplcation 클래스를 생성합니다. 이 main 클래스는 libraryService를 사용하여 메시지를 렌더링하는 @RestController가 될 것입니다.

package guides.multimodule.application;

import guides.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 = "guides.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);
    }
}

@SpringBootApplication은 다음을 추가하는 편리한 어노테이션이에요:

  • @Configuration: 해당 클래스를 애플리케이션 컨텍스트의 빈 정의 소스로 태그합니다.
  • @EnableAutoConfiguration: Spring Boot에게 클래스패스 설정, 다른 빈, 여러 속성 설정을 기반으로 빈을 추가하도록 지시합니다. 예를 들어, spring-webmvc가 클래스패스에 있으면 이 어노테이션은 애플리케이션을 웹 애플리케이션으로 표시하고 DispatcherServlet 설정과 같은 주요 동작을 활성화합니다.
  • @ComponentScan: Spring에게 guides 패키지에서 다른 컴포넌트, 설정, 서비스를 찾도록 지시하여 컨트롤러를 찾을 수 있게 합니다.

main() 메서드는 Spring Boot의 SpringApplication.run() 메서드를 사용하여 애플리케이션을 시작합니다. XML 한 줄도 없었죠? web.xml 파일도 없었습니다. 이 웹 애플리케이션은 100% 순수 Java로 구성되었으며 어떠한 플러밍이나 인프라 구성도 건드리지 않았어요.

DemoApplicationMyService(guides.multimodule.service)와 다른 패키지(guides.multimodule.application)에 있기 때문에 @SpringBootApplication은 자동으로 인식할 수 없습니다. MyService를 선택하게 하는 여러 방법이 있어요:

  1. 직접 @Import(MyService.class)로 가져옵니다.
  2. @SpringBootApplication(scanBasePackageClasses={...})로 해당 패키지에서 모든 것을 가져옵니다.
  3. 패키지 이름(guides.multimodule)을 명시적으로 지정합니다. (이 가이드에서는 이 방법을 사용합니다)

만약 애플리케이션이 JPA 또는 Spring Data를 사용한다면, @EntityScan@EnableJpaRepositories (관련된 것들도 포함됨) 어노테이션은 @SpringBootApplication에서 기본 패키지만 상속받습니다. 즉, scanBasePackageClassesscanBasePackages를 명시한 경우 @EntityScan@EnableJpaRepositories를 해당 패키지 스캔이 명시적으로 구성된 상태에서 사용해야 할 수 있습니다.

resource 디렉터리 생성 후 application.properties 추가

src/main/resources 디렉터리를 생성하고 application.properties 파일을 생성합니다. 이 파일에 libraryservice에서 사용할 messsage를 제공할 것입니다.

service.message=Hello, World

application 테스트

애플리케이션을 시작하여 엔드투엔드 결과를 테스트합니다. IDE에서 애플리케이션을 시작하거나 명령줄을 사용할 수 있습니다. 애플리케이션이 실행되면 브라우저의 http://localhost:8080/에서 클라이언트 애플리케이션을 방문하십시오. 거기에서 응답에 Hello, World가 반영된 것을 볼 수 있습니다.

Gradle을 사용하는 경우 다음 명령(실제로는 두 개의 명령이 순서대로 실행됨)을 사용하여 먼저 라이브러리를 빌드한 다음 애플리케이션을 실행합니다.

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

0개의 댓글