읽기 시간 : 15 minutes

당신은 이미 그 순간을 경험했습니다. 단순한`./gradlew build`당신이 하루에 100번 실행하는 것이 갑자기 한 번도 본 적 없는 오류로 인해 멈추기 시작한다면? 이를 연속 11번의 오류로 곱하고, Docker Engine 29 호환성 문제를 충분히 추가한 뒤, 10년 동안 사용해 온 API를 삭제하는 Gradle 9을 뿌리세요. 여기서 전체 로그를 확인할 수 있습니다.


다이어그램 1 — Gradle 9 마이그레이션 (오류 체인)

목적 : 불호환 시리즈를 보여주다

👉 명료하고 Gradle에 중점을 둔
@startuml
left to right direction
title Gradle 9 마이그레이션
rectangle "1
reportsDir" as A
rectangle "2
mainClass" as B
rectangle "3\nsourceCompatibility" as C
rectangle "4
Groovy assert" as D

A --> B
B --> C
C --> D

@enduml

다이어그램 2 — 호환되지 않는 JHipster 플러그인

목표 : 손상된 플러그인 격리

👉 의존성에 대한 명확한 비전.
@startuml
left to right direction
title 수정해야 할 플러그인
skinparam shadowing false
skinparam roundcorner 18

rectangle "1
openApiGenerate" as A #FFF3E0
rectangle "2
gitProperties
2.5.7" as B #FFF3E0
rectangle "3
버전
수정된" as C #E3F2FD
rectangle "4
조립 OK" as D #E8F5E9

A --> B
B --> C
C --> D

@enduml

도표 3 — Docker 29 / Testcontainers

목표: 런타임 문제

👉 명확한 런타임 기록.
@startuml
left to right direction
title Docker 29 와 Testcontainers
skinparam shadowing false
skinparam roundcorner 18
skinparam defaultFontSize 14

rectangle "1
조립 OK" as A #E8F5E9
rectangle "2\n테스트 Cucumber 실패" as B #FFEBEE
rectangle "3
Testcontainers 1.32
거부" as C #FFF3E0
rectangle "4
업그레이드
버전" as D #E3F2FD
rectangle "5\n빌드 최종 OK" as E #E8F5E9

A --> B
B --> C
C --> D
D --> E

@enduml

그것을 표현하는 또 다른 방법
@startuml

!option handwritten true

skinparam backgroundColor #FEFEFE
skinparam shadowing false
skinparam defaultFontName Inter
skinparam defaultFontSize 14
skinparam ArrowThickness 2
skinparam RoundCorner 18

title 안정화 로드맵 — Gradle 9부터 최종 빌드까지
left to right direction

rectangle "⚙️ Gradle 9 마이그레이션" as A #E3F2FD
rectangle "🧩 JHipster 플러그인" as B #FFF3E0
rectangle "🐳 Docker 29\nTestcontainers" as C #FCE4EC
rectangle "✅ 안정적인 빌드" as D #E8F5E9

note bottom of A
reportsDir
mainClass
sourceCompatibility
Groovy assert
end note

note bottom of B
openApiGenerate
gitProperties
versions alignées
end note

note bottom of C
tests Cucumber KO
image Docker rejetée
upgrade libs
end note

A -[#42A5F5]-> B : corrections build.gradle
B -[#FB8C00]-> C : assemble OK
C -[#E53935]-> D : tests réparés

@enduml

현장 : JHipster 프로젝트, 더 이상 로드되지 않는 빌드

프로젝트`edster`는 2024년에 생성된 JHipster 애플리케이션입니다. 루트 빌드 스크립트`build.gradle`개월 동안 안정 상태를 유지했습니다. 그러고 나서 버전 업그레이드를 결정했습니다 :

컴포넌트

대상 버전

Gradle

9.4.1

자바

21.0.11-tem (Eclipse Temurin)

JHipster

8.x (프레임워크`tech.jhipster:jhipster-framework:8.11.0`)

스프링 부트

3.4.5

코틀린

2.3.0

도커 엔진

29.4.1 (API 1.54)

자바 버전 변경은 SDKMAN을 통해 이루어집니다 :

sdk use java 21.0.11-tem

첫 번째 발사:

$ ./gradlew build --no-daemon

FAILURE: Build failed with an exception.

* Where:
Build file '/home/.../edster/build.gradle' line: 18

* What went wrong:
An exception occurred applying plugin request [id: 'jhipster.cucumber-conventions']
> Failed to apply plugin 'jhipster.cucumber-conventions'.
   > Could not create task ':cucumberTest'.
      > Could not create task ':consoleLauncherTest'.
         > Could not set unknown property 'reportsDir' for task ':consoleLauncherTest'

우리는 빌드 스크립트 로딩조차 벗어나지 못한다. 문제는`buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle`.

오류 1 : 유령 변수 `reportsDir

스크립트`jhipster.cucumber-conventions.gradle`포함:

tasks.register('consoleLauncherTest', JavaExec) {
    dependsOn(testClasses)
    String cucumberReportsDir = file("$buildDir/reports/tests")
    outputs.dir(reportsDir)           // <-- reportsDir n'existe PAS
    classpath = sourceSets["test"].runtimeClasspath
    main = "org.junit.platform.console.ConsoleLauncher"
    // ...
}

정의된 변수는`cucumberReportsDir`. 사용된 것은`reportsDir`-- 어디에서도 정의되지 않은.

수정:`outputs.dir(cucumberReportsDir)`

오류 2 : main = "…​" 더 이상 Gradle 9에서 존재하지 않습니다.

즉시 재시작 :

Could not set unknown property 'main' for task ':consoleLauncherTest' of type org.gradle.api.tasks.JavaExec.

Gradle 9는 속성을 제거합니다`main`의 이익을 위해`mainClass`과제에`JavaExec`.

수정 : mainClass = "org.junit.platform.console.ConsoleLauncher"

오류 3: `sourceCompatibility`는 더 이상 프로젝트 속성이 아닙니다.

새 오류:

Could not set unknown property 'sourceCompatibility' for root project 'edster' of type org.gradle.api.Project.

루트 스크립트는 다음과 같았습니다 :

sourceCompatibility=17
targetCompatibility=17

Gradle 9은 루트 수준에서 이러한 속성을 제거합니다. 이러한 속성은 블록 내에서 존재해야 합니다.java.

수정:

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

오류 4: 세 피연산자 Groovy 어서션

이전 스크립트에는:

assert System.properties["java.specification.version"] == "17" || "21" || "24"

Groovy에서는,"21" et "24"`진실한 문자열이다. 표현은 …​로 해결됩니다(…​ == "17") || true || true`, 항상 참인 것 — 하지만 원하는 엄격한 주장에 대한 구문은 유효하지 않습니다.

수정:

assert System.properties["java.specification.version"] in ["17", "21", "23", "24"]

오류 5 : 암묵적 의존성 compileKotlin → `openApiGenerate

빌드가 진행 중입니다. Kotlin 컴파일이 성공했습니다. 그리고 :

Task ':compileKotlin' uses this output of task ':openApiGenerate'
without declaring an explicit or implicit dependency.

Gradle 9은 태스크 사이의 암시적 의존성을 거부합니다. 왜냐하면`compileKotlin`생성된 소스를 읽는`openApiGenerate`, 링크를 선언해야 합니다.

수정안에`build.gradle`:

afterEvaluate {
    tasks.named("compileKotlin").configure {
        dependsOn(tasks.named("openApiGenerate"))
    }
}

Le `afterEvaluate`필요합니다 왜냐하면`openApiGenerate`OpenAPI Generator 플러그인(확장 기능)이며, 구성 단계에서 직접 접근할 수 있는 작업이 아닙니다.

오류 6 : `generateGitProperties`가 `FilterOutputStream.write()`에서 실패합니다.

Java 컴파일이 성공합니다. 리소스가 처리됩니다. 그 후에 :

> Task :generateGitProperties FAILED

No signature of method: java.io.FilterOutputStream.write() is applicable for argument types: (Integer) values: [103]

플러그인`gradle-git-properties`version 2.5.0는 Java 21 / Gradle 9와 호환되지 않습니다. 내부 버그가 호출을 시도합니다`write(int)`그루비를 통해 스트림이 닫힌 것.

수정안에`gradle.properties` :

gitPropertiesPluginVersion=2.5.7

마지막으로: Testcontainers가 무대에 등장합니다.

여기까지는 순수한 "build script debugging"이었다. 각 오류는 빌드 스크립트에서의 Gradle 9 또는 Java 21 호환성 문제였다. 위에서 언급한 여섯 가지 수정을 마친 후,`./gradlew assemble`성공적으로 통과합니다.

하지만`./gradlew build`테스트도 실행하고, Cucumber 테스트는 Testcontainers를 사용하여 임시 PostgreSQL을 시작합니다.

Could not find a valid Docker environment.

EnvironmentAndSystemPropertyClientProviderStrategy: failed with exception BadRequestException
(Status 400: {"message":"client version 1.32 is too old. Minimum supported API version is 1.40"}
UnixSocketClientProviderStrategy: failed with exception BadRequestException
(Status 400: {"message":"client version 1.32 is too old. Minimum supported API version is 1.40"}

Testcontainers은 Docker와 통신할 수 없습니다. 서로 다른 두 가지 전략(환경 변수 + Unix 소켓)이 동일한 오류에서 실패합니다.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false

actor "Testcontainers\n1.20.6" as TC
participant "docker-java\n3.4.1" as DJ
participant "도커 엔진
29.4.1" as DE

TC -> DJ : new DockerClient()
DJ -> DE : GET /_ping\nUser-Agent: docker-java/3.4.1\nAPI-Version: 1.32
DE --> DJ : HTTP 400 Bad Request\nclient version 1.32 is too old.\nMinimum supported API version is 1.40
DJ --> TC : BadRequestException
TC --> TC : Could not find a valid\nDocker environment

note right of DE
  Docker Engine 29.x impose
  API minimum = 1.40
  (défaut ancien : 1.24)
end note

note left of DJ
  docker-java 3.4.1 envoie
  hardcoded API version 1.32
  dans l'en-tête HTTP
end note

@enduml

Phase 1 : docker-java 트랙

첫 번째 반응: Testcontainers에서 사용하는 Java Docker 클라이언트가 API 버전을 전송`1.32`그의 HTTP 핸드셰이크에서, 하지만 Docker Engine 29.4.1은 최소한을 요구한다`1.40`. 문제는 클라이언트 측에 있으며, 데몬 측이 아닙니다.

Testcontainers 1.20.6에 의해 해결된 docker-java 버전 검증 :

$ ./gradlew dependencies --configuration testRuntimeClasspath | grep docker-java

+--- com.github.docker-java:docker-java-api:3.4.1
+--- com.github.docker-java:docker-java-transport:3.4.1

docker-java 3.4.1, Testcontainers 1.20.6에서 유래한 버전입니다. docker-java의 최신 공개 버전은 3.5.1입니다. 우리는 버전 강제 업그레이드를 시도해 봅니다.resolutionStrategy`블록 안에`configurations de build.gradle :

configurations {
    all {
        resolutionStrategy.eachDependency { details ->
            if (details.requested.group == "com.github.docker-java"
                && details.requested.name.startsWith("docker-java")) {
                details.useVersion("3.5.1")
            }
        }
    }
}

빌드를 다시 시작합니다. 같은 오류:`client version 1.32 is too old`.

Gradle 캐시 확인: JAR이 다시 다운로드되었지만 오류가 계속됩니다. docker-java 3.5.1은 여전히 보냅니다`1.32`. 이 버전은 따라서 Docker Engine 29.4.1에 충분하지 않습니다.

정화 작용: Gradle 캐시를 완전히 비우기, 확실히 하기 위해 :

rm -rf ~/.gradle/caches
rm -rf /path/to/project/.gradle

정리 후 전체 재시작. 같은 오류.

2단계 : 웹 검색 및 GitHub 브레드크럼

docker-java 3.5.1은 부족합니다. 이제 누군가가 이 정확한 문제를 겪었는지 찾아볼 수밖에 없습니다.

testcontainers-java 및 docker-java 이슈에 대한 집중된 검색:

site:github.com/testcontainers "client version 1.32 is too old"
site:github.com/docker-java "client version 1.32" Docker Engine 29

즉시 결과:

아리아드네의 실은 명확합니다: Docker Engine 29.x는 최소 API 버전을 올렸다`1.24`(이전 결함) 에`1.40`. Testcontainers 1.20.x는 docker-java 3.4.x를 사용하여 API 버전을 협상하여 보내는`1.32`. 데몬은 예의 바르게 그러나 단호히 거부합니다.

이슈 #11491에서, 유지관리자가 답변합니다 :

_ 안녕하세요, 저는 GH 러너에서 엔진 29.1을 사용하는 버전 2.0.3으로 실행되는 프로젝트가 있는데, 잘 작동합니다. 모두 버전 충돌이 없는지 확인해 주실 수 있나요? 모든 모듈이 무언가로 prefix되었음을 기억해 주세요`testcontainers-. 그래서 버전 2.x부터 우리는 …​부터 시작했습니다`postgresql to testcontainers-postgresql _

이 답변에는두 개의 중요한 정보:

  1. Testcontainers 2.0.3+은 문제를 해결합니다

  2. 모듈들이testcontainers-` 접두사로 이름이 바뀐

3단계 : 이름이 바뀐 아티팩트의 함정

이름 변경은 가혹합니다. Testcontainers 2.x에서는 더 이상 과거 이름 중 어느 것도 해당 이름으로 작동하지 않습니다:

이전 이름 (1.x)

새 이름 (2.x)

org.testcontainers:postgresql

org.testcontainers:testcontainers-postgresql

org.testcontainers:jdbc

org.testcontainers:testcontainers-jdbc

org.testcontainers:junit-jupiter

org.testcontainers:testcontainers-junit-jupiter

org.testcontainers:testcontainers

변경되지 않은

첫 번째 시도: Testcontainers BOM 2.0.5를 적용하고 새로운 아티팩트 이름을 안에`build.gradle`.

dependencies {
    testImplementation platform("org.testcontainers:testcontainers-bom:2.0.5")
    testImplementation "org.testcontainers:testcontainers-postgresql"
    testImplementation "org.testcontainers:testcontainers-jdbc"
    testImplementation "org.testcontainers:testcontainers-junit-jupiter"
    testImplementation "org.testcontainers:testcontainers"
}

./gradlew build

FAILURE: Could not find org.testcontainers:jdbc:2.0.5.
Could not find org.testcontainers:junit-jupiter:2.0.5.

testcontainers-bom의 BOM은 충분하지 않다.왜? Spring Boot 3.4.5가 자체적인 의존성 관리 BOM을 노출하기 때문에 (spring-boot-dependencies) 누구이긴다Testcontainers BOM의 전달적 해결에서 Spring Boot이 고정합니다`testcontainers` à 1.20.6, 따라서 Spring Boot BOM에 명시적으로 들어 있지 않은 모든 아티팩트(예: 새로운`testcontainers-*) 이전 이름으로 해결됩니다`1.20.6-- 더 이상 존재하지 않는 이 버전.

점검하면서`dependencies --configuration testRuntimeClasspath`, 찾을 수 있습니다 :

+--- org.testcontainers:testcontainers -> 1.20.6 (*)
|    \--- org.testcontainers:testcontainers:2.0.5 -> 1.20.6

Le →`나타나냅니다 대체 : Testcontainers 2.0.5가 *요구*됩니다, 하지만 Spring Boot이 강제합니다`1.20.6.

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false
skinparam componentStyle rectangle

left to right direction

component "build.gradle
(소비자)" as BG #E3F2FD {
    rectangle "요청
`testcontainers-bom:2.0.5" as REQ
    rectangle "요청
`testcontainers-postgresql" as REQ2
}

component "Maven 중앙" as MC #FFF3E0

component "선언" as DEC #E8F5E9 {
    rectangle "testcontainers-bom:2.0.5`
수정 2.0.5" as BOM2
    rectangle "spring-boot-dependencies:3.4.5`\n고정 1.20.6" as BOM1
}

component "Gradle 해결" as RES #FFEBEE {
    rectangle "우승 버전
`testcontainers:1.20.6" as WIN #EF9A9A
}

BG --> DEC : "BOM을 분석하다"
BOM2 --> RES : "제안 2.0.5"
BOM1 --> RES : "제안하다 1.20.6"
WIN -> MC : "다운로드
testcontainers:1.20.6"

note bottom of WIN
  Spring Boot BOM prime car
  le plugin spring-boot applique
  ses dépendances de gestion
  après la résolution du consommateur
end note

@enduml

4단계 : 해결 강제

전략이 두 배가 됩니다 :

  1. docker-java를 버전 3.7.1로 강제 지정 (Docker Engine 29와 호환됨으로 테스트됨)

  2. Spring Boot BOM을 우회하여 Testcontainers core를 2.0.5로 강제

@startuml
skinparam backgroundColor #FEFEFE
skinparam handwritten false
skinparam componentStyle rectangle

left to right direction

component "build.gradle
(resolutionStrategy와 함께)" as BG #E8F5E9 {
    rectangle "BOM Testcontainers
`2.0.5" as BOM2
    rectangle "resolutionStrategy\n.eachDependency" as RS #66BB6A
}

component "해결된 갈등" as RESOLVED #E3F2FD {
    rectangle "docker-java
`3.7.1` (강제)" as DJ
    rectangle "testcontainers
`2.0.5` (강제)" as TC #81C784
    rectangle "testcontainers-postgresql
`2.0.5" as TPG
    rectangle "testcontainers-jdbc
`2.0.5" as TJ
    rectangle "testcontainers-junit-jupiter
`2.0.5" as TJJ
}

component "BOM Spring Boot
(암시적, 재정의된)" as BOM1 #FFEBEE

component "Maven Central" as MC #FFF3E0

BG --> RESOLVED : "BOM Testcontainers 2.0.5\n바로 통과합니다"
RS -> DJ : "힘 3.7.1"
RS -> TC : "힘 2.0.5"
RESOLVED --> MC : "JARs를 다운로드하세요\nDocker 29와 호환되는"
BOM1 -[#gray,dashed]-> RESOLVED : "testcontainers에서 무시됨\ndocker-java"

note bottom of RS
  Le `eachDependency` intercepte
  la résolution Gradle AVANT
  que le BOM Spring Boot ne gagne.
  C'est une intervention chirurgicale.
end note

@enduml

최종 수정안에`build.gradle`:

configurations {
    all {
        resolutionStrategy.eachDependency { details ->
            if (details.requested.group == "com.github.docker-java"
                && details.requested.name.startsWith("docker-java")) {
                details.useVersion("3.7.1")
            }
            if (details.requested.group == "org.testcontainers"
                && details.requested.name == "testcontainers") {
                details.useVersion("2.0.5")
            }
        }
    }
}

dependencies {
    testImplementation platform("org.testcontainers:testcontainers-bom:2.0.5")
    testImplementation "org.testcontainers:testcontainers-jdbc"
    testImplementation "org.testcontainers:testcontainers-junit-jupiter"
    testImplementation "org.testcontainers:testcontainers-postgresql"
    testImplementation "org.testcontainers:testcontainers"
    // ... autres dépendances Spring Boot
}

Le `resolutionStrategy.eachDependency`이것이 BOM을 우회하는 유일한 방법이다`spring-boot-dependencies`Spring Boot 플러그인에 의해 관리됨. 동등 테스트`details.requested.name == "testcontainers"`은 의도적으로 제한적 : 핵심 모듈만 강제합니다. 모듈들`testcontainers-*`Testcontainers 2.0.5의 BOM을 명시적으로 선언했습니다.

최종 결과

$ ./gradlew build --no-daemon

> Task :consoleLauncherTest
[2 containers found]
[2 containers started]
[2 containers successful]

BUILD SUCCESSFUL in 1m 16s

Testcontainers 2.0.5 + docker-java 3.7.1은 드디어 Docker Engine 29.4.1을 통해 PostgreSQL 컨테이너를 생성할 수 있게 되었습니다. 최종 비즈니스 오류 (Cucumber 테스트에서의 HTTP 500)는 빌드 스크립트와 완전히 무관한 애플리케이션 문제입니다.

수정 사항 요약표

# 파일 문제 수정

1

buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle

변수`reportsDir`존재하지 않는

outputs.dir(cucumberReportsDir)

2

buildSrc/src/main/groovy/jhipster.cucumber-conventions.gradle

`main`지원 종료된 Gradle 9

mainClass

3

build.gradle

`sourceCompatibility`top-level 금지된 Gradle 9

블록`java { sourceCompatibility = JavaVersion.VERSION_21 }`

4

build.gradle

구문적으로 잘못된 Groovy 어설션

assert …​ in ["17", "21", "23", "24"]

5

build.gradle

암시적 의존성`compileKotlin` → openApiGenerate

afterEvaluate { tasks.named("compileKotlin").dependsOn("openApiGenerate") }

6

gradle.properties

`gitPropertiesPluginVersion`호환되지 않는 Java 21

2.5.7

7

Gradle 캐시

docker-java 3.5.1 테스트됨, 해결되었지만 부족함

정리 + 통과 docker-java 3.7.1

8

build.gradle

Testcontainers 1.20.6는 Docker Engine 29와 호환되지 않습니다.

강제 Testcontainers 2.0.5 + 접두사`testcontainers-*`+ resolutionStrategy 대 Spring Boot BOM

결론

JHipster/Gradle 빌드가 Gradle 9 + Docker Engine 29로 버전 업그레이드 후 실패하면 오류는 두 가지 범주로 나뉩니다:

빌드 스크립트 오류(첫 6개) : Gradle 9는 수년간 작동했던 속성과 구문을 제거합니다 (sourceCompatibility=, main =, reportsDir). 이것들은 새로운 API를 알면 이루어지는 기계적 마이그레이션입니다.

Docker 런타임 오류(마지막 두) : Docker Engine 29.4.1은 API 클라이언트 버전 1.40 미만을 제외합니다. Testcontainers 1.20.6 (Spring Boot 3.4.5에 의해 강제됨)는 docker-java 3.4.1을 사용하며, 이는 버전 1.32를 전송합니다. Testcontainers 2.0.5와 docker-java 3.7.1만이 문제를 해결하며, 아티팩트 이름 변경 및 버전 강제를 통해`resolutionStrategy.eachDependency`Spring Boot의 내재된 BOM을 우회하는 유일한 방법.

프로젝트 루트의 Gradle 빌드 스크립트가 이제 깨끗합니다.`./gradlew build`Java 21, Gradle 9.4.1, Spring Boot 3.4.5 및 Docker Engine 29.4.1에서 실행됩니다.

관련 기사