[트러블슈팅] Java 21 + SonarQube 에러: ‘Unsupported class file major version 65’ 완벽 해결법

▶ TECH LOG — Troubleshooting


서론: 은행 차세대 프로젝트의 ADO 파이프라인이 멈추다

금융권 차세대 프로젝트를 진행하면서 기존 레거시 시스템을 Java 21(LTS)로 마이그레이션하는 작업을 맡고 있습니다. 은행 시스템 특성상 코드 품질과 보안 취약점 관리가 매우 엄격하게 요구되어, Azure DevOps(ADO)를 활용한 모든 빌드 파이프라인에서 SonarQube 스캔을 필수 게이트(Quality Gate)로 통과해야 했습니다. 관련해서 ADO + Java 21 Maven 에러 해결법 글도 참고하면 도움이 됩니다.

로컬 환경에서 Virtual Threads를 비롯한 Java 21의 신규 기능들을 검증하고 쾌재를 부른 것도 잠시, 코드를 푸시하고 ADO 파이프라인(Azure Pipelines)을 태우는 순간 시뻘건 빌드 실패 로그가 떨어졌습니다. 문제의 구간은 코드 품질과 정적 분석을 담당하는 SonarQube 스캔 단계였습니다. 테스트 코드도 모두 통과했고 애플리케이션 빌드도 정상인데, 오직 소나큐브 Maven 플러그인만 돌아가면 아래와 같은 에러를 뱉으며 장렬하게 산화했습니다.

[ERROR] Failed to execute goal org.sonarsource.scanner.maven:sonar-maven-plugin:3.9.1.2184:sonar (default-cli) on project my-service:
Execution default-cli of goal org.sonarsource.scanner.maven:sonar-maven-plugin:3.9.1.2184:sonar failed:
java.lang.IllegalArgumentException: Unsupported class file major version 65

차세대 프로젝트 특성상 QA 사이클이 촘촘하게 잡혀 있었고, 소나큐브 Quality Gate 실패는 곧 전체 배포 파이프라인 블로킹을 의미했기 때문에 빠른 원인 파악이 급선무였습니다.


원인 분석: ‘Major Version 65’의 진짜 의미

에러 로그에 등장하는 Unsupported class file major version 65라는 문구가 이 트러블슈팅의 핵심 키(Key)입니다. 자바(Java)는 컴파일될 때 .class 파일 내부에 고유한 버전 넘버(Major Version)를 각인합니다.

Java SE 버전 Class File Major Version
Java 8 52
Java 11 55
Java 17 61
Java 21 65

즉, 우리가 작성한 코드는 Java 21(버전 65)로 완벽하게 컴파일되었으나, 구버전의 Sonar Scanner(내부적으로 사용하는 ASM 바이트코드 분석 라이브러리)가 버전 65의 바이트코드를 해석할 능력이 없어 뻗어버린 것입니다. ASM 라이브러리는 JVM 바이트코드를 읽고 분석하는 핵심 컴포넌트인데, 특정 버전 이하의 ASM은 Java 21이 도입한 새로운 바이트코드 명세를 알지 못합니다. 이를 해결하기 위해서는 스캐너와 플러그인의 체급을 Java 21에 맞춰 올려주어야 합니다.


시행착오s: 이렇게 하면 안 된다.

원인을 파악하기 전에 몇 가지 잘못된 방향으로 시간을 허비했습니다. 같은 삽질을 반복하지 않도록 기록해 둡니다.

❌ 실패 시도 1: maven.compiler.release 옵션으로 다운그레이드 컴파일

처음에는 “소나큐브가 Java 21 바이트코드를 못 읽는 거라면, 빌드 결과물을 Java 17 수준으로 낮춰서 뱉으면 되지 않을까?”라고 생각했습니다. 그래서 pom.xml에 아래처럼 설정을 추가했습니다.

<!– ❌ 잘못된 접근: 컴파일 타겟을 17로 다운그레이드 –>
<properties>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

결과는 당연히 실패였습니다. source와 target 버전 불일치로 인해 Maven 컴파일 자체가 에러를 뱉었고, 설령 통과하더라도 Java 21의 신규 API(Virtual Thread 등)를 사용한 코드는 런타임에 NoSuchMethodError를 유발할 것이 뻔했습니다. Java 버전 마이그레이션은 롤백이 아닌 생태계 전체를 앞으로 올리는 방향으로 해결해야 한다는 교훈을 얻었습니다.

❌ 실패 시도 2: SonarQube Scanner CLI 단독 업데이트

두 번째 시도는 Maven 플러그인은 그대로 두고, 파이프라인 에이전트에 설치된 SonarQube Scanner CLI 바이너리만 최신 버전으로 교체하는 것이었습니다. Scanner CLI를 최신 버전으로 업데이트했더니 에러 메시지가 미묘하게 바뀌었지만 근본적인 문제는 해결되지 않았습니다.

원인을 파악해보니, Maven 빌드 환경에서는 실제로 실행되는 스캐너가 Maven 플러그인에 번들된 내장 Scanner이기 때문이었습니다. 즉, 외부에 별도로 설치된 Scanner CLI 버전과는 무관하게, pom.xml에 선언된 sonar-maven-plugin 내부의 ASM 버전이 문제의 핵심이었습니다. Scanner CLI 단독 업데이트는 Gradle 프로젝트나 CLI를 직접 호출하는 환경에서만 유효한 방법입니다.


해결 방법 1: Maven 플러그인 및 의존성 업데이트

가장 먼저 소스코드의 pom.xml 파일에서 SonarQube 관련 플러그인의 버전을 점검해야 합니다. Java 21을 공식적으로 안전하게 지원하기 위해서는 sonar-maven-plugin 버전을 최소 3.10.0 이상(권장 3.11.0 이상)으로 업그레이드해야 합니다.

<!– pom.xml의 properties 영역 수정 –>
<properties>
    <java.version>21</java.version>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
    <!– 기존 3.9.x 버전을 최신 호환 버전으로 변경 –>
    <sonar-maven-plugin.version>3.11.0.3922</sonar-maven-plugin.version>
</properties>

만약 프로젝트에서 jacoco-maven-plugin을 함께 사용하여 코드 커버리지를 측정하고 있다면, JaCoCo 역시 Java 21 바이트코드를 읽지 못해 커버리지가 0%로 나오는 현상이 동반될 수 있습니다. JaCoCo 버전도 0.8.11 이상으로 함께 올려주는 것이 정신 건강에 좋습니다. 금융권 프로젝트의 경우 코드 커버리지 수치가 품질 지표로 직결되기 때문에, 커버리지 측정 오류는 단순한 수치 이상의 문제를 야기할 수 있습니다.


해결 방법 2: Azure DevOps(ADO) 파이프라인 JDK 환경 점검

pom.xml을 수정하고 로컬(PC)에서는 스캔이 잘 도는데, 유독 ADO 파이프라인을 타면 똑같은 에러가 발생하는 경우가 있습니다. 이때는 백발백중 파이프라인을 구동하는 빌드 에이전트(Agent)가 구버전 JDK를 물고 있기 때문입니다.

💡 실무 체크리스트: 망분리 환경의 자체 호스팅 에이전트(Self-hosted Agent)

금융권 특성상 MS에서 제공하는 퍼블릭 호스팅 에이전트(Microsoft-hosted agent) 대신 폐쇄망 내부에 구축된 자체 호스팅 에이전트(Self-hosted agent)를 사용하는 경우가 많습니다. 이 경우 azure-pipelines.yml 파일만 수정한다고 해결되지 않으며, 인프라 담당 팀과 협의하여 해당 Linux/Windows 빌드 머신에 직접 JDK 21을 설치하고 OS의 JAVA_HOME 환경 변수를 수동으로 업데이트해야 합니다.

만약 에이전트에 이미 JDK 21이 설치되어 있거나 퍼블릭 에이전트를 사용 중이라면, azure-pipelines.yml 파일에 JavaToolInstaller@0 태스크를 명시적으로 추가하여 파이프라인 단계에서 사용할 Java 버전을 21로 픽스해 줍니다.

# azure-pipelines.yml
steps:
– task: JavaToolInstaller@0
  displayName: ‘Set up JDK 21’
  inputs:
    versionSpec: ’21’
    jdkArchitectureOption: ‘x64’
    jdkSourceOption: ‘PreInstalled’

SonarQube 서버 버전 호환성 확인

Maven 플러그인과 ADO 에이전트 환경을 모두 고쳤는데도 문제가 지속된다면, SonarQube 서버 자체의 버전도 점검해야 합니다. 이번 프로젝트에서 사용한 SonarQube는 Data Center Edition v2025.2로, 해당 버전은 Java 21을 공식 지원합니다.

📌 SonarQube 버전별 Java 21 지원 현황

SonarQube 2026.2 (최신)부터 Java 21 분석을 공식 지원합니다. 만약 서버 버전이 9.x 대라면 플러그인을 아무리 올려도 서버 측 분석 엔진이 Java 21 바이트코드를 처리하지 못해 동일한 에러가 재현됩니다. Data Center Edition 사용자라면 v2024.1 이상으로 서버를 업그레이드하는 것이 권장됩니다.

SonarQube 서버 버전 확인은 관리자 계정으로 접속 후 [Administration] > [System] > [Update Center] 메뉴에서 가능합니다. 사실 소나큐브 접속해서 footer에 보면 그냥 나와있기도 합니다. 버전 업그레이드는 DBA 및 인프라 팀과의 협의가 필요하므로, 차세대 프로젝트 초기 환경 구성 단계에서 SonarQube 서버 버전을 Java 21 호환 버전으로 미리 확정해 두는 것이 가장 이상적입니다.


결론: 에러 로그는 거짓말을 하지 않는다

‘Unsupported class file major version 65’ 에러는 텍스트만 보면 무시무시해 보이지만, 본질은 단순한 ‘스캐너와 컴파일러 간의 버전 불일치’입니다. Sonar Maven 플러그인을 최신으로 업데이트하고, ADO 빌드 에이전트의 JDK 버전을 일치시키며, SonarQube 서버 자체의 호환성까지 확인하는 것만으로 파이프라인에 다시 초록불(Success)을 켤 수 있습니다.

Java 21은 압도적인 퍼포먼스를 자랑하지만, 그만큼 주변 생태계(빌드 도구, 정적 분석 도구, CI/CD 환경)의 완벽한 서포트가 뒷받침되어야 합니다. 특히 금융권 차세대 프로젝트처럼 엄격한 품질 게이트가 적용된 환경에서는, Java 버전 업그레이드 전 모든 연관 도구의 호환성 매트릭스를 사전에 검토하는 것이 예상치 못한 파이프라인 중단을 막는 가장 확실한 방법입니다. 마이그레이션을 진행하며 겪는 이 작은 에러들이 결국 인프라를 더 단단하게 만드는 밑거름이 될 것입니다. 자세한 내용은 SonarQube 공식 요구사항 문서에서 확인할 수 있습니다.