Java 25와 Spring Boot
Gradle 도구 체인과 실행 출력으로 Java 25·Spring Boot 4.1·Spring 프레임워크 7의 기준선을 검증하고 게시판 프로젝트를 시작합니다.
Spring은 Java 객체를 만들고 연결하며 웹 요청을 처리하는 데 필요한 기반을 제공하는 프레임워크입니다.
Spring Boot는 Spring을 없애는 별도 기술이 아니라, 자주 쓰는 의존성과 설정을 합리적인 기본값으로 조립해 첫 실행을 쉽게 만드는 도구입니다.
이 관계를 먼저 기억하면 Spring과 Spring Boot라는 이름을 섞지 않게 됩니다.
첫 실행의 목표는 화려한 화면이 아니라 누가 받아도 같은 버전으로 빌드되는 프로젝트입니다.
JDK는 Java 코드를 컴파일하고 실행하는 도구 묶음이고, Gradle은 필요한 라이브러리를 내려받아 컴파일·실행 명령을 반복해 주는 빌드 도구입니다.
IDE가 표시하는 JDK, 터미널의 java, Gradle이 사용하는 JVM이 다르면 invalid source release 같은 오류가 생길 수 있습니다.
이 자습서의 누적 예제는 게시글을 등록하고 조회하는 회원가입·게시판 웹 애플리케이션입니다.
이번 문서에서는 아직 도메인 기능을 만들지 않습니다.
대신 앞으로 추가할 모든 코드가 공유할 실행 계약을 먼저 고정합니다.
프로젝트 버전 선택
먼저 Spring Initializr에서 Gradle-Groovy, Java, Spring Boot 4.1, Java 25를 선택하고 Spring Web MVC와 Thymeleaf 의존성을 추가해 프로젝트를 내려받습니다.
압축을 풀면 gradlew, gradlew.bat, gradle/wrapper가 함께 들어 있습니다.
이 파일들이 Gradle Wrapper이며 별도의 전역 Gradle 설치 없이 프로젝트가 정한 Gradle 버전을 실행하게 합니다.
이미 빈 디렉터리에서 시작했다면 전역 Gradle을 한 번 설치한 뒤 그 디렉터리에서 gradle wrapper를 실행해야 합니다.
Wrapper 파일이 생기기 전에는 ./gradlew 명령을 사용할 수 없습니다.
이 교재는 가장 단순한 Initializr 경로를 기준으로, 내려받은 프로젝트의 다음 두 파일을 확인하고 수정합니다.
settings.gradle은 프로젝트의 논리적 이름을 정하고, build.gradle은 플러그인·의존성·Java 도구 체인을 정합니다.
rootProject.name = 'board'plugins {
id 'java'
id 'org.springframework.boot' version '4.1.0'
}
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.0')
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
testImplementation platform(
'org.springframework.boot:spring-boot-dependencies:4.1.0')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}
tasks.named('test') {
useJUnitPlatform()
}Spring Boot 4.1은 Java 17 이상에서 실행할 수 있지만 이 교재는 Java 25를 언어 기준으로 사용합니다.
sourceCompatibility = 25만 적는 방식과 달리 도구 체인은 Gradle이 컴파일에 사용할 JDK 자체를 선택합니다.
로컬에 조건을 만족하는 JDK가 없고 자동 다운로드 저장소도 설정하지 않았다면 빌드가 즉시 실패합니다.
이 실패가 예전 JDK로 조용히 컴파일하는 것보다 안전합니다.
Boot 4.1에서는 spring-boot-starter-web도 사용할 수 있지만 spring-boot-starter-webmvc가 MVC와 Tomcat을 선택한다는 뜻을 더 직접 드러냅니다.
테스트용 MVC 모듈도 별도 스타터로 분리되어 있으므로 spring-boot-starter-webmvc-test를 함께 둡니다.
Boot 플러그인은 실행 JAR 작업을 추가하지만 의존성 버전을 단독으로 관리하지는 않습니다.
여기서는 네이티브 Gradle BOM 플랫폼을 사용해 각 Spring 라이브러리 버전을 따로 적지 않습니다.
io.spring.dependency-management 플러그인을 적용하는 방식도 가능하지만 두 방식을 중복 적용하지 않습니다.
최소 애플리케이션 실행
패키지 루트에 시작 클래스를 둡니다.
이후 컴포넌트 스캔은 이 패키지 아래만 대상으로 삼기 때문에 시작 클래스의 위치가 프로젝트 경계를 정합니다.
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);
}
}./gradlew bootRunWindows PowerShell 또는 명령 프롬프트에서는 gradlew.bat bootRun을 사용합니다.
시작 로그에서 다음 사실을 확인합니다.
:: Spring Boot :: (v4.1.0)
Tomcat started on port 8080 (http)
Started BoardApplication포트가 이미 사용 중이면 애플리케이션은 시작되지 않습니다.
이때 로그의 핵심은 Port 8080 was already in use입니다.
임의로 프로세스를 종료하기 전에 어떤 프로그램이 포트를 점유했는지 확인하거나 --server.port=8081로 한 번만 다른 포트를 선택합니다.
버전 확인 지점
빌드가 성공했다는 사실만으로는 실제 런타임 버전을 알 수 없습니다.
Gradle JVM, Java 도구 체인, 실행 중인 Spring 프레임워크를 각각 관찰합니다.
./gradlew --version
./gradlew javaToolchains애플리케이션 내부에서도 확인할 수 있도록 작은 값 객체를 추가합니다.
이 클래스는 웹과 무관하며 Spring 정적 API만 읽습니다.
package board.support;
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(),
SpringBootVersion.getVersion(),
SpringVersion.getVersion());
}
}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()).startsWith("4.1.");
assertThat(versions.framework()).startsWith("7.0.");
}
}./gradlew test --tests board.support.RuntimeVersionsTestRuntimeVersionsTest > 교재의_실행_기준과_일치한다() PASSED
BUILD SUCCESSFUL이 테스트는 애플리케이션 컨텍스트를 띄우지 않습니다.
필요한 것은 세 정적 버전 값뿐이므로 순수 JUnit 테스트가 가장 작고 정확합니다.
반대로 자동 설정이나 빈 연결을 검증하는 문서에서는 컨텍스트를 실제로 띄웁니다.
확인 대상에 따라 테스트 크기를 선택해야 합니다.
디렉터리 역할 구성
아직 존재하지 않는 클래스를 미리 만들 필요는 없지만, 앞으로 코드가 쌓일 위치는 합의해 둡니다.
board/
├─ build.gradle
├─ settings.gradle
└─ src/
├─ main/
│ ├─ java/board/
│ │ ├─ BoardApplication.java
│ │ ├─ application/ # 사용 사례
│ │ ├─ domain/ # 업무 규칙
│ │ ├─ infrastructure/ # 저장 기술
│ │ ├─ support/ # 실행 지원
│ │ └─ web/ # HTTP·화면 경계
│ └─ resources/
│ ├─ static/
│ └─ templates/
└─ test/java/board/빈 패키지는 Git에 저장되지 않습니다.
위 트리는 앞으로의 위치를 보여 주는 지도일 뿐이며 실제 디렉터리는 해당 코드가 생길 때 만듭니다.
controller, service, repository만으로 최상위 패키지를 나누면 기술 역할은 보이지만 게시판라는 도메인의 응집이 약해질 수 있습니다.
이번 자습서의 규모에서는 경계를 먼저 명확히 보기 위해 위 구조로 시작하고, 기능이 커지면 post 기능 아래에 계층을 모으는 방식과 비교합니다.
도구 체인 오류 해석
build.gradle의 도구 체인을 25에서 21로 바꾸고 Java 25에서만 허용되는 소스를 추가했다고 가정합니다.
가능한 실패는 두 종류입니다.
- Gradle이 Java 21 컴파일러를 정상 선택하고 새 문법에서 컴파일 오류를 낸다.
- Java 21 도구 체인을 찾지 못해 컴파일 전에 도구 체인 해결 오류를 낸다.
둘 다 JAVA_HOME만 바꾸면 된다고 단정하면 안 됩니다.
Gradle 데몬이 이미 다른 JVM으로 떠 있을 수 있고, 도구 체인이 별도 JDK를 선택할 수 있습니다.
다음 순서로 범위를 좁힙니다.
java --version
./gradlew --version
./gradlew javaToolchains
./gradlew clean test --infojava --version은 셸 기본 Java, gradlew --version의 JVM 항목은 Gradle 실행 Java, javaToolchains는 컴파일 후보를 보여 줍니다.
세 출력이 같은 값일 필요는 없지만, 어떤 Java가 어떤 일을 맡는지는 설명할 수 있어야 합니다.
연습 문제
프로젝트에 VersionReport라는 CommandLineRunner 빈을 추가해 시작할 때 Java·Boot·프레임워크 버전을 한 줄로 출력하세요.
단, 테스트에서는 로그 문자열을 비교하지 말고 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는 컨텍스트가 준비된 뒤 한 번 호출됩니다.
운영 로그가 너무 시끄럽다면 프로필이나 로그 레벨로 출력 여부를 조절할 수 있습니다.
버전 테스트는 여전히 RuntimeVersionsTest 하나로 충분합니다.
이제 실행 기반이 고정되었습니다.
다음 문서에서는 같은 서버에서 정적 파일, MVC 뷰, JSON 응답을 각각 만들고 반환 문자열이 어떤 처리기로 흘러가는지 직접 비교합니다.