javac, java, JVM이 하는 일
Hello 프로그램이 화면에 출력되기까지 javac, java, JVM이 각각 무엇을 받아 무엇을 만드는지 따라가고, 클래스 경로와 버전이 실행에 미치는 영향을 이해합니다.
앞 글에서 명령 두 개로 Hello 프로그램을 실행했습니다. javac는 파일을 하나 만들었고, java는 그 파일을 실행했습니다. 명령을 외워서 입력하는 것만으로도 프로그램은 돌아가지만, 각 명령이 무엇을 받아 무엇을 만드는지 모르면 문제가 생겼을 때 어디를 봐야 할지 알 수 없습니다. 이 글에서는 이미 성공한 Hello 프로그램을 따라가며 javac, java, JVM의 역할을 나눠 보고, 클래스 경로와 버전이 왜 중요한지 살펴봅니다.
Hello.java가 화면에 출력되기까지
앞 글에서 일어난 일을 순서대로 늘어놓으면 네 단계가 됩니다.
- 사람이 편집기로
src/Hello.java를 작성하고 저장합니다. javac가 소스의 문법과 이름을 검사한 뒤out/Hello.class를 만듭니다.java실행기가 JVM을 시작하고,out폴더에서Hello클래스를 찾아 JVM에 싣습니다.- JVM이
Hello의main부터 실행해 문자열을 출력합니다.
각 단계는 앞 단계가 만든 것을 받아서 다음 단계에 넘깁니다. javac는 .java 파일을 받아 .class 파일을 만들고, java는 그 .class를 실행합니다. 두 명령은 이름이 비슷하지만 받는 것과 만드는 것이 다르므로, 실패했을 때 나오는 오류도 종류가 다릅니다.
| 명령 | 받는 것 | 성공하면 | 실패하면 |
|---|---|---|---|
javac -d out src/Hello.java | .java 소스 파일의 경로 | out/Hello.class가 생기거나 새로 바뀜 | 컴파일 오류가 나고 .class는 새로 만들어지지 않음 |
java -cp out Hello | 클래스를 찾을 위치와 클래스 이름 | main이 실행됨 | 클래스를 찾지 못했다는 오류나 실행 중 오류가 남 |
그래서 오류가 나면 먼저 어느 명령에서 났는지 봅니다. javac에서 났다면 소스 코드를 고칠 차례입니다. 반면 java에서 클래스를 찾지 못했다면 소스는 멀쩡하고 실행 명령이나 파일 위치가 문제일 가능성이 큽니다. 앞 글에서 본 것처럼, 소스를 저장하는 것만으로는 .class가 바뀌지 않는다는 점도 이 흐름에서 나옵니다. 1단계와 2단계는 서로 다른 일이라서, 2단계를 다시 하지 않으면 4단계는 예전 결과를 실행합니다.
JDK 안에 들어 있는 것
첫 글에서 JDK를 개발 도구 묶음이라고 소개했습니다. 그 안을 조금 더 들여다보면 이 글에 나오는 도구들이 모두 들어 있습니다.
JDK
├─ javac : 소스를 컴파일하는 컴파일러
├─ java : JVM을 시작하는 실행기
├─ JVM : .class의 바이트코드를 실행하는 가상 머신
├─ 표준 라이브러리
│ └─ System.out.println 같은 기능
└─ javap, jar, jshell 같은 보조 도구JVM(Java Virtual Machine)은 이름 그대로 소프트웨어로 만든 가상의 컴퓨터입니다. 실제 CPU는 x64나 ARM처럼 종류마다 자기 기계어만 실행할 수 있고, 실행 파일의 형식도 운영체제마다 다릅니다. 그런데 javac가 만드는 .class 파일에는 특정 CPU의 기계어가 아니라 바이트코드(bytecode)라는, JVM이 이해하는 명령이 들어 있습니다. JVM은 이 바이트코드를 읽어서 지금 컴퓨터의 운영체제와 CPU에 맞게 실행해 줍니다. 그래서 JDK는 운영체제마다 따로 설치하지만, 그 위에서 도는 .class 파일은 같은 것을 쓸 수 있습니다.
java 명령은 이 JVM을 시작하는 실행기(launcher)입니다. 명령에 적은 옵션에 맞춰 JVM을 시작하고, 클래스 경로에서 실행할 클래스를 찾아 JVM에 실은 뒤 그 클래스의 main을 호출합니다. 그다음 main 안의 바이트코드를 실제로 실행하는 것은 JVM입니다. main 안에서 쓴 System.out.println은 직접 만든 코드가 아니라 JDK의 표준 라이브러리에 들어 있는 기능입니다. 결국 화면의 Hello, Java!는 우리가 만든 Hello.class와 JDK가 제공한 표준 라이브러리가 함께 실행된 결과입니다.
소스에서 main 실행까지, JDK 도구의 경계
javac가 class를 만들고 launcher/JVM이 main·라이브러리를 호출하는 실제 실행 과정입니다. 전체 도구 경계를 유지한 채 현재 입력·산출물만 순차 강조할 수 있습니다.
ARCHITECTURE · TOOLCHAIN BOUNDARY
소스에서 main 실행까지, JDK 도구의 경계
보존하는 소스와 다시 만드는 class를 구분하고, javac·java launcher·JVM·표준 라이브러리가 다음 단계에 무엇을 넘기는지 추적합니다.
JDK 제공 경계 javac · java launcher · JVM · 표준 라이브러리
소스 보존
src/Hello.java는 사람이 편집하고 저장하는 원본입니다.javac로 class 갱신
javac -d out이 문법과 이름을 검사해out/Hello.classJVM bytecode를 만듭니다. 저장만으로는 class가 바뀌지 않습니다.산출물 경계
out/*.class는 소스에서 다시 만들 수 있는 시점별 산출물입니다.launcher가 실행 대상을 해석
java -cp out Hello에서out은 classpath root,Hello는 확장자 없는 binary class name입니다.JVM 시작
javalauncher가 JVM을 시작하고 발견한 class의Hello.main을 호출합니다.표준 라이브러리 호출
JVM이
System.out.println을 실행해 표준 출력 기능을 사용합니다.출력 관찰
화면의
Hello, Java!는 main이 실행된 뒤 관찰되는 결과입니다.
- 입력 전달과 도구 호출
- class 생성 경로와 JVM 실행 초점
소스는 보존하고 class는 다시 만듭니다. javac의 출력은 launcher의 입력이 되고, JVM의 실행은 표준 라이브러리를 거쳐 화면 출력으로 관찰됩니다.
이 예의 조건
- main 출력은 Hello, Java!인 원 예제이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 소스에서 main 실행까지, JDK 도구의 경계 소스, class 산출물, JDK 도구, JVM 실행과 화면 출력의 경계 ARCHITECTURE · TOOLCHAIN BOUNDARY 소스에서 main 실행까지, JDK 도구의 경계 보존하는 소스와 다시 만드는 class를 구분하고, javac·java launcher·JVM·표준 라이브러리가 다음 단계에 무엇을 넘기는지 추적합니다. JDK 제공 경계 javac · java launcher · JVM · 표준 라이브러리 소스 보존 src/Hello.java 는 사람이 편집하고 저장하는 원본입니다. javac로 class 갱신 javac -d out 이 문법과 이름을 검사해 out/Hello.class JVM bytecode를 만듭니다. 저장만으로는 class가 바뀌지 않습니다. 산출물 경계 out/*.class 는 소스에서 다시 만들 수 있는 시점별 산출물입니다. launcher가 실행 대상을 해석 java -cp out Hello 에서 out 은 classpath root, Hello 는 확장자 없는 binary class name입니다. JVM 시작 java launcher가 JVM을 시작하고 발견한 class의 Hello.main 을 호출합니다. 표준 라이브러리 호출 JVM이 System.out.println 을 실행해 표준 출력 기능을 사용합니다. 출력 관찰 화면의 Hello, Java! 는 main이 실행된 뒤 관찰되는 결과입니다. 입력 전달과 도구 호출 class 생성 경로와 JVM 실행 초점 소스는 보존하고 class는 다시 만듭니다. javac의 출력은 launcher의 입력이 되고, JVM의 실행은 표준 라이브러리를 거쳐 화면 출력으로 관찰됩니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 컴파일 입력 javac가 src/Hello.java를 검사합니다. |
|
|
| 실행 준비 launcher가 out에서 Hello를 찾고 JVM을 시작합니다. |
|
|
| main과 출력 JVM의 main이 표준 라이브러리 println을 호출합니다. |
|
|
소스에서 main 실행까지, JDK 도구의 경계
javac가 class를 만들고 launcher/JVM이 main·라이브러리를 호출하는 실제 실행 과정입니다. 전체 도구 경계를 유지한 채 현재 입력·산출물만 순차 강조할 수 있습니다.
ARCHITECTURE · TOOLCHAIN BOUNDARY
소스에서 main 실행까지, JDK 도구의 경계
보존하는 소스와 다시 만드는 class를 구분하고, javac·java launcher·JVM·표준 라이브러리가 다음 단계에 무엇을 넘기는지 추적합니다.
JDK 제공 경계 javac · java launcher · JVM · 표준 라이브러리
소스 보존
src/Hello.java는 사람이 편집하고 저장하는 원본입니다.javac로 class 갱신
javac -d out이 문법과 이름을 검사해out/Hello.classJVM bytecode를 만듭니다. 저장만으로는 class가 바뀌지 않습니다.산출물 경계
out/*.class는 소스에서 다시 만들 수 있는 시점별 산출물입니다.launcher가 실행 대상을 해석
java -cp out Hello에서out은 classpath root,Hello는 확장자 없는 binary class name입니다.JVM 시작
javalauncher가 JVM을 시작하고 발견한 class의Hello.main을 호출합니다.표준 라이브러리 호출
JVM이
System.out.println을 실행해 표준 출력 기능을 사용합니다.출력 관찰
화면의
Hello, Java!는 main이 실행된 뒤 관찰되는 결과입니다.
- 입력 전달과 도구 호출
- class 생성 경로와 JVM 실행 초점
소스는 보존하고 class는 다시 만듭니다. javac의 출력은 launcher의 입력이 되고, JVM의 실행은 표준 라이브러리를 거쳐 화면 출력으로 관찰됩니다.
이 예의 조건
- main 출력은 Hello, Java!인 원 예제이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 소스에서 main 실행까지, JDK 도구의 경계 소스, class 산출물, JDK 도구, JVM 실행과 화면 출력의 경계 ARCHITECTURE · TOOLCHAIN BOUNDARY 소스에서 main 실행까지, JDK 도구의 경계 보존하는 소스와 다시 만드는 class를 구분하고, javac·java launcher·JVM·표준 라이브러리가 다음 단계에 무엇을 넘기는지 추적합니다. JDK 제공 경계 javac · java launcher · JVM · 표준 라이브러리 소스 보존 src/Hello.java 는 사람이 편집하고 저장하는 원본입니다. javac로 class 갱신 javac -d out 이 문법과 이름을 검사해 out/Hello.class JVM bytecode를 만듭니다. 저장만으로는 class가 바뀌지 않습니다. 산출물 경계 out/*.class 는 소스에서 다시 만들 수 있는 시점별 산출물입니다. launcher가 실행 대상을 해석 java -cp out Hello 에서 out 은 classpath root, Hello 는 확장자 없는 binary class name입니다. JVM 시작 java launcher가 JVM을 시작하고 발견한 class의 Hello.main 을 호출합니다. 표준 라이브러리 호출 JVM이 System.out.println 을 실행해 표준 출력 기능을 사용합니다. 출력 관찰 화면의 Hello, Java! 는 main이 실행된 뒤 관찰되는 결과입니다. 입력 전달과 도구 호출 class 생성 경로와 JVM 실행 초점 소스는 보존하고 class는 다시 만듭니다. javac의 출력은 launcher의 입력이 되고, JVM의 실행은 표준 라이브러리를 거쳐 화면 출력으로 관찰됩니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 컴파일 입력 javac가 src/Hello.java를 검사합니다. |
|
|
| 실행 준비 launcher가 out에서 Hello를 찾고 JVM을 시작합니다. |
|
|
| main과 출력 JVM의 main이 표준 라이브러리 println을 호출합니다. |
|
|
JDK는 여러 회사가 각자 배포하지만, 같은 기능 버전이라면 모두 Java SE 규격을 구현한 것입니다. 그래서 Temurin에서 컴파일한 .class를 Oracle JDK나 Corretto에서 실행해도 언어 문법과 표준 라이브러리의 동작은 같습니다. 배포판마다 다른 점은 업데이트 지원 기간, 라이선스, 함께 넣어 주는 부가 도구 같은 것입니다.
더 깊이: .class 파일 안 들여다보기
JDK에 들어 있는 javap로 .class 파일의 내용을 사람이 읽을 수 있는 형태로 볼 수 있습니다. java-board에서 javap -c out/Hello.class를 실행하면 main이 바이트코드 명령 네 개로 이루어져 있다는 것을 볼 수 있습니다. 아래는 JDK 21에서 컴파일한 결과이며, 명령 옆의 #7 같은 번호는 JDK 버전에 따라 달라질 수 있습니다.
Compiled from "Hello.java"
public class Hello {
public Hello();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Hello, Java!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}getstatic은 System.out을 가져오고, ldc는 문자열 "Hello, Java!"를 준비하고, invokevirtual은 println을 호출합니다. 소스의 한 줄이 이렇게 몇 개의 바이트코드로 바뀐 것입니다. 소스에 쓰지 않은 public Hello()도 보이는데, 클래스에 생성자를 쓰지 않으면 컴파일러가 기본 생성자를 자동으로 넣기 때문입니다. 생성자는 5장에서 배웁니다.
대부분의 JDK에 들어 있는 HotSpot JVM은 이 바이트코드를 처음에는 명령 하나씩 해석하며 실행합니다. 그러다 자주 실행되는 부분을 찾아내면 그 부분을 실행 도중에 CPU의 기계어로 컴파일해 두고 빠르게 실행하는데, 이것을 JIT(Just-In-Time) 컴파일이라고 합니다. 같은 컴파일이라는 말을 쓰지만, javac의 컴파일은 실행 전에 소스를 바이트코드로 바꾸는 일이고 JIT 컴파일은 실행하는 동안 바이트코드를 기계어로 바꾸는 일입니다.
클래스 경로로 클래스 찾기
실행 명령을 다시 보겠습니다. 맨 앞의 java는 JVM을 시작하는 명령이고 맨 끝의 Hello는 실행할 클래스 이름입니다. 이 절에서 살펴볼 것은 그 사이에 있는 -cp out입니다.
java -cp out Hello-cp는 클래스 경로(class path)를 지정하는 옵션이며, -classpath나 --class-path로 길게 써도 같은 뜻입니다. 클래스 경로는 JVM이 우리가 만든 클래스나 외부 라이브러리의 클래스 파일을 찾을 때 살펴보는 위치의 목록입니다. 첫 글에서 본 PATH가 터미널이 명령을 찾는 폴더 목록이었다면, 클래스 경로는 JVM이 클래스를 찾는 목록이라고 보면 됩니다. 이 명령에서는 목록에 out 폴더 하나만 들어 있으므로, JVM은 Hello라는 이름을 받으면 out 안의 Hello.class를 찾습니다. System이나 String 같은 JDK의 표준 클래스는 클래스 경로와 상관없이 언제나 찾을 수 있습니다.
-cp를 빼거나 클래스 이름을 잘못 쓰면 어떻게 되는지 직접 실행해 보겠습니다. 모두 java-board 폴더에서 실행한 결과입니다. 먼저 -cp out을 빼고 실행합니다.
java HelloError: Could not find or load main class Hello
Caused by: java.lang.ClassNotFoundException: HelloHello.class는 분명히 out 안에 있는데도 찾지 못했습니다. 클래스 경로를 지정하지 않으면 JVM은 우리가 만든 클래스를 현재 폴더, 즉 java-board에서만 찾기 때문입니다. 정확히는 CLASSPATH라는 환경 변수가 설정돼 있으면 그 값을 쓰고, 없으면 현재 폴더를 씁니다. java-board에는 src와 out 폴더만 있고 Hello.class는 없으니 실패합니다. Caused by 줄의 ClassNotFoundException은 이름 그대로 클래스를 찾지 못했다는 뜻입니다.
다음으로 클래스 이름 뒤에 .class를 붙여 봅니다.
java -cp out Hello.classError: Could not find or load main class Hello.class
Caused by: java.lang.ClassNotFoundException: Hello.class이번에는 오류 메시지 안의 클래스 이름이 Hello.class로 바뀌었습니다. java가 Hello.class 전체를 클래스 이름으로 받아들였다는 뜻입니다. java 명령의 마지막 자리에는 파일 이름이 아니라 클래스 이름을 쓰기 때문에 확장자를 붙이면 안 됩니다. 점(.)은 클래스 이름 안에서 패키지를 구분하는 기호로 쓰이므로, JVM은 이 이름을 Hello라는 패키지 안의 class라는 클래스로 해석해 찾으려 합니다.
마지막으로 java-board가 아니라 src 폴더 안에서 원래 명령을 실행하면, 명령이 맞더라도 첫 번째와 같은 오류가 납니다. 클래스 경로의 out도 현재 폴더를 기준으로 한 상대 경로라서, src 안에서는 src/out이라는 없는 폴더를 찾기 때문입니다. 이때는 cd ..로 java-board에 돌아오거나, 한 단계 위를 뜻하는 ..을 써서 java -cp ../out Hello로 실행합니다.
정리하면 클래스를 찾지 못했다는 오류가 날 때 확인할 것은 세 가지입니다. 현재 폴더가 어디인지, -cp가 가리키는 폴더에 .class 파일이 실제로 있는지, 클래스 이름을 확장자 없이 정확히 썼는지입니다. 세 가지 모두 소스 코드와는 상관이 없으므로 .java 파일을 고칠 필요가 없습니다. 지금은 src에 소스를 두고 out에 컴파일 결과를 두는 이 한 가지 구조만 사용합니다. 클래스를 패키지로 묶기 시작하면 폴더 구조와 클래스 이름이 함께 바뀌는데, 이것은 5장의 패키지와 import에서 다룹니다.
out 폴더를 비우고 다시 만들기
out 폴더의 내용은 언제든 src로부터 다시 만들 수 있습니다. 거꾸로 말하면, 컴파일 결과가 소스와 맞지 않는다고 의심될 때는 out을 비우고 처음부터 다시 컴파일하면 됩니다. 이런 일은 생각보다 자주 생깁니다. 예를 들어 연습용으로 src/Temp.java를 만들어 컴파일한 뒤 소스 파일을 지웠다고 해 봅시다. out/Temp.class는 그대로 남아 있어서 java -cp out Temp가 여전히 실행됩니다. 소스는 없는데 실행되는 클래스가 남아 있으면 지금 무엇이 실행되는지 헷갈리기 쉽습니다.
다음 명령은 out 폴더를 지운 뒤 빈 폴더로 다시 만들고, Hello.java를 다시 컴파일해 결과를 확인합니다. Windows PowerShell에서는 다음과 같이 실행합니다.
Remove-Item -Recurse -Force out -ErrorAction SilentlyContinue
New-Item -ItemType Directory out | Out-Null
javac -d out src/Hello.java
Get-ChildItem out -NameRemove-Item -Recurse -Force는 폴더를 안의 내용과 함께 지우고, -ErrorAction SilentlyContinue는 지울 폴더가 없어도 오류를 보여 주지 않게 합니다. macOS와 Linux에서는 같은 일을 다음 명령으로 합니다.
rm -rf out
mkdir out
javac -d out src/Hello.java
ls outrm -rf out은 out 폴더를 내용과 함께 묻지 않고 지웁니다. 두 방법 모두 지운 파일이 휴지통을 거치지 않고 바로 사라지므로, out이라는 이름을 정확히 입력했는지 확인한 뒤 실행하세요. 마지막 명령의 출력은 두 방법 모두 다음과 같습니다.
Hello.classout에는 방금 컴파일한 Hello.class만 남고, 소스가 사라진 Temp.class 같은 파일은 없어졌습니다. 이 명령들은 src 폴더를 건드리지 않으므로, 몇 번을 실행해도 원본이 사라지지 않습니다. 원본은 지켜야 하고 결과물은 언제든 다시 만들 수 있게 한다는 원칙이 첫 글에서 폴더를 둘로 나눈 이유입니다.
소스 파일이 몇 개일 때는 이렇게 직접 명령을 입력해도 괜찮습니다. 하지만 파일이 수십 개로 늘고 외부 라이브러리를 쓰기 시작하면 어떤 파일을 어떤 순서로 컴파일하고 어떤 라이브러리를 클래스 경로에 넣을지 관리하기 어려워집니다. 실무에서는 Gradle이나 Maven 같은 빌드 도구가 이 일을 대신합니다. 이 교재에서는 각 단계에서 무슨 일이 일어나는지 보이도록 당분간 명령을 직접 입력합니다.
같은 .class를 다른 운영체제에서 실행할 수 있는 이유
Windows에서 컴파일한 Hello.class를 macOS나 Linux 컴퓨터로 복사해도, 그곳에 설치된 JVM으로 그대로 실행할 수 있습니다. .class에 든 바이트코드가 특정 운영체제나 CPU의 명령이 아니고, 각 운영체제용 JVM이 그 바이트코드를 자기 환경에 맞게 실행해 주기 때문입니다. Java의 이식성(portability)은 바로 이것을 말합니다. 운영체제마다 다시 컴파일할 필요 없이, 한 번 컴파일한 결과를 여러 환경에서 쓸 수 있습니다.
다만 조건이 하나 있습니다. 실행하는 JVM이 그 .class 파일의 버전을 알아야 합니다. javac는 .class 파일에 어느 버전의 형식으로 만들었는지를 클래스 파일 버전이라는 번호로 적어 둡니다. JDK 25가 만든 파일은 69, JDK 21이 만든 파일은 65입니다. JVM은 자기 버전 이하의 형식만 실행할 수 있으므로, JDK 25에서 옵션 없이 컴파일한 .class를 Java 21에서 실행하면 다음과 같은 오류가 납니다.
Error: LinkageError occurred while loading main class Hello
java.lang.UnsupportedClassVersionError: Hello has been compiled by a more recent version of the Java Runtime (class file version 69.0), this version of the Java Runtime only recognizes class file versions up to 65.0메시지는 Hello가 더 새로운 Java로 컴파일됐고, 이 JVM은 65.0까지만 알아본다고 말하고 있습니다. 운영체제가 같든 다르든 이 조건은 똑같습니다. 첫 글에서 java와 javac가 둘 다 25를 출력하게 맞춘 이유가 여기에 있습니다. 반대 방향은 파일 형식 면에서 문제가 없어서, 예전 JDK로 컴파일한 .class는 더 새로운 JVM에서도 실행됩니다. 다만 새 버전에서 삭제된 표준 기능을 쓴 프로그램이라면 실행 도중에 오류가 날 수 있습니다.
이식성이 운영체제의 차이를 모두 없애 주는 것은 아닙니다. 바이트코드는 같아도 프로그램이 다루는 바깥 환경은 운영체제마다 다르기 때문입니다.
- 파일 경로는 Windows에서
C:\Users\...처럼 역슬래시로 구분하고, macOS와 Linux에서는/Users/...나/home/...처럼 슬래시로 구분합니다. - 텍스트 파일을 읽고 쓸 때는 어떤 문자 인코딩을 쓸지 정해야 합니다. 앞 글에서 한글 소스를 UTF-8로 저장한 것도 같은 문제입니다.
- 네트워크 설정이나 파일 권한은 프로그램이 실행되는 환경의 영향을 받습니다.
이런 차이를 다루려면 아직 배우지 않은 파일·문자 API가 필요합니다. 문자와 바이트는 23장 문자 인코딩과 스트림에서, 파일 경로는 24장 Path·Files와 파일 영속화에서, JVM이 클래스를 읽어 들이는 과정은 38장 JVM 진단에서 자세히 다룹니다.
더 깊이: 예전 Java에서 실행할 클래스 만들기
새 JDK로 컴파일하면서 예전 JVM에서도 실행되는 .class를 만들어야 할 때는 javac의 --release 옵션을 씁니다. 예를 들어 JDK 25에서 javac --release 21 -d out src/Hello.java로 컴파일하면 클래스 파일 버전이 65인 .class가 만들어져 Java 21에서도 실행됩니다. 이 옵션은 파일 형식만 바꾸는 것이 아니라 Java 21의 언어 문법과 표준 API 목록을 기준으로 소스를 검사합니다. 그래서 Java 22 이후에 추가된 기능을 실수로 쓰면 컴파일 단계에서 오류가 납니다.
버전 번호 규칙은 Java 가상 머신 명세에 정해져 있습니다. JVM 명세 1.2절에는 Java SE 버전마다 지원하는 클래스 파일 버전(major version)이 표로 정리돼 있습니다. .class 파일의 버전은 javap -v out/Hello.class의 출력에서 major version 줄로 확인할 수 있습니다. 실무에서 빌드 JDK와 실행 JDK를 맞추는 방법은 36장의 Java 25 마이그레이션에서 다룹니다.
정리
javac는.java소스를 검사해 바이트코드가 든.class파일을 만들고,java는 JVM을 시작해 그.class를 실행합니다.- JVM은 바이트코드를 지금 컴퓨터의 운영체제와 CPU에 맞게 실행합니다. 그래서 같은
.class를 여러 운영체제에서 쓸 수 있습니다. -cp out은 클래스를 찾을 위치를 지정합니다. 지정하지 않으면 보통 현재 폴더에서 찾으며, 클래스 이름에는.class를 붙이지 않습니다.out은 언제든 지우고 다시 만들 수 있습니다. 컴파일 결과가 의심스러우면 비우고 다시 컴파일합니다.- JVM은 자기 버전보다 새로운
.class를 실행하지 못합니다. 이 교재에서java와javac를 모두 25로 맞추는 이유입니다.
연습 문제
- 다음 일이 소스 작성, 컴파일, 실행 가운데 어느 단계에서 일어나는지 분류하세요.
- (가)
Hello.java의 문자열을 고친다. - (나) 세미콜론이 빠졌다는 오류가 나온다.
- (다)
out에Hello.class가 생긴다. - (라) JVM이
main을 실행하기 시작한다. - (마) 화면에
Hello, Java!가 나타난다.
- (가)
java-board/src폴더에서java -cp out Hello를 실행했더니ClassNotFoundException: Hello가 났습니다. 소스 코드를 고쳐야 할까요? 원인과 해결 방법 두 가지를 적으세요.- 친구가 JDK 25로 컴파일한
Hello.class를 보내 줬는데, Java 21만 설치된 컴퓨터에서 실행하자UnsupportedClassVersionError가 났습니다. 왜 그런지 설명하고, 해결 방법을 두 가지 적으세요.
힌트
- 각 일이 소스 파일,
.class파일, 실행 중인 JVM 가운데 무엇과 관련 있는지 보세요. - 클래스 경로의
out은 무엇을 기준으로 한 경로일까요? - 오류 메시지에 나온 두 숫자 69와 65가 각각 무엇을 뜻하는지 떠올려 보세요.
해설
- (가)는 소스 작성 단계입니다. (나)와 (다)는 컴파일 단계인데,
javac가 소스를 검사하다가 문법 오류를 찾으면 (나)가 일어나고, 검사를 통과하면 (다)처럼.class를 만듭니다. (라)와 (마)는 실행 단계로,java가 시작한 JVM이Hello.class를 읽어main을 실행하고 그 안의println이 출력합니다. 같은 컴파일 단계라도 (나)가 일어나면 (다)는 일어나지 않는다는 점도 기억해 두세요. - 소스 코드는 고칠 필요가 없습니다. 클래스 경로의
out은 현재 폴더를 기준으로 한 상대 경로라서,src안에서 실행하면 JVM은 없는 폴더인src/out에서 클래스를 찾습니다.cd ..로java-board에 돌아와 같은 명령을 실행하거나, 현재 폴더에서java -cp ../out Hello처럼out의 위치를 바르게 알려 주면 됩니다. - 69는
Hello.class의 클래스 파일 버전, 즉 JDK 25가 만든 형식이고, 65는 Java 21의 JVM이 알아보는 가장 높은 버전입니다. JVM은 자기 버전보다 새로운 형식을 실행할 수 없어서 오류가 났습니다. 해결 방법은 실행하는 컴퓨터에 JDK 25를 설치하는 것과, 친구에게javac --release 21로 다시 컴파일해 달라고 하는 것입니다. 소스가 있다면 Java 21 컴퓨터에서 직접 다시 컴파일해도 됩니다.
다음 글
다음 글 처음 만나는 Java 오류에서는 컴파일 오류, 클래스를 찾지 못하는 오류, 실행 중 오류를 하나씩 재현하고, 오류 메시지에서 고칠 위치를 찾는 방법을 익힙니다.