Java 25와 Spring Boot
Gradle 도구 체인과 실행 출력으로 Java 25·Spring Boot 4.1.2·Spring 프레임워크 7의 기준선을 검증하고 게시판 프로젝트를 시작합니다.
Spring은 Java 객체를 만들고 서로 연결하며, 웹 요청을 처리하는 데 필요한 기반을 제공하는 프레임워크(framework)입니다. Spring Boot는 Spring을 대신하는 별도 기술이 아니라, 자주 쓰는 의존성과 설정을 알맞은 기본값으로 묶어 첫 실행을 쉽게 해 주는 도구입니다. 이 관계를 먼저 알아 두면 Spring과 Spring Boot라는 이름을 섞어 쓰지 않게 됩니다.
이 교재는 장마다 기능을 조금씩 더해 가며 회원가입·게시판 웹 애플리케이션을 만듭니다. 이번 글에서는 아직 게시글 같은 도메인 기능을 만들지 않고, 앞으로 추가할 모든 코드가 같은 조건에서 빌드되도록 프로젝트의 실행 기준을 정합니다. 누가 받아서 빌드해도 같은 Java 기능 수준과 같은 프레임워크 버전으로 빌드되는 프로젝트가 이번 글의 목표입니다.
이 기준을 정하려면 도구 두 가지를 구분해야 합니다. JDK(Java Development Kit)는 Java 코드를 컴파일하고 실행하는 도구 묶음이고, Gradle은 필요한 라이브러리를 내려받아 컴파일과 실행을 대신 반복해 주는 빌드 도구입니다. 그런데 한 컴퓨터에서 쓰이는 Java가 하나뿐인 것은 아닙니다. IDE가 표시하는 JDK, 터미널에서 실행되는 java, Gradle Wrapper를 시작하는 런처(launcher) JVM, 빌드를 실제로 수행하는 데몬(daemon) JVM, 작업(task)마다 고르는 도구 체인(toolchain) JDK가 모두 다를 수 있습니다. 이 역할을 구분하지 못하면 invalid source release 같은 오류의 원인을 엉뚱한 Java 설정에서 찾게 됩니다.
이 글은 프로젝트의 버전을 고르고 최소 애플리케이션을 실행해 시작 로그를 확인하는 데서 시작합니다. 페이지 응답은 다음 글에서 정적 파일을 추가한 뒤에 확인합니다. 다음 글의 JSON 예제가 쓰는 RuntimeVersions.java는 이 글에서 미리 작성해 두어야 하고, 그 뒤의 RuntimeVersionsTest와 도구 체인 진단은 버전이 기대와 다르거나 빌드가 막혔을 때 돌아와서 볼 확인 방법으로 읽어도 됩니다.
SPRING · JAVA EXECUTION EVIDENCE
선언한 버전은 실제 선택과 실행 증거로 확인한다
Gradle 자체의 JVM, 작업의 JDK, 테스트 프로세스는 각각 확인합니다. 앞 단계부터 대조해 처음 기대와 달라진 경계를 고친 뒤 다시 실행합니다.
선언과 실행 후보
- 빌드 기준
build.gradle에서 Java 기능 수준 25와 Boot 4.1.1 BOM을 선언합니다. JDK vendor나 patch까지 고정한 선언은 아닙니다. - Gradle의 JVM
./gradlew --version에서 Wrapper의 Launcher JVM과 빌드를 수행하는 Daemon JVM을 구분합니다. - Toolchain 후보
./gradlew javaToolchains는 발견한 JDK 목록입니다. 특정 작업이 선택한 실행 파일의 증거는 아닙니다.
실제 선택과 실행
- 작업의 실제 JDK
./gradlew clean test --info의 compile/test 실행 경로를 확인합니다. compiler와 test launcher는 구분합니다. - 테스트 프로세스
RuntimeVersionsTest가 실제 Java 25와 테스트 classpath의 Boot 4.1.1·Spring 7.0.x metadata를 읽습니다. metadata가 없으면 unknown입니다. - 첫 불일치 경계
선언 → Gradle JVM → 후보 → 작업 선택 → 테스트 프로세스를 대조합니다. 다른 경계부터 수정하고 같은 검사를 반복합니다.
셸 Java 확인의 범위
java --version은 셸 기본 Java의 단서입니다. Gradle JVM과 작업 compiler를 확정하지 않습니다.
애플리케이션 실행은 별도 관찰
테스트 프로세스의 버전이 실제 bootRun 애플리케이션 JVM까지 증명하지 않습니다. 연습의 VersionReport처럼 애플리케이션 시작 경로에서 다시 읽습니다.
Java toolchain 기능 수준과 vendor·patch는 다른 조건입니다. Wrapper 호환 범위와 JDK 설치를 먼저 확인한 뒤 실제 선택과 실행을 관찰합니다.
위 그림은 build.gradle에 적은 버전이 실제 실행에 쓰였는지를 앞 단계부터 차례로 확인하는 순서를 보여 줍니다. build.gradle에서 Java 기능 수준 25와 Spring Boot BOM을 선언하고, Gradle 자체를 실행하는 런처 JVM과 데몬 JVM을 확인한 다음, 발견된 JDK 후보 가운데 각 작업이 실제로 고른 컴파일러와 런처를 확인합니다. 마지막으로 테스트 프로세스와 bootRun으로 띄운 애플리케이션 JVM에서 버전을 각각 읽습니다. test와 bootRun은 서로 다른 명령의 갈래이며 둘을 이어서 실행하라는 뜻은 아닙니다. 그림 왼쪽의 java --version과 javaToolchains는 참고할 단서일 뿐, 특정 작업이 어느 JDK를 골랐는지 알려 주지는 않습니다. 그림에 적힌 Boot 4.1.1은 이 교재를 처음 쓸 때의 버전이고, 본문은 그 뒤에 나온 패치 버전인 4.1.2를 기준으로 합니다. 패치 버전만 다르므로 확인하는 순서는 같습니다.
프로젝트 버전 선택
먼저 Spring Initializr에서 Gradle - Groovy, Java, Spring Boot 4.1.2, Java 25를 선택하고, Spring Web과 Thymeleaf 의존성을 추가해 프로젝트를 내려받습니다. 압축을 풀면 gradlew, gradlew.bat, gradle/wrapper 디렉터리가 함께 들어 있습니다. 이 파일들이 Gradle Wrapper이며, Gradle을 따로 설치하지 않아도 프로젝트가 정한 버전의 Gradle을 내려받아 실행해 줍니다. 2026년 11월에 Initializr가 만든 Wrapper는 Gradle 9.8.2를 가리킵니다.
이 교재는 Initializr가 만든 Wrapper를 그대로 씁니다. 빈 디렉터리에서 Wrapper를 직접 만드는 방법은 다루지 않으며, 아무 버전의 Gradle로 Wrapper를 만들어서도 안 됩니다. Spring Boot 4.1.2는 Gradle 8.14 이상의 8.x 또는 9.x를 요구하므로, 직접 만들 때에도 먼저 이 범위의 버전을 골라야 합니다.
Boot가 요구하는 Gradle 버전과, Gradle이 Java 25를 지원하는 버전은 따로 확인해야 합니다. Gradle의 Java 호환 표에 따르면 Java 25를 도구 체인으로 쓰는 것과 Gradle 자체를 Java 25로 실행하는 것은 모두 Gradle 9.1.0부터 지원됩니다. 그러므로 Boot의 최소 조건인 8.14만 보고 Java 25와 함께 써도 된다고 판단하면 안 되고, gradle/wrapper/gradle-wrapper.properties의 distributionUrl과 ./gradlew --version으로 Wrapper가 9.1 이상인지 확인합니다.
이제 내려받은 프로젝트에서 두 파일을 확인하고 고칩니다. settings.gradle은 프로젝트의 이름을 정하고, build.gradle은 플러그인과 의존성, Java 도구 체인을 정합니다.
rootProject.name = 'board'plugins {
id 'java'
id 'org.springframework.boot' version '4.1.2'
}
group = 'dev.andongmin'
version = '0.1.0'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation platform(
'org.springframework.boot:spring-boot-dependencies:4.1.2')
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}Spring Boot 4.1.2는 Java 17 이상에서 실행되지만, 이 교재는 Java 25를 언어 기준으로 씁니다. sourceCompatibility = 25만 적는 방식은 지금 Gradle을 실행하는 JDK에 컴파일 옵션만 바꾸어 넘기지만, 도구 체인은 컴파일에 쓸 JDK 자체를 고릅니다. 다만 languageVersion = 25는 Java 기능 수준만 정할 뿐 JDK 공급사(vendor)나 패치 버전까지 정하지는 않습니다. 컴퓨터에 조건에 맞는 JDK가 없고 자동으로 내려받을 저장소도 설정하지 않았다면 빌드는 바로 실패합니다. 예전 JDK로 조용히 컴파일되는 것보다 이렇게 바로 실패하는 편이 안전합니다.
의존성도 하나씩 살펴보겠습니다. Initializr의 Spring Web은 spring-boot-starter-webmvc로 들어옵니다. Boot 4에서는 예전 이름인 spring-boot-starter-web도 아직 쓸 수 있지만, 4.1.2에서는 spring-boot-starter-webmvc로 대체되어 사용 중단 예정인 스타터입니다. webmvc라는 이름은 Spring MVC와 내장 Tomcat을 쓴다는 뜻도 더 분명하게 보여 줍니다. 테스트용 MVC 모듈도 별도 스타터로 나뉘어 있어서 spring-boot-starter-webmvc-test를 추가하며, 이 스타터가 공통 테스트 스타터인 spring-boot-starter-test를 함께 가져오므로 둘을 중복해 적지 않습니다. junit-platform-launcher는 Gradle이 JUnit 테스트를 찾아 실행할 때 쓰는 라이브러리입니다. Gradle의 테스트 문서는 JUnit Jupiter를 쓸 때 이 라이브러리를 testRuntimeOnly로 선언하라고 안내하며, Initializr도 같은 줄을 넣어 줍니다.
Boot 플러그인은 실행 가능한 JAR를 만드는 작업 등을 추가하지만, 의존성 버전을 관리하지는 않습니다. Initializr가 만든 build.gradle에는 버전 관리를 위해 io.spring.dependency-management 플러그인이 들어 있는데, 이 교재는 그 플러그인을 빼고 Gradle이 기본으로 제공하는 platform으로 Spring Boot의 BOM(Bill of Materials)을 가져옵니다. BOM은 함께 쓰기로 검증된 라이브러리 버전의 목록이며, 덕분에 Spring 라이브러리마다 버전을 따로 적지 않아도 됩니다. 테스트 구성은 implementation 구성을 물려받으므로 테스트 쪽에 BOM을 한 번 더 적을 필요도 없습니다. 두 방식 중 무엇을 써도 되지만, 둘을 함께 적용하지는 않습니다.
최소 애플리케이션 실행
패키지의 가장 위에 시작 클래스를 둡니다. @SpringBootApplication의 기본 설정은 이 클래스가 있는 패키지와 그 아래 패키지에서 컴포넌트를 찾으므로, 시작 클래스의 위치가 곧 Spring이 살펴볼 범위가 됩니다.
package board;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BoardApplication {
public static void main(String[] args) {
SpringApplication.run(BoardApplication.class, args);
}
}SpringApplication.run()은 이 클래스를 설정의 출발점으로 삼아 애플리케이션 컨텍스트를 만들고, 웹 의존성이 있으므로 내장 Tomcat까지 시작합니다. run()이 돌아온 뒤에도 Tomcat이 요청을 기다리므로, 프로세스는 직접 멈출 때까지 계속 실행됩니다. 실행은 Wrapper로 합니다. macOS와 Linux의 셸에서는 다음 명령으로 실행합니다.
./gradlew bootRun./gradlew의 ./는 현재 디렉터리에 있는 스크립트를 실행하라는 뜻이므로, build.gradle이 있는 프로젝트 루트에서 실행해야 합니다. 처음 실행할 때는 Wrapper가 정한 버전의 Gradle을 내려받으므로 시간이 더 걸립니다. Windows의 PowerShell에서는 Wrapper의 배치 파일을 실행하며, PowerShell도 현재 디렉터리의 파일을 실행할 때는 앞에 .\를 붙여야 합니다.
.\gradlew.bat bootRunbootRun은 프로젝트의 Java 도구 체인으로 고른 JDK로 애플리케이션을 실행하므로, 셸에서 java를 입력했을 때 실행되는 Java와 같을 필요는 없습니다. 시작 로그에서 다음 세 줄을 찾습니다. 실제 로그에는 앞에 시각과 스레드 이름이, 뒤에 컨텍스트 경로와 걸린 시간이 더 붙습니다.
:: Spring Boot :: (v4.1.2)
Tomcat started on port 8080 (http)
Started BoardApplication여기서는 서버가 시작되었는지만 확인합니다. 아직 페이지나 컨트롤러를 만들지 않았으므로 GET /에 200 응답이 오기를 기대하지 않습니다. 다음 글의 정적 시작 화면에서 static/index.html을 추가한 뒤 첫 HTML 응답을 확인합니다.
다른 프로그램이 이미 8080 포트를 쓰고 있으면 애플리케이션은 시작되지 않고, Spring Boot가 다음과 같이 실패한 이유와 해결 방법을 알려 줍니다.
***************************
APPLICATION FAILED TO START
***************************
Description:
Web server failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.이때 아무 프로세스나 종료하기 전에 어떤 프로그램이 포트를 쓰고 있는지 먼저 확인합니다. 이번 한 번만 다른 포트로 띄우려면 ./gradlew bootRun --args='--server.port=8081'처럼 실행할 때 인자로 포트를 넘깁니다.
버전 확인 지점
이 절의 RuntimeVersions.java는 다음 글의 JSON 응답이 호출하는 값 객체이므로, JSON 예제로 넘어가기 전에 반드시 작성해 둡니다. 그 뒤의 테스트와 명령은 서로 다른 실행 지점에서 버전을 확인할 때 씁니다.
빌드가 성공했다는 사실만으로는 실제로 어떤 버전이 실행되었는지 알 수 없습니다. 그래서 그림의 순서대로 Gradle의 런처 JVM과 데몬 JVM, 발견된 도구 체인 후보, 작업이 실제로 고른 컴파일러, 테스트 워커(worker)가 불러온 Java와 Boot, Spring의 버전을 하나씩 확인합니다. bootRun으로 띄운 애플리케이션 프로세스는 테스트와 다른 프로세스이므로 시작 과정에서 따로 확인합니다.
./gradlew --version
./gradlew javaToolchains./gradlew --version의 출력에는 Gradle 버전과 함께 Launcher JVM 줄과 Daemon JVM 줄이 따로 나옵니다. 앞의 것은 Gradle 클라이언트를 시작한 JVM이고, 뒤의 것은 빌드를 수행할 데몬의 JVM입니다. javaToolchains는 설치되었거나 자동으로 찾은 JDK 후보를 나열할 뿐이며, 특정 작업이 그중 어느 JDK를 골랐는지는 알려 주지 않습니다. 작업이 실제로 고른 컴파일러는 ./gradlew clean test --info의 작업 로그에서 확인하고, 테스트 워커가 실행한 Java와 불러온 프레임워크 버전은 아래 값 객체로 확인합니다.
테스트 워커와 애플리케이션 코드 양쪽에서 다시 쓸 수 있도록 작은 값 객체를 추가합니다. 이 클래스는 웹과 관계가 없고, Spring이 제공하는 정적 메서드만 호출합니다.
package board.support;
import static java.util.Objects.requireNonNullElse;
import org.springframework.boot.SpringBootVersion;
import org.springframework.core.SpringVersion;
public record RuntimeVersions(
int javaFeature,
String boot,
String framework
) {
public static RuntimeVersions current() {
return new RuntimeVersions(
Runtime.version().feature(),
requireNonNullElse(SpringBootVersion.getVersion(), "unknown"),
requireNonNullElse(SpringVersion.getVersion(), "unknown"));
}
}Runtime.version().feature()는 지금 실행 중인 JVM의 기능 버전, 곧 Java 25라면 25를 돌려줍니다. SpringBootVersion.getVersion()은 Boot를 빌드할 때 소스에 써 넣은 버전 문자열을 돌려주고, SpringVersion.getVersion()은 Spring JAR 파일에 기록된 패키지 정보에서 버전을 읽습니다. 보통의 Gradle 실행 클래스 경로에서는 두 값이 모두 정상적으로 읽히지만, 일부 클래스 로더나 패키지 정보를 지운 배포본에서는 SpringVersion.getVersion()이 null을 돌려줄 수 있습니다. 그래서 RuntimeVersions는 null 대신 unknown을 저장하고, 그런 경우 아래 테스트는 NullPointerException이 아니라 버전이 기대와 다르다는 실패로 끝납니다.
package board.support;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
class RuntimeVersionsTest {
@Test
void 교재의_실행_기준과_일치한다() {
var versions = RuntimeVersions.current();
assertThat(versions.javaFeature()).isEqualTo(25);
assertThat(versions.boot()).isEqualTo("4.1.2");
assertThat(versions.framework()).startsWith("7.0.");
}
}./gradlew test --tests board.support.RuntimeVersionsTestGradle은 기본적으로 실패한 테스트만 자세히 보여 주므로, 성공하면 마지막에 BUILD SUCCESSFUL 한 줄이 나옵니다. 테스트별 결과는 build/reports/tests/test/index.html 보고서에서 볼 수 있습니다.
BUILD SUCCESSFUL이 테스트는 애플리케이션 컨텍스트를 만들지도, bootRun 프로세스를 띄우지도 않습니다. 필요한 것이 정적 메서드로 읽는 세 버전 값뿐이므로 순수 JUnit 테스트가 가장 작고 정확합니다. 그러므로 이 결과를 실제 애플리케이션 프로세스의 버전으로 넓혀 해석해서는 안 됩니다. bootRun으로 띄운 애플리케이션의 버전까지 확인하려면 아래 연습 문제의 VersionReport처럼 애플리케이션이 시작할 때 같은 값을 따로 출력합니다.
Boot는 빌드에 선언한 패치 버전 4.1.2와 정확히 같은지 검사하고, Spring Framework는 Boot BOM이 관리하는 7.0.x 범위에 있는지만 검사합니다. Spring Boot 4.1.2의 BOM이 고른 Spring Framework는 7.0.10이지만, Framework의 패치 버전은 Boot를 올릴 때마다 함께 바뀌므로 테스트가 패치까지 고정하지 않게 했습니다. 반대로 자동 설정이나 빈 연결을 확인하는 글에서는 컨텍스트를 실제로 띄웁니다. 무엇을 확인하느냐에 따라 테스트의 크기를 고르는 것입니다.
디렉터리 역할 구성
아직 쓰지 않는 클래스를 미리 만들 필요는 없지만, 앞으로 코드가 쌓일 위치는 정해 둡니다. 아래 패키지 이름은 코드가 맡는 일을 기준으로 나눈 것입니다. 모두 BoardApplication이 있는 board 패키지 아래에 있으므로, 앞에서 말한 컴포넌트 탐색 범위 안에 들어갑니다.
board/
├─ build.gradle
├─ settings.gradle
└─ src/
├─ main/
│ ├─ java/board/
│ │ ├─ BoardApplication.java
│ │ ├─ application/ # 사용 사례
│ │ ├─ config/ # 빈 조립·설정
│ │ ├─ domain/ # 업무 규칙
│ │ ├─ infrastructure/ # 저장 기술
│ │ ├─ support/ # 실행 지원
│ │ └─ web/ # HTTP·화면 처리
│ └─ resources/
│ ├─ static/
│ └─ templates/
└─ test/java/board/Git은 빈 디렉터리를 저장하지 않습니다. 위 트리는 앞으로 코드가 놓일 위치를 보여 주는 지도이며, 실제 디렉터리는 그 코드를 처음 만들 때 만듭니다.
PROJECT ROADMAP · PACKAGE BOUNDARIES
스캔 루트 아래에서 책임을 나누고 안쪽 계약에 의존한다
BoardApplication은 board 하위의 기본 스캔 루트입니다. 화살표는 코드 의존 또는 조립 방향이며 실행 순서가 아닙니다. 패키지는 해당 책임의 코드가 생길 때 만듭니다.
BoardApplication · 기본 스캔 루트
board.* 아래에서 컴포넌트를 찾는 Boot 진입점입니다. 빈 패키지를 미리 만들 필요는 없습니다.
web
HTTP 입력·응답
application 사용 사례 호출
application
등록·조회 순서
domain 규칙과 port 사용
domain
업무 규칙·저장 port
안쪽 계약
infrastructure
바깥 기술 adapter
domain 또는 application port 구현
config
빈 생성·조립
application과 adapter 선택
support
실행 지원
버전 관찰·bootstrap 보조
후속 MemoryPostRepository는 domain의 PostRepository를 구현합니다. application에 port가 생겨도 infrastructure adapter가 그 안쪽 계약에 의존하는 원칙은 같습니다.
위 그림의 화살표는 실행 순서가 아니라 코드가 어느 쪽에 의존하는지를 나타냅니다. web은 HTTP 요청을 받아 application의 사용 사례를 호출하고, application은 domain의 업무 규칙과 저장 포트(port)를 씁니다. infrastructure는 바깥 기술을 다루는 어댑터(adapter)로서 domain이 정한 인터페이스를 구현합니다. 의존이 모두 안쪽의 domain을 향하므로, 저장 기술을 바꾸어도 업무 규칙은 그대로 둘 수 있습니다. 그림의 점선처럼 config는 application과 infrastructure의 구현을 골라 Spring 빈으로 조립하는 곳이며, 업무 규칙을 두는 곳은 아닙니다. support는 버전 확인처럼 실행을 돕는 코드를 두는 곳으로, 업무 계층 바깥에 있고 다른 패키지를 업무 목적으로 호출하지 않습니다. 뒤에서 만들 MemoryPostRepository가 domain의 PostRepository를 구현하는 것이 이 구조의 첫 예입니다.
최상위 패키지를 controller, service, repository로만 나누면 기술 역할은 잘 보이지만, 게시판이라는 업무와 관련된 코드가 여러 곳으로 흩어지기 쉽습니다. 이 교재는 계층 사이의 의존 방향을 먼저 분명히 보기 위해 위 구조로 시작하고, 기능이 커지면 post 같은 기능 패키지 아래에 계층을 모으는 방식과 비교합니다.
도구 체인 오류 해석
build.gradle의 도구 체인을 25에서 21로 바꾸고, Java 25에서만 쓸 수 있는 문법을 소스에 넣었다고 해 봅시다. 이때 일어날 수 있는 실패는 두 가지입니다. Gradle이 Java 21 컴파일러를 정상적으로 고른 뒤 새 문법에서 컴파일 오류를 낼 수도 있고, Java 21 도구 체인을 찾지 못해 컴파일하기 전에 도구 체인을 해결하지 못했다는 오류를 낼 수도 있습니다. 두 경우 모두 JAVA_HOME만 바꾸면 해결된다고 단정하면 안 됩니다. Gradle 데몬이 이미 다른 JVM으로 떠 있을 수 있고, 도구 체인은 JAVA_HOME과 다른 JDK를 고를 수 있기 때문입니다.
이럴 때는 다음 순서로 원인의 범위를 좁힙니다.
java --version
./gradlew --version
./gradlew javaToolchains
./gradlew clean test --infojava --version은 셸의 기본 Java를 알려 주는 단서일 뿐, Gradle이 쓸 Java를 정하지 않습니다. ./gradlew --version은 런처 JVM과 데몬 JVM을, javaToolchains는 발견된 후보를 보여 줍니다. 그다음 clean test --info의 로그에서 작업이 실제로 고른 컴파일러를 확인하고, RuntimeVersionsTest로 테스트 워커의 Java와 Boot, Spring 버전을 확인합니다. bootRun으로 띄운 애플리케이션은 VersionReport처럼 시작할 때 버전을 출력하는 코드로 따로 확인합니다. 이 값들이 모두 같은 JDK일 필요는 없지만, 어느 단계에서 처음으로 기대와 달라졌는지를 찾고 각 Java가 맡은 역할을 설명할 수 있어야 합니다.
정리
- Spring Boot는 Spring을 대신하는 기술이 아니라, Spring 애플리케이션의 의존성과 설정을 기본값으로 묶어 주는 도구입니다.
- 이 교재는 Java 25와 Spring Boot 4.1.2(Spring Framework 7.0.10), Initializr가 만든 Gradle 9.8.2 Wrapper를 기준으로 합니다. Java 25는 Gradle 9.1.0부터 공식 지원됩니다.
- Java 도구 체인은 컴파일과 실행에 쓸 JDK의 기능 수준을 정하지만, 공급사와 패치 버전까지 정하지는 않습니다.
- 셸의
java, Gradle의 런처와 데몬 JVM, 도구 체인 후보, 작업이 고른 JDK, 테스트 워커와 애플리케이션 JVM은 서로 다를 수 있으므로 각각 확인합니다. - 시작 클래스가 있는 패키지와 그 아래가 컴포넌트를 찾는 범위가 되며, 패키지는 의존이 안쪽의
domain을 향하도록 나눕니다.
연습 문제
1. 시작할 때 버전 출력하기
프로젝트에 VersionReport라는 CommandLineRunner 빈을 추가해, 애플리케이션이 시작할 때 Java와 Boot, Spring Framework의 버전을 한 줄로 출력하세요. 단, 테스트에서는 출력된 문자열을 비교하지 말고 RuntimeVersions.current()의 값 자체를 검사해야 합니다. 출력 형식은 쉽게 바뀌지만, 검사해야 할 버전 기준은 그대로이기 때문입니다.
해설 보기
package board.support;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public final class VersionReport implements CommandLineRunner {
@Override
public void run(String... args) {
var value = RuntimeVersions.current();
System.out.printf(
"runtime java=%d boot=%s framework=%s%n",
value.javaFeature(), value.boot(), value.framework());
}
}CommandLineRunner는 애플리케이션 컨텍스트가 준비된 뒤 한 번 호출되므로, bootRun으로 실행하면 시작 로그 끝에 runtime java=25 boot=4.1.2 framework=7.0.10 같은 줄이 나옵니다. 이 줄은 테스트 워커가 아니라 실제 애플리케이션 JVM에서 읽은 값입니다. 운영 환경에서 이 출력이 필요 없다면 프로필이나 조건부 빈 등록으로 VersionReport 자체를 등록하지 않습니다. 버전 테스트는 여전히 RuntimeVersionsTest 하나로 충분합니다.
2. 같은 JDK인가
java --version은 Java 21을 보여 주는데 RuntimeVersionsTest는 통과했습니다. 무엇이 일어났는지 설명하세요.
해설 보기
셸의 기본 Java와 테스트를 실행한 JDK가 다른 것입니다. build.gradle의 도구 체인이 Java 25를 요구하므로 Gradle은 발견한 후보 가운데 Java 25 JDK를 골라 컴파일과 테스트에 썼고, 그래서 테스트 워커의 Runtime.version().feature()가 25를 돌려주었습니다. ./gradlew javaToolchains에 Java 25 후보가 보이는지, clean test --info의 로그에서 어떤 JDK의 컴파일러를 썼는지 확인하면 이 설명을 직접 확인할 수 있습니다.
이제 실행 기준이 정해졌습니다. 다음 글 정적 콘텐츠·MVC·JSON 응답에서는 같은 서버에서 정적 파일과 MVC 뷰, JSON 응답을 각각 만들고, 컨트롤러의 반환값이 어떤 처리 경로로 흘러가는지 비교합니다.