반응형

0. 들어가며


스프링부트로 개발을 하다보면 다양한 외부 라이브러리를 활용하게 됩니다. 예를 들어, 웹 애플리케이션을 위해 spring-boot-starter-web을, DB 연동을 위해 spring-boot-starter-data-jpa를, JSON 처리를 위해 jackson 계열 라이브러리를 사용하죠.

그런데 스프링부트를 사용하면 특정 라이브러리의 버전을 적지 않아도 프로젝트가 잘 빌드되고 동작합니다. 스프링부트는 의존성을 관리해주는 기능을 제공하는데, 어떻게 라이브러리 버전을 자동으로 관리하는지 궁금하여 알아본 내용을 글로 정리합니다.

 

1. 스프링 부트와 의존성 관리


1.1 의존성 관리의 필요성

프로젝트를 진행하다보면 수많은 라이브러리를 가져다 쓰게됩니다. 하나의 웹 애플리케이션을 개발한다고 해도 적어도 다음과 같은 라이브러리들이 필요합니다.

  • spring-boot-starter-web
  • spring-boot-starter-data-jpa
  • jackson-databind
  • logback
  • 등등….

이 때 각 라이브러리는 또 다른 라이브러리에 의존성을 가질 수 있습니다. 예를 들면 spring-boot-starter-web 내부적으로는 spring MVC, Jackson, Tomcat 등 다양한 라이브러리를 포함합니다.

이 때 문제가 있습니다.

  1. 호환성 이슈 : 어떤 라이브러리는 Jackson 2.14 버전이 필요한데, 다른 라이브러리는 2.15 버전이 필요할 수 있습니다.
  2. 업데이트 부담 : 매번 라이브러리의 최신 버전을 확인하고, 코드 호환성 테스트를 하는 건 매우 귀찮은 일입니다.
  3. 프로젝트 불안정 : 잘못된 버전 조합으로 예기지 못한 버그가 발생할 수 있습니다.

이러한 문제점을 해결하기 위해 스프링부트는 BOM(Bill Of Materials)를 이용해 의존성 버전을 중앙에서 관리합니다. 즉, “우리가 정해둔 이 버전 조합을 쓰면 대체로 문제없이 돌아갈 것이다” 라는 안정적인 버전 세트를 제공하는 것이죠.

1.2 스프링부트 의존성 관리 개념 이해

스프링부트는 spring-boot-dependencies 라는 BOM을 통해 다양한 라이브러리의 호환 가능한 버전 리스트를 미리 정의해둡니다. 덕분에 개발자는 개별 라이브러리 버전을 하나하나 지정하지 않고도 믿을 수 있는 버전 세트를 바로 사용할 수 있게 됩니다.

즉, 개발자가 어떤 라이브러리를 사용하지만 결정하면 스프링부트의 BOM이 그 라이브러리의 어떤 버전을 쓸지 대신 결정해주는 것이죠.

1.3 코드 예제

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

group = 'me.jahni'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

repositories {
    mavenCentral()
}

dependencies {
		// 의존성 하나 추가
    implementation 'org.springframework.boot:spring-boot-starter'
}

tasks.named('test') {
    useJUnitPlatform()
}

위 코드처럼 spring-boot-starter 의존성을 추가하고

./gradlew dependencies

로 의존성을 확인해보면

 

위와 같이 다양한 라이브러리들이 같이 끌어와지고, 버전도 명시되어 있는 것을 확인할 수 있습니다. 이를 통해 버전을 따로 명시하지 않아도 “스프링부트가 알아서 버전을 관리해주는구나”를 알 수 있죠.

 

2. 스프링부트의 의존성 관리 방식


2.1 BOM(Bill Of Materials)란?

BOM은 말 그대로 자재 명세서와 비슷한 개념입니다. 여기서는 “버전 명세서”라고 보면 됩니다. 이때 스프링부트팀은 다양한 라이브러리의 호환 가능한 버전 조합을 미리 BOM에 정리해두는 것이죠.

예를 들어 jackson-databind는 어떤 버전을 써야 spring-boot-starter-web과 잘 어울리는지, spring-core는 어느 버전이 안정적인지 등을 BOM에 정의해둡니다. 이를 통해 개발자는 라이브러리 버전 선택 부담을 덜 수 있습니다.

정리하면

  • BOM은 여러 라이브러리의 안정적인 버전 세트(묶음) 입니다.
  • BOM을 사용하면 별도로 버전을 적지 않아도, BOM에 정의된 버전을 자동으로 사용합니다.

2.2 스프링부트에서 BOM 사용 원리 이해하기

스프링부트는 Gradle 프로젝트에 적용하면 spring-boot-dependencies 라는 BOM이 자동으로 참조됩니다.

 

 

이 BOM은 org.springframework.boot:spring-boot-dependencies라는 Maven POM 형태로 제공되며, 내부에는 여러 라이브러리와 버전 정보가 정리되어 있습니다.

Gradle에서는 이 Maven BOM을 의존성 해석 시 참고하게 됩니다. 이 덕분에 implementation 'org.springframework.boot:spring-boot-starter-web' 처럼 버전 없이 선언해도 문제가 없는 것입니다.

2.3 BOM 실제 파일 확인해보기

BOM이 실제로 어떻게 생겼는지 확인해봅시다. 스프링부트 BOM은 Maven Central에 업로드되어있습니다.

예시 : https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-dependencies/3.1.4/

위 링크를 들어가 실제 BOM 파일을 확인해보면, 스프링 부트가 이 파일을 통해 버전을 관리하고 있구나라는 것을 알 수 있습니다.

 

3. 커스텀한 BOM 만들고 적용하는 방법


지금까지는 스프링부트가 제공하는 spring-boot-dependencies BOM에 대해 알아보았습니다. 그러나 상황에 따라 내가 원하는 라이브러리 버전을 직접 관리하고 싶을 때가 있을 것 같습니다.

예를 들어,

  • 회사나 팀 내부에서 표준화된 라이브러리 버전을 사용하고 싶다.
  • 스프링부트 BOM에 포함되지 않은 별도의 라이브러리들의 버전을 일관되게 관리하고 싶다.
  • 등등..

이럴 때 직접 BOM 파일을 만들어서 관리할 수 있습니다.

3.1 BOM 파일의 역할과 구조 이해하기

BOM 파일은 기본적으로 Maven POM 파일 형태입니다. 이 파일은 실제 런타임에 사용되는 라이브러리가 아니라, 라이브러리 버전 정보를 관리하기 위한 “참조” 역할을 합니다. Gradle에서는 이 Maven POM 기반의 BOM을 platform()으로 불러와 사용합니다.

BOM 파일 구조 예시

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-custom-bom</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.15.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.12.0</version>
            </dependency>
            <!-- 여기서 내가 관리하고 싶은 다른 라이브러리들도 추가 -->
        </dependencies>
    </dependencyManagement>
</project>

위와 같은 POM 파일을 BOM으로 사용할 수 있고, 이 파일은 Maven 중앙 저장소나 사내 Nexus, Maven 레포지토리 등에 배포한 뒤 Gradle 프로젝트에서 platform() 을 이용해 참조할 수 있습니다.

plugins {
    id 'java'
}

repositories {
    mavenCentral()
    mavenLocal() // 로컬 Maven 레포지토리에 있는 my-custom-bom을 참고하기 위해
}

dependencies {
    // 나만의 BOM 플랫폼 적용
    implementation platform("com.example:my-custom-bom:1.0.0")

    // 이제 버전 없이 사용 가능
    implementation 'com.fasterxml.jackson.core:jackson-databind'
    implementation 'org.apache.commons:commons-lang3'
}

위와 코드와 같이 implemetation platform(”bom”) 과 같이 써주면 라이브러리에 버전을 생략하고도 적용을 시킬 수 있습니다.

 

4. 스프링부트 의존성 관리의 한계와 주의점


앞선 글에서 BOM을 활용하면 대부분의 라이브러리 버전 호환성 문제가 해결된다고 말했는데요. 하지만 몇 가지 상황에서 조금 더 생각해볼 것들이 있습니다.

4.1 특정 라이브러리 버전을 강제로 사용해야 하는 경우

스프링부트 BOM이 지정한 버전보다 더 최신 혹은 특정한 버전을 반드시 사용해야하는 상황이 생길 수 있습니다. 예를 들면 보안 이슈가 발견되어 특정 라이브러리의 최신 패치를 즉시 적용해야할 수 있죠.

이럴 때는 BOM이 관리하는 버전을 오버라이드해주면 됩니다. Gradle에서는 의존성의 버전을 직접 명시하면 BOM보다 높은 우선순위로 적용이 됩니다.

dependencies {
    // BOM에선 2.15.0으로 지정했지만, 내가 직접 2.15.1 버전을 사용하고 싶다면:
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.1'
}

4.2 BOM을 혼합 사용 시 주의

스프링부트 BOM 외에 내가 커스텀한 BOM이나 써드파티 BOM (Spring Cloud BOM, Kubernetes BOM 등)을 함께 사용할 경우, 버전 충돌이 발생할 수 있습니다. 이때는 BOM의 우선순위를 조정하거나, 특정 BOM에서 제공하는 라이브러리 버전을 명시적으로 오버라이드하는 방식으로 문제를 해결해야 합니다.

dependencies {
    implementation platform("com.example:my-custom-bom:1.0.0")
    implementation platform("org.springframework.cloud:spring-cloud-dependencies:2021.0.3")

    // 충돌 발생 시 여기서 특정 라이브러리 재정의
    implementation 'org.apache.commons:commons-lang3:3.12.0'
}

BOM이 여러개면?

  • 버전 충돌시 Gradle은 가장 높은 버전을 선택한다.
  • 여러 BOM을 사용할 경우, 충돌은 Gradle의 기본 버전 해결 규칙에 따라 처리된다.
  • Gradle 버전 해결 규칙은 커스텀할 수 있다.

Gradle 버전 충돌 관리 참고한 내용

 

5. Dependency Constraints and Conflict Resolution

When the same library is declared multiple times or when two different libraries provide the same functionality, a conflict can occur during dependency resolution.

docs.gradle.org

 

6. Dependency Resolution

Graph resolution is the process of determining the full set of transitive dependencies, and their versions, that are required for a given set of declared dependencies. Graph resolution operates solely on dependency metadata (GMM, POMs). In this phase, arti

docs.gradle.org

 

 

Platforms

Platforms are used to ensure that all dependencies in a project align with a consistent set of versions. Platforms help you manage and enforce version consistency across different modules or libraries, especially when you are working with a set of related

docs.gradle.org

 

5. 나가며


스프링 부트가 짜잔 해주는건 줄 알았는데 스프링부트는 버전 세트를 정의해둔 BOM 파일만 만들어둔거고, 의존성 버전 적절히 땡겨오는건 Gradle이 해주는거구나..

 

복사했습니다!