자바 프로그램은 컴퓨터가 바로 읽는 말로 번역되지 않는다. 먼저 어느 컴퓨터의 말도 아닌 중간 말로 한 번 번역해 두고, 실행할 때 각 컴퓨터에 깔린 통역 프로그램이 그 중간 말을 자기 컴퓨터의 말로 옮겨 가며 돌린다. 그래서 한 번 번역한 파일을 윈도우에서도 맥에서도 그대로 쓸 수 있다. 대신 그 통역 프로그램이 깔려 있지 않은 컴퓨터에서는 돌릴 수 없다.
예시로 보기
C로 짠 프로그램을 윈도우에서 번역(컴파일)하면 윈도우용 실행 파일이 나온다. 이 파일은 맥에서 돌지 않는다. 맥에서 쓰려면 맥에서 다시 번역해야 한다. 운영체제가 셋이면 번역도 세 번이다.
자바는 번역을 두 번으로 나눠 이 문제를 피한다. 아래 파일 하나로 따라가 본다.
package ch01.introduction;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, Java");
}
}
사람이 쓴 이 글을 소스 파일(Hello.java)이라 부른다. 터미널에서 두 명령을 차례로 친다.
javac -d bin src/ch01/introduction/Hello.java
java -cp bin ch01.introduction.Hello
- 첫 줄의
javac가 소스 파일을 읽어bin/ch01/introduction/Hello.class를 만든다. 이.class파일에 든 중간 말을 바이트코드라 부른다. - 둘째 줄의
java가 통역 프로그램을 띄운다. 이 통역 프로그램이 자바 가상 머신(JVM)이다. JVM이Hello.class를 읽어Hello, Java를 찍는다.
Hello.class를 사람이 읽을 수 있게 풀어 보면 main 안은 명령 네 줄이다1.
0: getstatic // System.out을 가져온다
3: ldc // 문자열 "Hello, Java"를 가져온다
5: invokevirtual // println을 부른다
8: return
윈도우의 명령도 맥의 명령도 아니다. JVM만 알아듣는 명령이다.
Hello, Java, 실행 중인 클래스의 바이트코드가 CA FE BA BE 네 바이트로 시작하는 것 — 01_jvm-bytecode_impl.java. 두 명령의 결과(bin/ch01/introduction/Hello.class 생성)와 바이트코드 네 줄은 JDK 21에서 따로 실행해 확인했다1.소스에서 실행까지
flowchart TD
A["소스 파일 Hello.java"] -->|"javac 명령"| B["바이트코드 파일 Hello.class"]
B -->|"java 명령"| C["윈도우용 JVM"]
B -->|"java 명령"| D["맥용 JVM"]
C --> E["윈도우의 기계어로 옮겨 실행"]
D --> F["맥의 기계어로 옮겨 실행"]
바이트코드 파일은 하나다. 갈라지는 것은 JVM이다. 운영체제마다 그 운영체제용 JVM이 따로 있고, 각 JVM이 같은 바이트코드를 자기 운영체제가 알아듣는 기계어로 옮긴다2. 자바의 구호 “한 번 쓰면 어디서나 돈다(Write once, run anywhere)”가 이 뜻이다.
번역하는 두 가지 방식. 프로그램을 기계어로 바꾸는 방식은 원래 둘이다.
| 방식 | 하는 일 | 자바에서 |
|---|---|---|
| 컴파일러 | 소스 전체를 미리 한꺼번에 번역해 둔다. 틀린 곳이 있으면 오류 메시지를 낸다 | javac가 소스를 바이트코드로 |
| 인터프리터 | 실행하면서 명령을 하나씩 가져와 그때그때 번역한다 | JVM이 바이트코드를 기계어로 |
자바는 둘을 이어 붙였다. 앞 단계에서 오류를 미리 잡고, 뒤 단계에서 컴퓨터마다 다른 부분을 맞춘다.
JVM 안에서 일어나는 일. java 명령을 치면 JVM은 네 단계를 거친다.
- 클래스 로더(Class Loader)가
.class파일을 찾아 메모리에 올린다. - 바이트코드 검증기(Bytecode Verifier)가 올린 바이트코드가 규칙에 맞는지 확인한다.
- 실행 시스템(Java Runtime System)이 바이트코드를 실행한다.
- 그 과정에서 운영체제(Native OS)에 일을 맡긴다. 화면에 글자를 찍는 것도 결국 운영체제가 한다.
JDK, JRE, JVM
셋은 큰 것이 작은 것을 품는다.
+------------------------------------------+
| JDK 개발 도구 묶음 (javac, 디버거 등) |
| +------------------------------------+ |
| | JRE 실행에 필요한 것 (자바 API) | |
| | +------------------------------+ | |
| | | JVM 바이트코드를 돌리는 통역 | | |
| | +------------------------------+ | |
| +------------------------------------+ |
+------------------------------------------+
- JVM: 바이트코드를 실행한다.
- JRE(Java Runtime Environment): JVM에 실행할 때 쓰는 기본 라이브러리를 더한 것이다. 남이 만든 프로그램을 돌리기만 할 때 필요하다2.
- JDK(Java Development Kit): JRE에
javac같은 개발 도구를 더한 것이다. 프로그램을 만들려면 이것이 필요하다. 무료로 공개된 JDK로 OpenJDK가 있다.
패키지와 폴더
패키지는 관련된 클래스를 한 이름 아래 모으는 상자다. 이름이 겹치는 것을 막고 코드를 정리하려고 쓴다. 패키지 이름의 점은 폴더의 칸막이와 같다.
| 무엇 | 예 |
|---|---|
| 패키지 이름 | ch01.introduction |
| 소스 폴더 | src/ch01/introduction |
| 바이트코드 폴더 | bin/ch01/introduction |
javac -d bin의 -d는 바이트코드를 둘 폴더다. java -cp bin의 -cp는 JVM이 .class를 찾기 시작할 폴더다. 실행할 때는 파일 경로가 아니라 패키지까지 붙인 클래스 이름을 준다. java -cp bin Hello라고만 치면 JVM은 bin/Hello.class를 찾다가 Could not find or load main class Hello라며 멈춘다1. 자세한 규칙은 패키지에서 다룬다.
활용
- 자바는 1995년 선 마이크로시스템즈가 내놓았고, 2010년 오라클이 인수해 JDK를 배포한다. 안드로이드 앱, 데스크톱 프로그램, 웹사이트를 만드는 데 쓰인다.
- 자바의 특징으로 흔히 넷을 꼽는다. 어느 운영체제에서나 돈다. 객체 지향 프로그래밍에 맞춰져 있다. 안 쓰는 메모리를 알아서 치운다(가비지 컬렉션). 무료 공개 라이브러리가 많다.
- 흔한 실수:
java Hello.class처럼 확장자를 붙여 실행한다.java명령에는 클래스 이름을 준다. - 흔한 실수: 소스를 고친 뒤
javac를 다시 돌리지 않는다. JVM이 읽는 것은.class라서 옛 프로그램이 그대로 돈다.
연결
- 다음: 변수와 기본 타입
- JVM이 실행 중에 메모리를 나눠 쓰는 방식: 참조 타입과 메모리 영역
- 패키지 선언과 import: 패키지
- JVM도 운영체제 위에서 도는 프로그램 하나다: 프로세스
과목별 관점
컴파일러 (3-2학기). 자바처럼 컴파일과 인터프리트를 이어 붙인 방식을 하이브리드 컴파일러(hybrid compiler)라 부른다. Java와 파이썬이 그 예다. 컴파일러 과목에서 보면 두 가지가 더 보인다.
- 두 방식을 가르는 세 물음. 무엇을 옮기나(원시 프로그램 전체 / 문장 하나), 언제 옮기나(프로그램이 돌기 전에 한 번 / 그 문장을 실행할 때마다), 무엇이 나오나(목적 프로그램 / 그 문장의 목적 코드). 위 표의 ‘하는 일’ 칸을 셋으로 나눠 본 것이다.
- 두 토막이 각각 맡은 것. 컴파일러 쪽 토막은 높은 효율(짧은 실행 시간)을 맡고, 인터프리터 쪽 토막은 여러 기계에서 그대로 도는 것(이식성)을 맡는다.
javac가 바이트코드를 미리 만들어 두는 것은 오류를 미리 잡는 데 더해 실행을 빠르게 하려는 것이기도 하다. 글자를 낱말로 끊고 문법을 따지는 일을 실행할 때마다 되풀이하지 않아도 되기 때문이다3.
두 방식의 장단점은 컴파일과 인터프리트 비교에, javac 같은 컴파일러 안에서 일어나는 여섯 단계는 컴파일러의 구조에 있다.
확인 문제
C1 Hello.java를 써서 화면에 글자가 찍히기까지, 어떤 명령이 어떤 파일을 만들고 무엇이 그 파일을 읽는지 순서대로 쓰라.
답: ① javac가 소스 파일 Hello.java를 바이트코드 파일 Hello.class로 번역한다. ② java 명령이 JVM을 띄운다. ③ JVM이 Hello.class를 읽어(클래스 로더 → 검증기 → 실행 시스템) 그 컴퓨터의 기계어로 옮겨 실행한다.
이유: 흔한 오답은 “java가 .java 파일을 읽는다”이다. JVM이 읽는 것은 .class다.
C2 윈도우에서 만든 Hello.class를 맥에 복사하면 다시 번역하지 않고 돌릴 수 있다. C의 실행 파일은 왜 안 되고 이것은 왜 되는가? 안 되는 경우는 언제인가?
답: C의 실행 파일에는 윈도우용 기계어가 들어 있어 맥이 알아듣지 못한다. .class에는 어느 운영체제의 것도 아닌 바이트코드가 들어 있고, 맥에 깔린 맥용 JVM이 그것을 맥의 기계어로 옮긴다. 맥에 JVM이 없으면 돌릴 수 없다.
이유: 운영체제마다 다른 부분을 프로그램이 아니라 JVM이 떠안는다. 그래서 JVM은 운영체제마다 따로 만들어야 한다.
C3 ① 친구가 준 .class 파일을 돌려 보기만 한다. ② 내가 .java를 써서 프로그램을 만든다. 각각 JVM, JRE, JDK 중 최소한 무엇이 있어야 하는가?
답: ①은 JRE(JVM과 기본 라이브러리). ②는 JDK. javac는 JDK에만 들어 있다.
이유: JVM만으로는 System.out.println 같은 기본 라이브러리가 없어 대부분의 프로그램이 돌지 않는다. JDK는 JRE를 품으므로 JDK가 있으면 ①도 된다.
C4 소스가 src/app/util/Calc.java에 있고 첫 줄이 package app.util;이다. 바이트코드를 out 폴더에 만들고 실행하는 두 명령을 쓰라. 바이트코드 파일은 어디에 생기는가?
답: javac -d out src/app/util/Calc.java, java -cp out app.util.Calc. 파일은 out/app/util/Calc.class에 생긴다.
이유: -d로 준 폴더 아래에 패키지 이름대로 폴더가 생긴다. 실행할 때는 패키지까지 붙인 이름을 준다.
C5 자바의 두 토막 가운데 하나씩을 없앤다고 하자. (가) 바이트코드를 만들지 않고 JVM이 .java 소스를 한 문장씩 읽어 바로 실행한다. (나) JVM 없이 javac가 곧장 윈도우의 기계어를 낸다. 각각 무엇을 잃는가?
답: (가) 실행 속도와 실행 전 오류 검사를 잃는다. 문장을 실행할 때마다 글자를 끊고 문법을 따져야 하고, 틀린 문장은 그 문장에 닿아야 드러난다. (나) 다른 운영체제에서 그대로 도는 성질을 잃는다. 맥에서 쓰려면 맥용으로 다시 번역해야 한다.
이유: 앞 토막(컴파일)은 짧은 실행 시간을, 뒤 토막(인터프리트)은 여러 기계에서 그대로 도는 것을 맡는다. 둘을 이어 붙여 두 가지를 함께 얻는다.