개발에 AtoZ까지

[에러] UnsupportedClassVersionError: class file version 65.0 — 숫자 두 개만 보면 끝납니다 본문

백엔드/JAVA

[에러] UnsupportedClassVersionError: class file version 65.0 — 숫자 두 개만 보면 끝납니다

AtoZ 개발자 2026. 8. 16. 21:40
반응형

로컬에서는 잘 돌던 jar를 서버에 올렸더니 실행하자마자 죽습니다. CI에서만 실패하는 경우도 있습니다.

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 65.0), this version of the Java Runtime only recognizes
class file versions up to 61.0

메시지가 길어 보이지만 필요한 정보는 숫자 두 개뿐입니다. 65.0과 61.0. 이 둘을 자바 버전으로 바꾸면 원인이 바로 나옵니다.

이 글을 다 읽으면 세 가지가 정리됩니다. 첫째, class file version을 자바 버전으로 머릿속에서 바로 변환할 수 있습니다. 둘째, 컴파일 쪽과 실행 쪽 중 어디를 고쳐야 하는지 판단할 수 있습니다. 셋째, 빌드 도구에서 버전을 고정해 다시 안 겪게 만드는 설정을 확인할 수 있습니다.

이 글은 자바 가상 머신 명세와 Gradle·Maven 공식 문서를 기준으로 정리했습니다.

class file major version과 자바 버전의 대응, 그리고 변환 규칙

숫자를 자바 버전으로 바꾸는 규칙

클래스 파일 첫머리에는 major_version과 minor_version이 들어갑니다. 자바 가상 머신 명세가 정한 값이고, 규칙이 아주 단순합니다.

major version = 자바 버전 + 44
자바 버전     = major version − 44

그래서 위 메시지는 이렇게 읽힙니다.

  • class file version 65.0 → 65 − 44 = 자바 21로 컴파일된 클래스
  • only recognizes ... up to 61.0 → 61 − 44 = 자바 17까지만 아는 런타임

즉 "자바 21로 만든 걸 자바 17에서 실행했다"는 뜻입니다. 자주 보게 되는 값만 표로 두면 이렇습니다.

major version 자바 버전   major version 자바 버전
52 8   61 17
55 11   65 21
59 15   69 25

자바는 위로는 호환되고 아래로는 호환되지 않습니다. 낮은 버전으로 만든 클래스는 높은 런타임에서 잘 돌지만, 그 반대는 이 예외가 납니다.

자바 가상 머신 명세의 클래스 파일 구조 — major_version 필드 (출처: docs.oracle.com, 확인일 2026-08-14)

원인은 셋 중 하나입니다

1. 실행 환경의 자바가 낮다

가장 흔합니다. 개발 PC에는 21이 깔려 있고 서버나 컨테이너에는 17이 있는 경우죠. Dockerfile의 베이스 이미지를 그대로 두고 프로젝트만 올린 상황이 대표적입니다.

# 실행 환경에서 먼저 확인
java -version

2. 라이브러리가 더 높은 버전으로 빌드됐다

내 코드는 17로 컴파일했는데, 의존성 중 하나가 21로 빌드된 경우입니다. 이때 메시지의 클래스 이름이 내 패키지가 아니라 라이브러리 패키지로 나옵니다. 그게 구분점입니다.

org/somelib/Foo has been compiled by a more recent version ...

이런 경우엔 그 라이브러리의 지원 자바 버전을 확인하고, 낮은 버전을 지원하는 릴리스로 내리거나 런타임을 올려야 합니다.

3. 빌드하는 JDK와 실행하는 JDK가 다르다

IDE는 21로 컴파일하는데 Gradle 데몬은 17을 쓰거나, CI 이미지의 기본 JDK가 로컬과 다른 경우입니다. "로컬에서는 되는데 CI에서만" 나는 실패가 대개 여기입니다.

컴파일 쪽을 내릴지, 실행 쪽을 올릴지 정하는 기준

어디를 고칠지 정하는 기준

상황 고칠 쪽
운영 서버·컨테이너의 자바를 바꿀 수 있다 실행 쪽을 올린다(가장 단순)
운영 자바 버전이 고정돼 있다 컴파일 쪽을 낮춘다
팀·CI마다 자바가 다르다 빌드 도구의 toolchain으로 고정한다
라이브러리 때문이다 그 라이브러리 버전을 내린다

컴파일 쪽을 낮추는 방법

-source/-target만 쓰면 함정이 있습니다. 낮은 버전으로 표시만 하고 새 버전의 API를 그대로 참조해 버려서, 실행 시 NoSuchMethodError가 나는 경우가 생깁니다. 그래서 --release를 쓰는 편이 안전합니다.

// Gradle — release 옵션
tasks.withType(JavaCompile).configureEach {
    options.release = 17
}
<!-- Maven -->
<properties>
    <maven.compiler.release>17</maven.compiler.release>
</properties>

--release는 지정한 버전의 표준 API만 참조하도록 컴파일러가 검사해 줍니다. 표시만 낮추는 것과 실제로 낮추는 것의 차이입니다.

팀 전체를 고정하는 방법 — toolchain

가장 재발이 적은 방법입니다. Gradle 툴체인을 쓰면 개발자 PC에 어떤 JDK가 깔려 있든 빌드가 쓸 JDK를 명시할 수 있습니다.

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}
// 어떤 JDK가 감지되는지 확인
// ./gradlew -q javaToolchains

Maven에서는 toolchains 플러그인이나 maven.compiler.release 조합을 씁니다. 어느 쪽이든 버전을 사람 기억이 아니라 파일에 적어 두는 것이 핵심입니다.

Gradle 공식 문서의 Java toolchain 설정 (출처: docs.gradle.org, 확인일 2026-08-14)

컨테이너로 배포한다면 베이스 이미지도 같이 맞춰 둡니다.

# 빌드와 실행 자바 버전을 명시적으로 일치시킨다
FROM eclipse-temurin:17-jdk AS build
WORKDIR /app
COPY . .
RUN ./gradlew clean bootJar --no-daemon

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

실제 버전을 눈으로 확인하는 명령

추측보다 확인이 빠릅니다.

# 1) 실행 환경의 자바 버전
java -version

# 2) 클래스 파일이 몇으로 컴파일됐는지 (major version 확인)
javap -verbose -cp app.jar com.example.App | grep major

# 3) jar 안의 특정 클래스만 꺼내 확인
unzip -p app.jar com/example/App.class | javap -verbose - | grep major

# 4) Gradle 이 어떤 JDK 를 쓰는지
./gradlew -q javaToolchains
./gradlew --version

javap의 major 값에서 44를 빼면 그 클래스가 어떤 자바로 컴파일됐는지 나옵니다. 라이브러리 때문인지 내 코드 때문인지도 이걸로 갈립니다.

확인 순서

  • [ ] 에러 메시지의 두 숫자를 각각 −44 해서 자바 버전으로 바꾼다
  • [ ] 메시지의 클래스가 내 패키지인지 라이브러리인지 본다
  • [ ] 실행 환경에서 java -version을 확인한다
  • [ ] javap로 실제 클래스의 major version을 확인한다
  • [ ] 운영 자바를 올릴 수 있으면 올리고, 아니면 --release로 낮춰 컴파일한다
  • [ ] 빌드 도구에 toolchain을 적어 재발을 막는다
  • [ ] 컨테이너 배포라면 베이스 이미지 태그도 같은 버전으로 맞춘다

다음에 볼 글

애플리케이션이 시작되지 못하는 다른 원인들도 이어서 정리해 두었습니다. 설정이 없어 DataSource를 못 만드는 Failed to determine a suitable driver class, 빈 등록에서 막히는 required a bean of type ... that could not be found입니다. 컨테이너 쪽 실행 환경을 정리하려면 Compose V2와 Spring Boot 연동 글도 함께 보시면 좋습니다.

잘못된 내용이나 보충이 필요한 부분이 있으면 댓글로 알려 주시면 반영하겠습니다.

참고한 자료

  • Java Virtual Machine Specification — The class File Format, major_version·minor_version (확인일 2026-08-14)
  • Gradle User Manual — Toolchains for JVM projects (확인일 2026-08-14)
  • Gradle User Manual / Maven Compiler Plugin — release 옵션 (확인일 2026-08-14)
  • javap, java -version 명령 문서 (확인일 2026-08-14)
반응형
Comments