javac
고친 사람 github-actions[bot]
javac 는 사람이 쓴 자바 소스를 자바 가상 머신이 실행할 클래스 파일로 옮기는 명령입니다. 옮기는 동안 문법과 타입을 함께 검사해서 어긋난 곳을 오류로 알려 줍니다. 옮긴 결과는 특정 기계의 기계어가 아니라서 운영체제가 달라도 같은 파일을 그대로 띄울 수 있습니다.
쉽고 빠른 이해
javac 는 자바 소스를 실행할 수 있는 파일로 옮기는 명령입니다. javac Hello.java 라고 치면
같은 폴더에 Hello.class 가 생깁니다. 프로그램을 띄울 때 읽히는 것은 이 파일입니다.
자바 가상 머신은 사람이 읽는 소스를 못 읽습니다. 옮겨 두지 않으면 프로그램을 띄울 수가 없습니다. 옮기는 김에 타입이 안 맞는 곳도 미리 걸러 줍니다.
돌아가는 방식은 이렇습니다.
- 소스를 읽어 문장의 구조를 잡습니다
- 이름을 찾아 잇고 타입이 맞는지 봅니다
- 맞으면 클래스마다 파일을 하나씩 써 냅니다
대가가 있습니다. 소스를 고칠 때마다 다시 돌려야 합니다. 걸러 주는 것도 문법과 타입까지라서, 값이 비어 있어 터지는 오류는 실행할 때 드러납니다.
상세
javac 는 자바 소스 파일을 받아 클래스 파일을 만들어 내는 명령입니다. javac Hello.java
라고 치면 Hello.class 라는 파일이 생깁니다. 소스를 클래스 파일로 옮기는 이 일을
컴파일이라 부릅니다.
컴파일하는 프로그램이 컴파일러입니다. 그래서 javac 를 자바 컴파일러라고도 부릅니다.
클래스 파일에 든 것은 컴퓨터가 바로 실행하는 기계어가 아니라 바이트코드입니다. 바이트코드는 JVM(Java Virtual Machine, 자바 가상 머신)이 읽어 대신 실행해 주는 명령입니다. 자바 가상 머신은 운영체제마다 따로 나와 있어서, 같은 클래스 파일을 노트북에서도 서버에서도 그대로 띄울 수 있습니다.
자바 가상 머신은 사람이 읽고 쓰는 소스 파일을 그대로는 못 읽습니다. 그래서 컴파일해 두지 않으면 프로그램을 띄울 수가 없습니다. javac 가 맡는 것이 이 일입니다.
javac 는 JDK(Java Development Kit, 자바 개발 도구 모음)에 들어 있습니다. 프로그램을 띄우기만 할 때는 JRE(Java Runtime Environment, 자바 실행 환경)처럼 자바 가상 머신을 담은 묶음이면 됩니다. JDK 는 그 JRE 를 품고 컴파일에 쓰는 명령까지 함께 담은 묶음입니다.
flowchart TD
subgraph JDK["JDK · 만들 때 쓰는 묶음"]
JAVAC["javac"]
subgraph JRE["JRE · 띄우기만 할 때 쓰는 묶음"]
JVM["JVM"]
end
end
만들지는 않고 띄우기만 하는 컴퓨터에는 JDK 까지 두지 않아도 됩니다.
javac 는 옮기기만 하지 않습니다. 옮기기 전에 문법이 맞는지, 부르는 메서드가 있는지, 타입이 맞는지를 검사합니다. 검사에서 걸린 것은 프로그램을 띄우기 전에 오류로 나옵니다.
소스 파일 하나가 클래스 파일 여럿이 되는 까닭
클래스 파일은 소스 파일이 아니라 클래스를 기준으로 나옵니다. 소스 파일 하나에 클래스를 둘 적어 두면 클래스 파일도 둘 나옵니다. 클래스 안에 들여 적은 중첩 클래스도, 이름을 안 붙이고 쓰는 곳에서 바로 만든 익명 클래스도 각자 파일을 하나씩 받습니다.
flowchart TD
A["Order.java"] --> B["javac"]
B --> C["Order.class"]
B --> D["Order$Item.class"]
B --> E["Order$1.class"]
$ 가 붙은 이름이 중첩 클래스와 익명 클래스의 파일입니다. 익명 클래스는 이름이 없어서
javac 가 번호를 붙여 이름을 지어 줍니다.
클래스 파일에 안 남는 것
소스에 적은 것이 전부 클래스 파일까지 가지는 않습니다. 주석은 남지 않습니다.
제네릭(List<String> 처럼 담을 것의 타입을 꺾쇠에 적어 두는 문법)에 적은 타입 인자도
검사가 끝나면 지워집니다. 지워진다는 것은 실행 중에 그 타입을 되물어도 답이 없다는 뜻입니다.
컴파일이 지나가는 네 단계
javac 안에서 일은 단계로 나뉘어 돕니다. 앞 단계가 끝나야 뒤 단계가 시작합니다.
flowchart TD
A["소스를 읽어 구조로 만든다 · 괄호와 세미콜론"] --> B["애너테이션을 읽고 소스를 더 만든다"]
B -->|"새로 만들어진 소스"| A
B --> C["이름을 잇고 타입을 검사한다 · 없는 메서드와 안 맞는 타입"]
C --> D["바이트코드를 써 낸다 · 상수와 메서드 목록"]
C --> E["오류가 있으면 여기서 멈춘다"]
첫 단계는 글자를 읽어 구조로 바꾸는 일입니다. 어디까지가 한 문장이고 무엇이 클래스이고 무엇이 메서드인지를 가려서 나무 모양으로 세웁니다. 여기서 걸리는 것은 괄호를 안 닫았다거나 세미콜론을 빠뜨린 것처럼 글 모양이 어긋난 잘못입니다.
둘째 단계는 애너테이션 처리입니다. 애너테이션 처리기는 컴파일 도중에 애너테이션을 읽고 소스를 더 만들어 내는 프로그램입니다. 새로 만들어진 소스도 같은 컴파일에서 함께 처리됩니다.
셋째 단계에서 이름이 이어집니다. 소스에 적힌 이름 하나하나가 어느 클래스의 무엇을 가리키는지 찾아 붙입니다. 붙인 뒤에 타입이 맞는지 봅니다. 자바를 쓰면서 만나는 오류의 대부분이 이 단계에서 나옵니다.
넷째 단계에서 클래스 파일이 나옵니다. 앞에서 세운 구조를 바이트코드 명령으로 바꿔 적습니다. 상수와 메서드 목록도 함께 담습니다. 앞 세 단계에서 오류가 났으면 이 단계는 돌지 않습니다.
참조하는 클래스를 찾는 길
소스 하나만 놓고 컴파일이 끝나는 프로그램은 드뭅니다. 남이 만든 클래스를 부르고 있으면 javac 도 그 클래스가 어떻게 생겼는지 알아야 합니다. 메서드 이름이 맞는지, 인자 타입이 맞는지를 봐야 하기 때문입니다.
그래서 javac 는 클래스를 찾아다닐 곳의 목록을 받습니다. 이 목록이 클래스패스입니다. 클래스패스에 놓이는 것은 이미 컴파일이 끝난 클래스 파일이나, 그것들을 모아 둔 JAR(Java Archive, 자바 압축 묶음) 파일입니다.
아직 컴파일 안 한 소스에서 찾게 하려면 소스가 놓인 곳을 따로 알려 줍니다. 그러면 javac 가 그 소스까지 같이 컴파일합니다.
어느 쪽에서도 못 찾으면 「cannot find symbol」이라고 알려 줍니다. 이름을 잘못 적었을 때 나옵니다. 이름은 맞는데 클래스패스에 그 클래스가 없을 때도 나옵니다.
flowchart TD
A["부르는 클래스의 생김새가 필요하다"] --> B["클래스패스에서 찾는다"]
A --> C["소스 놓인 곳에서 찾는다"]
B --> D["클래스 파일을 읽어 모양만 본다"]
C --> E["그 소스도 같이 컴파일한다"]
B --> F["cannot find symbol"]
C --> F
컴파일 때 걸리는 잘못과 실행 때 터지는 잘못
javac 는 프로그램을 돌려 보지 않고 글만 읽습니다. 그래서 글만 봐도 아는 잘못은 걸러 냅니다. 값을 알아야 하는 잘못은 못 걸러 냅니다.
자바에는 미리 대비했다고 적어야만 부를 수 있는 예외가 있습니다. 이를 검사 예외라 합니다. 대비도 안 하고 위로 떠넘기지도 않으면 javac 가 막습니다.
| javac 가 걸러 주는 것 | 실행할 때 드러나는 것 |
|---|---|
| 문법이 어긋난 문장 | 값이 비어 있는데 부른 메서드 |
| 없는 메서드나 필드를 부른 곳 | 배열 범위를 넘긴 읽기 |
| 타입이 안 맞는 대입 | 실제 타입과 어긋난 형 변환 |
| 받지도 넘기지도 않은 검사 예외 | 클래스패스에서 사라진 클래스 |
오른쪽 칸이 남는 것은 그 잘못들이 값에 달려 있기 때문입니다. 어떤 값이 들어올지는 돌려 봐야 압니다. javac 는 돌려 보지 않습니다.
최적화를 실행할 때로 미루는 까닭
javac 는 컴파일할 때 최적화를 거의 하지 않습니다. 상수끼리 더한 식을 미리 계산해 두는 정도로 그칩니다. 그래서 클래스 파일 안의 명령은 소스 문장과 얼추 짝이 맞습니다. javap(클래스 파일 안의 바이트코드를 사람이 읽게 풀어 보여 주는 명령)로 열어 보면 그 짝이 눈에 보입니다.
바이트코드를 기계어로 바꾸는 일은 실행 중에 JIT 컴파일러(Just-In-Time compiler, 실행 중에 기계어로 옮기는 컴파일러)가 맡습니다. 어느 메서드가 많이 도는지는 돌려 봐야 알 수 있어서, 그 판단은 컴파일할 때가 아니라 실행할 때 내리는 쪽이 유리합니다.
flowchart TD
subgraph P1["컴파일할 때"]
S["자바 소스"] --> J["javac"]
J --> B["바이트코드"]
end
subgraph P2["실행할 때"]
B2["바이트코드"] --> T["JIT 컴파일러"]
T --> M["기계어"]
end
클래스 파일에 함께 담기는 것
클래스 파일에는 줄 번호와 소스 파일 이름도 같이 담깁니다. 예외가 터졌을 때 나오는 호출 목록, 곧 스택 트레이스에 줄 번호가 찍히는 것이 이 덕입니다. 지역 변수 이름처럼 실행에는 필요 없는 것까지 담으려면 javac 에 따로 옵션을 줘야 합니다.
직접 부르는 때와 빌드 도구가 부르는 때
파일이 몇 개뿐인 프로그램이면 javac 를 손으로 부르는 것으로 충분합니다. 소스 이름을 죽 적어 주면 클래스 파일이 나옵니다.
프로젝트가 커지면 Gradle·Maven 같은 빌드 도구가 대신 부릅니다. 어떤 라이브러리를 클래스패스에 넣을지, 무엇이 바뀌었으니 무엇을 다시 컴파일할지를 도구가 정해서 넘깁니다. 그래도 소스를 컴파일하고 오류를 내는 것은 javac 입니다. 빌드가 깨졌을 때 읽어야 하는 줄이 도구의 로그가 아니라 그 안에 실린 javac 의 오류인 까닭입니다.
같은 클래스 파일을 만드는 다른 컴파일러
같은 자바 가상 머신 위에서 도는 다른 언어는 자기 컴파일러를 씁니다. 코틀린 소스를 클래스 파일로 컴파일하는 것은 kotlinc 입니다. 나온 클래스 파일은 javac 가 만든 것과 같은 모양이라, 한 프로젝트 안에서 섞어 쓸 수 있습니다.
관련 항목
javac 가 읽고 써 내는 파일
클래스 파일 · 바이트코드 · JAR · 상수 풀 · 소스 코드
javac 를 담고 있는 배포 묶음
javac 와 같은 묶음에 들어 있는 다른 명령
java · javap · jar · javadoc · jshell · jlink · jdeps
javac 가 만든 파일을 실행하는 실행기
JVM · JIT 컴파일러 · 클래스 로더 · 인터프리터 · 가상 머신
컴파일이 지나가는 처리 단계
구문 분석 · 추상 구문 트리 · 타입 검사 · 심볼 테이블 · 코드 생성 · 애너테이션 처리
컴파일할 클래스를 찾아보는 경로
클래스패스 · 소스패스 · 모듈 경로 · 부트 클래스패스
javac 를 대신 불러 주는 빌드 도구
Gradle · Maven · Ant · Bazel · 증분 빌드
같은 가상 머신을 겨냥하는 다른 언어의 컴파일러
kotlinc · scalac · groovyc · 코틀린 · 스칼라
javac 가 검사할 때 보는 언어 규칙
제네릭 · 타입 소거 · 검사 예외 · 오토박싱 · 상수 접기
javac 를 쓰다 자주 만나는 오류
컴파일 오류 · 타입 오류 · NoClassDefFoundError · 클래스 파일 판 불일치
다른 이름: javac 명령 · 자바 컴파일러