본문 바로가기
잡다구리

Building a Multi-Module Spring Boot Application with Gradle

by Growing! 2022. 8. 13.

원문: https://reflectoring.io/spring-boot-gradle-multi-module/ (by Tom Hombergs)
(원문을 임의로 번역한 글입니다)

Spring Initializr는 Spring Boot 애플리케이션을 처음부터 빠르게 생성할 수 있는 좋은 방법입니다. 애플리케이션을 개발하기 위해 확장할 수 있는 하나의 Gradle 파일을 생성합니다.

그러나 프로젝트가 커질수록 여러 개의 모듈로 코드베이스를 나누고 싶어할지도 모릅니다. 코드의 유지 관리와 이해도가 좋아지기 때문입니다.

이 글은 Gradle로 되어 있는 스프링 부트 애플리케이션을 어떻게 여러 빌드 모듈로 나누는지 보여줍니다.

Code Example (GitHub)

이 글에는 동작하는 예제 코드가 GitHub으로 제공됩니다.

모듈이란?

이 튜토리얼에서 "모듈"이라는 용어를 사용할 것인데, 모듈이 무엇인지 정의하며 시작해봅시다.

모듈은 ...

  • 다른 모듈의 코드와 분리된 코드베이스를 가집니다.
  • 빌드시에 분리된 아티팩트(JAR 파일)로 변환됩니다.
  • 다른 모듈들이나 서드파티 라이브러리에 대한 의존관계를 정의할 수 있습니다.

모듈은 다른 모듈들의 코드베이스와는 독립적으로 유지 관리하고 빌드할 수 있는 코드베이스입니다.

그러나 모듈은 여전히 parent 빌드 프로세스의 일부분입니다. parent 빌드 프로세스는 애플리케이션을 이루는 모든 모듈들을 빌드하여 WAR 파일과 같은 하나의 아티팩트로 결합합니다.

멀티 모듈이 필요한 이유는?

단일 모놀리식 모듈로 모든 것이 잘 작동하는데, 왜 코드베이스를 여러 모듈로 분할하려고 노력해야 할까요?

주된 이유는 단일 모놀리식 코드베이스가 아키텍처 붕괴에 취약하기 때문입니다. 코드베이스 내에서는 일반적으로 패키지를 사용하여 아키텍처 경계를 구분합니다. 그러나 Java의 패키지는 이러한 경계를 잘 보호하지 못합니다(내 의 "Enforcing Architecture Boundaries" 장에서 이에 대한 자세한 내용을 다룸). 단일 모놀리식 코드베이스 내에서 클래스 간의 종속성은 빠르게 나빠져서 큰 진흙덩어리(big ball of mud)로 변하기 쉽다고 말할 수 있습니다.

코드베이스를 여러 개의 작은 모듈(다른 모듈에 대한 종속성을 명확하게 정의)로 분할하면, 쉽게 유지 관리할 수 있는 코드베이스를 향한 큰 발걸음을 내딛게 됩니다.

예제 애플리케이션

이 튜토리얼에서 빌드할 모듈식 예제 웹 애플리케이션을 살펴보겠습니다. 이 애플리케이션은 "BuckPal"이라는 온라인 결제 기능을 제공합니다. 이 애플리케이션은 내 에서 설명하는 헥사고날 아키텍처 스타일을 따릅니다. 이 아키텍처는 코드베이스를 독립적으로 분리되며 명확하게 정의된 아키텍처 요소로 분할합니다. 이러한 각 아키텍처 요소에 대해 다음 폴더 구조가 나타내는 것 처럼 별도의 Gradle 빌드 모듈을 생성합니다.

├── build.gradle                (1)
├── settings.gradle             (7)
│
├── buckpal-configuration       (2)
│   ├── src
│   └── build.gradle
├── common                      (3)
│   ├── src
│   └── build.gradle
├── buckpal-application         (4)
│   ├── src
│   └── build.gradle
└── adapters
    ├── buckpal-persistence     (6)
    │   ├── src
    │   └── build.gradle
    └── buckpal-web             (5)
         ├── src
         └── build.gradle

각 모듈은 별도의 폴더에 있으며 Java 소스, build.gradle 파일, 고유한 책임들을 가집니다.

  • 최상위 레벨의 build-gradle(1) 파일은 모든 서브 모듈 사이에서 공유되는 빌드 동작을 구성합니다. 그래서 서브 모듈에서 반복적인 구성을 할 필요가 없습니다.
  • buckpal-configuration(2) 모듈에는 실제 Spring Boot 애플리케이션과 Spring 애플리케이션 컨텍스트를 통합하는 Spring Java Configuration을 포함합니다. 애플리케이션 컨텍스트를 생성하려면 애플리케이션의 특정 부분을 각각 제공하는 여러 모듈을 참조해야 합니다. 다른 곳에서는 이런 모듈을 infrastructure라고 부르기도 합니다.
  • common(3) 모듈은 다른 모든 모듈에서 참조할 수 있는 클래스들을 제공합니다.
  • buckpal-application(4) 모듈은 도메인 모델을 쿼리하고 변경하는 유즈케이스를 구현하는 서비스인 "application layer"를 구성하는 클래스를 가지고 있습니다.
  • adapter/buckpal-web(5) 모듈은 애플리케이션 모듈에서 구현된 유즈 케이스를 호출하는 웹 레이어를 구현합니다.
  • adapter/buckpal-persistence(6) 모듈은 애플리케이션의 퍼시스턴스 레이어를 구현합니다.

이 글의 나머지 부분에서는 이러한 각 애플리케이션 모듈에 대해 별도의 Gradle 모듈을 만드는 방법을 살펴보겠습니다. 우리는 Spring을 사용하고 있으므로 Spring 애플리케이션 컨텍스트를 동일한 경계를 따라 여러 Spring 모듈로 자르는 것이 합리적입니다. 하지만 이 부분은 다른 글에서 다룹니다.

Parent Build File

parent 빌드에 모든 모듈을 포함하려면 먼저 parent 폴더의 settings.gradle(7) 파일에 모듈의 목록을 작성해야 합니다:

include 'common'
include 'adapters:buckpal-web'
include 'adapters:buckpal-persistence'
include 'buckpal-configuration'
include 'buckpal-application'

이제 parent(root) 폴더에서 ./gradlew build를 실행하면 Gradle은 settings.gradle에 나열된 순서에 관계없이 모듈 간의 모든 종속성을 자동으로 해결하고 올바른 순서로 빌드합니다.

예를 들어, common 모듈은 다른 모든 모듈이 의존하고 있으므로 다른 모든 모듈보다 먼저 빌드됩니다.

이제 parent build.gradle(1) 파일에서 모든 하위 모듈에서 공유되는 기본 구성을 정의합니다:

plugins {
  id 'org.springframework.boot' version '2.4.3'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
}

subprojects {

  group = 'io.reflectoring.buckpal'
  version = '0.0.1-SNAPSHOT'

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

  repositories {
    jcenter()
  }

  //dependencyManagement {
  //  imports {
  //    mavenBom("org.springframework.boot:spring-boot-dependencies:2.1.7.RELEASE")
  //  }
  //}

}

(원문 보정함) dependencyManagement를 사용하지 않고, pluginsid 'org.springframework.boot' version '2.4.3'를 지정해주기만 하면 됨.
(이하 박스 내용은 무시)

맨 먼저 Spring Dependency Management Plugin을 추가합니다. 나중에 사용할 dependencyManagement 클로저를 제공합니다.

그리고 subprojects 클로저 안에 공유할 구성을 정의합니다. subprojects 에 포함된 모든 구성은 모든 서브 모듈에 적용됩니다.

subprojects에서 가장 중요한 부분은 dependencyManagement 클로저입니다. 여기에 특정 버전의 Maven 아티팩트에 대한 디펜던시를 지정할 수 있습니다. 서브 모듈 내에서 이러한 디펜던시 중 하나가 필요한 경우, 버전 번호가 dependencyManagement 클로저에서 로드되므로 서브 모듈에서 버전 번호 없이 디펜던시를 지정할 수 있습니다.

이를 통해 특정한 버전 번호를 여러 모듈에 지정하지 않고 한 곳에서 버전 번호를 지정할 수 있습니다. Maven의 pom.xml 파일에 있는 <dependencyManagement> 요소와 매우 비슷한 방식입니다.

예제에서 추가한 유일한 디펜던시는 Spring Boot의 Maven BOM(bill of materials)에 대한 것 입니다. 이 BOM에는 Spring Boot 애플리케이션이 잠재적으로 필요할 수 있는 모든 디펜던시가 지정한 Spring Boot 버전과 호환되는 정확한 버전으로 포함됩니다(여기서는 2.4.3.RELEASE). 따라서 디펜던시마다 버전을 지정할 필요가 없으며, 잘못된 버전을 가져올 가능성도 없어집니다.

또한, 모든 서브 모듈에 java-library 플러그인을 적용합니다. 이를 통해 디펜던시 스코프를 보다 세밀하게 지정할 수 있는 apiimplementation 구성을 사용할 수 있습니다.

Module Build Files

모듈의 빌드파일에는 이제 모듈에 필요한 디펜던시만 추가하면 됩니다.

adapters/buckpal-persistence/build.gradle 파일은 다음과 같습니다:

dependencies {
  implementation project(':common')
  implementation project(':buckpal-application')
  implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

  // ... more dependencies
}

퍼시스턴스 모듈은 commonapplication 모듈에 의존하고 있습니다. common 모듈은 모든 모듈에서 사용되므로 이 디펜던시는 자연스럽습니다. 이 애플리케이션은 헥사고날 아키텍처 스타일을 따르며 퍼시스턴스 모듈은 애플리케이션 레이어에 있는 인터페이스를 구현합니다. 따라서 application 모듈에 대한 디펜던시가 있어야 하며, 애플리케이션 레이어의 퍼시스턴스 "플러그인"으로 작동합니다.

그러나 Spring Boot 애플리케이션에 Spring Data JPA 지원을 제공하는 spring-boot-starter-data-jpa 디펜던시를 추가하는 것이 더 중요합니다. parent 빌드 파일의 spring-boot-dependencies BOM에서 자동으로 식별되기 때문에 버전 번호를 지정하지 않았다는 것에 유의합니다. 이 경우 Spring Boot 2.4.3.RELEASE와 호환되는 버전을 가져옵니다.

spring-boot-starter-data-jpa 디펜던시를 implementation 구성으로 추가하였습니다. 이것은 퍼시스턴스 모듈을 디펜던시로 포함하는 모듈들이 컴파일될 때에 이 디펜던시가 누출되어 들어가지 않는다는 것을 의미합니다. 원지 않는 모듈에 의도치않게 JPA 클래스가 사용되는 것을 막아줍니다.

웹 레이어의 빌드 파일인 adapters/buckpal-web/build.gradlespring-boot-starter-web에 대한 디펜던시 외에는 비슷합니다.

dependencies {
  implementation project(':common')
  implementation project(':application')
  implementation 'org.springframework.boot:spring-boot-starter-web'

  // ... more dependencies
}

애플리케이션의 모듈들은 불필요한 디펜던시 없이 Spring Boot 애플리케이션을 위한 웹 또는 퍼시스턴스 레이어를 빌드하는 데 필요한 모든 클래스에 액세스할 수 있습니다.

웹 모듈은 퍼시스턴스 모듈에 대해서는 알지 못하며 그 반대도 마찬가지입니다. 개발자는 의도적으로 build.gradle 파일에 디펜던시를 추가하지 않고서는, 웹 레이어에 퍼시스턴스 코드를 실수로 추가하거나 퍼시스턴스 레이어에 웹 코드를 실수로 추가할 수 없습니다. 이는 "big ball of mud"를 피하도록 도와줍니다.

Spring Boot Application Build File

이제 우리가 해야 할 일은 이러한 모듈들을 하나의 Spring Boot 애플리케이션으로 모으는 것입니다. 이 작업은 buckpal-configuration 모듈에서 수행합니다.

buckpal-configuration/build.gradle 빌드 파일에는 모든 모듈을 디펜던시로 추가합니다:

plugins {
  id "org.springframework.boot" version "2.4.3.RELEASE"
}

dependencies {

  implementation project(':common')
  implementation project(':buckpal-application')
  implementation project(':adapters:buckpal-persistence')
  implementation project(':adapters:buckpal-web')
  implementation 'org.springframework.boot:spring-boot-starter'

  // ... more dependencies
}

bootRun Gradle 태스크를 제공하는 Spring Boot Gradle 플러그인을 추가합니다. 이제 Gradle의 ./gradlew bootRun으로 애플리케이션을 실행할 수 있습니다.

@SpringBootApplication 애노테이션을 가지는 필수 클래스를 buckpal-configuration 모듈의 소스 폴더에 추가합니다:

@SpringBootApplication
public class BuckPalApplication {

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

}

이 클래스는 spring-boot-starter 디펜던시가 제공하는 SpringBootApplicationSpringApplication의 참조가 필요합니다.

Conclusion

이 튜토리얼에서는 Gradle의 Spring Dependency Plugin을 사용하여 Spring Boot 애플리케이션을 여러 Gradle 모듈로 분할하는 방법을 살펴보았습니다. 우리는 이 접근 방식을 따라 technical layerfunctional boundary 혹은 둘 다를 따라 애플리케이션을 분할할 수 있습니다.

Maven도 매우 비슷한 접근법을 사용할 수 있습니다.

이 주제에 대한 다른 관점을 원하면, 다중 모듈 Spring Boot 애플리케이션 생성에 대한 Spring guide도 있습니다(다양한 측면에 대해 설명합니다).

참고

 
 

댓글