/개발/UnsupportedClassVersionError 30초 진단·복구 런북 (class file 61.0)
개발UnsupportedClassVersionErrorclass file version

UnsupportedClassVersionError 30초 진단·복구 런북 (class file 61.0)

class file version 61.0/65.0 숫자만 보고 UnsupportedClassVersionError 원인을 30초에 특정하세요. Maven·Gradle·Docker·IntelliJ 복붙 설정으로 즉시 복구하는 실전 런북입니다.

UnsupportedClassVersionError 30초 진단·복구 런북 (class file 61.0)

"빌드는 됐는데 왜 실행이 안 되지?" — 에러 한 줄로 원인 90% 특정

로컬에서 mvn packagegradle build는 초록불로 끝났는데, 막상 서버나 컨테이너에서 실행하는 순간 이런 로그를 마주치고 이 글에 들어온 분이 많을 겁니다.

TEXT
Exception in thread "main" java.lang.UnsupportedClassVersionError:
com/example/App has been compiled by a more recent version of the Java Runtime
(class file version 61.0), this version of the Java Runtime only recognizes
class file versions up to 55.0

결론부터 말하면 이 에러는 컴파일에 사용한 JDK가 실행 중인 JRE보다 최신일 때만 발생합니다. 반대(구버전으로 컴파일 → 신버전으로 실행)는 하위 호환이 되므로 문제가 없습니다. 즉 이 에러를 만난 순간, 원인은 딱 하나로 좁혀집니다.

판단 규칙: 컴파일 버전 > 실행 버전 → 무조건 이 에러.

에러 메시지 안에 답이 이미 다 들어 있습니다. class file version 61.0은 이 클래스가 Java 17로 컴파일됐다는 뜻이고, up to 55.0은 지금 실행 중인 런타임이 Java 11까지만 이해한다는 뜻입니다. 두 숫자만 표에서 역추적하면 끝입니다. 스크롤을 내리며 순서대로 따라 하면 대부분 3~5분 안에 복구됩니다.

30초 진단: class file version 숫자를 JDK 버전으로 역추적

가장 먼저 에러의 두 숫자를 아래 표에서 찾으세요. major version 규칙은 Java 1.1이 45.0이고, 이후 메이저 버전마다 +1입니다.

class file versionJDK(Java) 버전릴리스 성격
52.0Java 8LTS
53.0Java 9
54.0Java 10
55.0Java 11LTS
56.0Java 12
57.0Java 13
58.0Java 14
59.0Java 15
60.0Java 16
61.0Java 17LTS
62.0Java 18
63.0Java 19
64.0Java 20
65.0Java 21LTS

위 예시 에러(61.0 vs 55.0)를 표에 대입하면 Java 17로 컴파일한 코드를 Java 11 런타임에서 실행한 것입니다. Spring Boot 3.x가 Java 17을 최소 요구로 잡으면서 이 조합의 충돌이 특히 급증했습니다.

실행 중인 런타임 확인

Bash
java -version

정상 출력 예시(Java 11 런타임):

TEXT
openjdk version "11.0.22" 2024-01-16
OpenJDK Runtime Environment Temurin-11.0.22+7
OpenJDK 64-Bit Server VM Temurin-11.0.22+7

여기서 11.0.22가 실행측 버전입니다. 에러의 55.0(Java 11)과 일치하죠.

컴파일된 class 파일의 버전 확인

.class 파일이 정말 몇으로 컴파일됐는지 직접 확인하려면 javap를 씁니다.

Bash
javap -verbose com/example/App.class | grep "major version"

정상 출력 예시:

TEXT
  major version: 61

61이 나왔다면 Java 17 컴파일이 확정입니다. 표의 두 숫자만 비교하면(컴파일 61 > 실행 55) 진단 끝입니다.

JAR 안에 뭐가 들어있는지 확인

배포된 JAR이 어떤 JDK로 빌드됐는지는 매니페스트에서 확인할 수 있습니다.

Bash
unzip -p app.jar META-INF/MANIFEST.MF

출력 예시:

TEXT
Manifest-Version: 1.0
Build-Jdk-Spec: 17
Build-Jdk: 17.0.10+7
Created-By: Maven JAR Plugin 3.4.1

Build-Jdk가 17인데 서버 java -version이 11이면, 답은 정해졌습니다. 이제 어디를 고칠지만 결정하면 됩니다. 선택지는 두 가지입니다.

  • 실행 런타임을 올린다 (서버/컨테이너 JRE를 컴파일 버전 이상으로)
  • 빌드 타겟을 낮춘다 (실행 런타임에 맞춰 컴파일)

프로덕션이 특정 버전에 고정돼 있다면 빌드 타겟을 낮추고, 최신으로 전환 중이라면 런타임을 올리는 편이 낫습니다.

빌드 타겟 정렬: Maven / Gradle 복붙 설정

Maven — source/target 대신 release를 쓰세요

pom.xml<properties>에 아래 한 줄만 추가합니다.

XML
<properties>
    <maven.compiler.release>17</maven.compiler.release>
</properties>

과거에는 이렇게 두 줄로 썼습니다.

XML
<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

release를 권장하는 이유는 부트클래스패스까지 함께 맞춰주기 때문입니다. source/target만 지정하면 문법 레벨과 바이트코드 버전은 맞지만, 실제로는 빌드 JDK의 최신 API를 참조할 수 있어 하위 런타임에서 NoSuchMethodError가 뜰 위험이 남습니다. releasejavac --release 플래그로 컴파일해 해당 버전의 API 시그니처만 노출하므로 이 함정을 원천 차단합니다.

Gradle — toolchain을 쓰세요 (Kotlin DSL)

KOTLIN
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

Groovy DSL이라면:

GROOVY
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

구식 방식은 이렇습니다.

GROOVY
sourceCompatibility = '17'
targetCompatibility = '17'

둘의 결정적 차이는 다음과 같습니다.

항목sourceCompatibilitytoolchain
의미문법/바이트코드 레벨만 지정컴파일에 쓸 JDK 자체를 지정
빌드 JDK 의존Gradle 실행 JDK에 종속없으면 자동 다운로드/탐색
팀 재현성낮음(개발자별 JDK 편차)높음(모두 동일 JDK 보장)

sourceCompatibility는 "Gradle을 Java 21로 돌리는데 target만 17"인 상황을 못 막습니다. 이때도 최신 API를 잘못 참조할 수 있죠. toolchain은 컴파일러 JDK 자체를 고정하므로 팀 전체가 동일한 결과를 냅니다. Java 17→21 전환기에 개발자마다 로컬 JDK가 뒤섞인 팀이라면 toolchain이 사실상 필수입니다.

환경별 함정 잡기: JDK가 여러 개일 때

빌드 설정을 맞췄는데도 재현된다면, 어떤 JDK가 실제로 선택되는지가 문제일 가능성이 큽니다.

macOS — 설치된 JDK 목록과 전환

Bash
/usr/libexec/java_home -V

출력 예시:

TEXT
Matching Java Virtual Machines (2):
    21.0.2 (arm64) "Eclipse Adoptium" - "OpenJDK 21.0.2"
    17.0.10 (arm64) "Eclipse Adoptium" - "OpenJDK 17.0.10"

특정 버전으로 JAVA_HOME 고정:

Bash
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
java -version   # 17로 바뀌었는지 확인

Linux — update-alternatives

Bash
sudo update-alternatives --config java

프롬프트에서 원하는 번호를 선택합니다. 다만 이건 java(런타임)만 바꿉니다. 컴파일에 쓰는 javac는 별도이므로 아래도 함께 맞추세요.

Bash
sudo update-alternatives --config javac

셸 세션 단위로만 바꾸려면 JAVA_HOME을 직접 지정하는 편이 안전합니다.

Bash
export JAVA_HOME=/usr/lib/jvm/temurin-17-jdk-amd64
export PATH=$JAVA_HOME/bin:$PATH

Windows — 어떤 java가 잡히는지 확인

POWERSHELL
where java

여러 경로가 뜨면 맨 위 경로가 실제로 실행되는 java입니다. JAVA_HOME과 시스템 Path를 원하는 JDK로 정리하세요. temurin, Corretto, Oracle JDK가 뒤섞여 설치된 환경에서 특히 자주 꼬입니다.

Docker 함정: 빌드는 21, 런타임은 17

가장 흔한 프로덕션 재현 케이스입니다. 로컬이나 CI는 Java 21로 빌드했는데 실행 이미지는 17-jre인 경우입니다.

Dockerfile
# ❌ 불일치 — 빌드 21, 런타임 17
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew bootJar

FROM eclipse-temurin:17-jre    # ← 여기가 문제
COPY --from=build /app/build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

이 이미지를 실행하면 class file version 65.0(Java 21) vs up to 61.0(Java 17) 에러가 뜹니다. build 스테이지와 runtime 스테이지의 메이저 버전을 반드시 맞추세요.

Dockerfile
# ✅ 일치 — 빌드/런타임 모두 21
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew bootJar

FROM eclipse-temurin:21-jre
COPY --from=build /app/build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

runtime 이미지를 낮출 수 없다면(예: 운영 정책상 17-jre 고정) 반대로 build 스테이지와 Gradle toolchain을 17로 낮춰야 합니다. 둘 중 하나로 통일하는 것이 핵심입니다.

IntelliJ 정렬: 세 곳을 전부 맞춰야 한다

IDE에서만 재현되거나, IDE 실행과 터미널 빌드 결과가 다르다면 IntelliJ 설정 3곳을 확인하세요. 하나만 어긋나도 증상이 재현됩니다.

  1. Project Structure → Project → SDK / Language level 프로젝트가 컴파일에 사용하는 기본 JDK와 문법 레벨입니다.
  2. Settings → Build, Execution, Deployment → Build Tools → Gradle → Gradle JVM Gradle 태스크를 실행할 때 쓰는 JVM입니다. 여기가 21인데 Project SDK가 17이면 CLI 빌드와 결과가 달라집니다.
  3. Settings → Build Tools → Maven → Runner → JRE (Maven 프로젝트) Maven 실행에 사용하는 JDK입니다.

세 곳이 각각 다른 이유는 IntelliJ가 "IDE 컴파일", "빌드 도구 실행", "프로젝트 기본"을 분리해서 관리하기 때문입니다. 헷갈리면 세 곳을 모두 같은 버전으로 통일하는 것이 가장 안전합니다.

"다시 안 터지게" 하는 체크리스트 3줄 + 팀 표준화

복구 후 재발을 막는 최소 체크리스트입니다.

  1. 빌드 타겟 명시: Maven은 maven.compiler.release, Gradle은 toolchain으로 JDK를 코드에 박아둔다.
  2. Docker build/runtime 버전 일치: 멀티스테이지의 build·runtime 이미지 메이저 버전을 동일하게.
  3. 실행 환경 검증: 배포 전 java -version(런타임) ↔ Build-Jdk(JAR) 두 숫자를 비교한다.

팀 표준화 팁으로는, Gradle toolchain에 자동 다운로드 프로비저닝을 설정하거나 .sdkmanrc(SDKMAN) 같은 파일로 JDK 버전을 리포지토리에 고정하는 방법이 있습니다. 개발자마다 temurin/Corretto가 뒤섞인 상태를 리포지토리 차원에서 통일하면, "내 로컬에선 되는데" 유형의 버전 충돌을 크게 줄일 수 있습니다. Java 17→21 LTS 전환기에는 CI 파이프라인의 빌드 JDK를 명시적으로 고정해 두는 것도 중요합니다.

정확한 매핑값과 최신 배포 정책은 공식 자료(Oracle JVM Specification의 The class File Format, Eclipse Temurin 도커 태그 문서) 확인이 필요합니다.

자주 묻는 질문 (FAQ)

Q. class file version 61.0은 정확히 어떤 Java 버전인가요? A. Java 17입니다. major version은 Java 1.1이 45이고 이후 버전마다 +1이므로, 61 = 45 + 16 = Java 17로 계산됩니다. 65.0은 Java 21입니다.

Q. 실행 런타임을 못 올립니다. 코드를 낮은 버전으로 컴파일만 하면 되나요? A. 대부분 됩니다. Maven은 maven.compiler.release, Gradle은 toolchain을 실행 런타임 이하로 맞추세요. 단, Java 17 이상의 문법(레코드, sealed 클래스 등)이나 신규 API를 이미 사용 중이라면 컴파일 자체가 실패하므로 코드 수정이 필요합니다.

Q. mvn compile은 되는데 실행만 에러가 납니다. 왜죠? A. 빌드에 사용한 JDK와 실행에 사용한 JRE가 다르기 때문입니다. java -version(실행측)과 JAR의 Build-Jdk(컴파일측) 두 숫자를 비교하세요. 컴파일 숫자가 더 크면 그게 원인입니다.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

초안 · AI (Content Reviewer)·검토 · Nodelog 편집자·발행 ·
관련 공식 문서Oracle Java 공식 문서

댓글

첫 번째 댓글을 남겨보세요.