사전 클래스 파일
포맷

클래스 파일

gabury1

클래스 파일은 자바로 쓴 코드를 자바 가상 머신이 읽을 수 있는 바이트로 옮겨 담습니다. 컴파일러가 소스 파일을 읽어 이 파일을 만들고, 실행할 때는 소스 없이 이 파일만 있으면 됩니다. 클래스 하나가 파일 하나가 됩니다. 이름 끝에는 .class 가 붙습니다.

쉽고 빠른 이해

클래스 파일은 자바 코드를 컴파일한 결과를 담는 파일입니다. Order.java 를 컴파일하면 Order.class 가 나오고, 실행할 때 자바 가상 머신이 읽는 것은 이 파일뿐입니다.

가상 머신 위에서 돌릴 코드라면 언어를 가리지 않고 반드시 이 파일이 됩니다. 골라서 쓰는 포맷이 아니라 거치지 않을 수 없는 포맷입니다.

가상 머신은 자바 문법을 모릅니다. 정해진 순서로 놓인 바이트만 읽습니다. 컴파일러가 소스를 그 바이트 배치로 옮겨 놓아야 합니다. 그 배치를 미리 정해 둔 것이 클래스 파일입니다.

어떻게 도는가:

  1. 컴파일러가 소스를 읽어 클래스마다 파일을 하나씩 만듭니다
  2. 파일 앞머리에 자기가 클래스 파일이라는 표식과 판 번호를 적습니다
  3. 코드가 쓰는 이름을 표 하나에 모으고, 명령어는 그 표의 번호로 이름을 가리킵니다
  4. 메서드 몸체와 그 밖의 정보는 이름이 붙은 덩이로 뒤에 이어 붙입니다

대가도 있습니다. 소스에만 있던 것은 안 남습니다. 주석과 지역 변수 이름이 그렇습니다.

상세

자바 소스에 적은 것과 JVM(Java Virtual Machine, 자바 가상 머신)이 실행하는 것은 서로 다른 물건입니다. 가상 머신은 for 문도 class 라는 낱말도 모릅니다. 정해진 순서로 놓인 바이트만 읽습니다.

클래스 파일은 그 둘 사이를 잇는 파일입니다. javac 같은 컴파일러가 소스를 읽어 이 파일을 만들고, 가상 머신은 소스를 한 번도 안 보고 이 파일만 읽어 실행합니다.

클래스 하나가 파일 하나

소스 파일 하나에 클래스를 여럿 적어도 나오는 클래스 파일은 클래스마다 하나씩입니다. 안쪽에 중첩해 적은 클래스도 자기 파일을 따로 갖습니다. 그 파일 이름은 바깥 클래스 이름과 안쪽 이름을 $ 로 이은 꼴이 됩니다.

flowchart TD
    S["Order.java"] --> C["javac"]
    C --> A["Order.class"]
    C --> B["Order$Item.class"]
    C --> D["OrderStatus.class"]

이렇게 갈라 두면 가상 머신이 필요한 클래스만 골라 읽을 수 있습니다. 프로그램을 켤 때 모든 코드를 한 번에 읽어 들이지 않습니다. 어떤 클래스가 처음 쓰이는 때에 그 파일을 찾아 읽습니다. 그 파일을 어디서 찾을지 알려 주는 목록이 클래스패스입니다.

파일 앞머리의 매직 넘버와 판 번호

파일의 첫 네 바이트는 언제나 같은 값입니다. 16진수로 CAFEBABE 라고 적히는 이 값을 매직 넘버라고 부릅니다. 파일 이름과 확장자는 누구나 바꿔 붙일 수 있으니, 가상 머신은 이름 대신 이 값을 보고 클래스 파일인지 가립니다.

그다음에 판 번호가 옵니다. 이 파일이 어느 자바 판에 맞춰 만들어졌는지를 나타내는 번호입니다. 자바 판이 올라가면 이 번호도 올라갑니다.

가상 머신은 자기가 아는 것보다 높은 번호를 만나면 읽기를 그만두고 UnsupportedClassVersionError 를 냅니다. 새 JDK(Java Development Kit, 자바 개발 도구 모음)로 빌드한 코드를 낡은 서버에 올리면 이 오류가 납니다. 반대로 낮은 번호는 그대로 읽습니다. 예전에 컴파일해 둔 파일은 새 가상 머신에서도 돕니다.

flowchart TD
    A["파일을 연다"] --> B{"앞 네 바이트가 CAFEBABE 인가"}
    B -- 아니다 --> X["클래스 파일이 아니다"]
    B -- 맞다 --> C{"판 번호가 가상 머신이 아는 것보다 높은가"}
    C -- 높다 --> Y["UnsupportedClassVersionError"]
    C -- 높지 않다 --> Z["그대로 읽는다"]

앞에서부터 차례로 읽는 배치

클래스 파일 안에는 구획을 나누는 표식도 목차도 없습니다. 각 부분이 정해진 순서로 붙어 있을 뿐입니다. 길이가 달라지는 부분은 앞에 개수나 길이를 먼저 적어 두고, 읽는 쪽은 그 수만큼 읽고 다음으로 넘어갑니다.

block-beta
columns 1
  a["매직 넘버"]
  b["판 번호"]
  c["상수 풀"]
  d["접근 플래그"]
  e["이 클래스 · 상위 클래스 · 구현한 인터페이스"]
  f["필드"]
  g["메서드"]
  h["속성"]

상수 풀은 코드가 쓰는 이름을 모아 둔 표입니다. 어떻게 쓰이는지는 뒤에서 따로 봅니다.

접근 플래그는 이 클래스가 public 인지 final 인지 인터페이스인지를 비트 하나씩으로 적어 둔 값입니다. 소스에 적은 제어자가 여기로 옮겨 옵니다. 그 뒤로는 이 클래스 자신의 이름, 상속받은 상위 클래스, 구현한 인터페이스 목록이 차례로 놓입니다.

맨 뒤의 속성은 이름과 길이를 먼저 적고 내용을 잇는 덩이입니다. 메서드 몸체도 이 꼴로 들어갑니다.

이 배치에는 대가가 있습니다. 파일 뒤쪽의 메서드 하나만 보려 해도 앞에서부터 죽 읽어야 합니다. 중간을 건너뛰고 바로 집을 수 없습니다. 대신 파일이 작아지고 읽는 쪽이 지킬 규칙이 단순해집니다.

이름을 모아 둔 상수 풀

메서드를 부르는 명령어는 상대를 메모리 주소로 가리키지 않습니다. 컴파일하는 때에는 그 메서드가 실행 중에 메모리 어디에 놓일지 알 수 없기 때문입니다. 그래서 명령어는 이름으로 상대를 가리킵니다.

그 이름들을 표 하나에 모아 두고 번호로 꺼내 쓰게 한 것이 상수 풀입니다. 문자열 리터럴, 클래스 이름, 메서드 이름과 타입이 전부 이 표에 들어갑니다. 파일의 다른 부분은 값을 직접 적지 않고 표의 번호만 적습니다. 같은 이름을 여러 곳에서 써도 파일에는 한 번만 들어간다는 뜻입니다.

flowchart TD
    subgraph MS["메서드들"]
        M1["명령어 · #7 을 부른다"]
        M2["명령어 · #7 을 부른다"]
    end
    subgraph CP["상수 풀"]
        C7["#7 · Order.print 이름과 타입"]
        C9["#9 · 문자열 리터럴"]
    end
    M1 --> C7
    M2 --> C7

메서드 몸체를 담는 코드 속성

메서드 목록에 있는 것은 이름과 타입뿐입니다. 몸체는 그 메서드에 딸린 Code 라는 속성에 들어갑니다. 그 안에 담기는 것이 바이트코드입니다. 가상 머신이 하나씩 읽어 실행하는 명령어의 나열입니다.

Code 는 명령어만 담지 않습니다. 이 메서드가 값을 쌓아 두는 스택을 얼마나 깊게 쓰는지, 지역 변수를 몇 칸 쓰는지도 같이 적혀 있습니다. 가상 머신은 메서드를 부르기 전에 그만큼의 공간을 미리 잡습니다. 예외를 붙잡는 구간도 표로 여기에 붙습니다.

flowchart TD
    M["메서드 · 이름과 타입"] --> CODE
    subgraph CODE["Code 속성"]
        B1["바이트코드"]
        B2["최대 스택 깊이"]
        B3["지역 변수 칸 수"]
        B4["예외 테이블"]
    end

모르는 속성을 건너뛰는 규칙

속성은 이름과 길이를 먼저 적는 꼴이라, 읽는 쪽이 그 이름을 몰라도 길이만큼 건너뛸 수 있습니다. 이 규칙 하나가 포맷을 늘릴 여지를 만듭니다.

flowchart TD
    A["속성 이름과 길이를 읽는다"] --> B{"아는 이름인가"}
    B -- 맞다 --> C["내용을 해석한다"]
    B -- 아니다 --> D["길이만큼 건너뛴다"]
    C --> A
    D --> A

새 자바 판이 속성을 하나 더해도 옛 도구는 그것을 건너뛰고 나머지를 읽습니다. 애너테이션이나 디버그 정보처럼 나중에 들어온 것들이 이렇게 붙었습니다.

다만 건너뛸 수 있다는 것은 없어도 된다는 뜻이기도 합니다. 실행에 꼭 있어야 하는 속성은 몇 안 됩니다. 나머지는 빠져 있어도 프로그램이 돕니다. 다음 절이 그 빠지는 것들입니다.

클래스 파일이 담지 않는 것

포맷은 무엇을 담느냐보다 무엇을 못 담느냐가 성격을 더 잘 보여 줍니다. 클래스 파일에서 빠지는 것은 대개 실행에 필요 없는 것들입니다.

소스에 있던 것 클래스 파일에서
주석 안 남습니다
지역 변수 이름 컴파일할 때 따로 시켜야 남습니다
소스의 줄 번호 마찬가지로 따로 시켜야 남습니다
제네릭에 적은 타입 인자 검사에 쓰고 지웁니다
소스 파일 여럿에 걸친 구조 파일 하나는 클래스 하나만 압니다

지역 변수 이름과 줄 번호가 빠지면 예외가 났을 때 스택 트레이스에 줄 번호가 안 찍힙니다. 서버에 올리는 빌드에는 대개 이 정보를 남기게 해 둡니다.

제네릭에 적은 타입 인자가 지워지는 것은 조금 다릅니다. 컴파일러가 그 타입으로 소스를 검사한 다음 지웁니다. 실행할 때는 대신 상위 타입이 들어갑니다. 타입 인자를 지우기 때문에 손에 든 리스트 하나를 놓고 무엇을 담기로 했던 것인지 실행 중에 물어볼 수 없습니다.

소스 없이 열어 보는 도구

클래스 파일은 사람이 읽으라고 만든 것이 아닙니다. 그래도 구조가 정해져 있어서 도구로 풀면 그대로 읽힙니다. 개발 도구 모음에 함께 오는 javap 가 상수 풀과 메서드별 명령어를 글로 찍어 줍니다.

한 걸음 더 나아가 소스에 가까운 코드를 되살리는 도구도 있습니다. 이 일을 역컴파일이라고 부릅니다. 클래스 이름과 메서드 이름이 파일에 그대로 남아 있어서 되살린 코드가 원본과 꽤 닮습니다. 배포하는 프로그램에는 대개 이름을 뜻 없는 글자로 바꾸는 난독화를 겁니다.

flowchart TD
    S["소스"] -- 컴파일 --> C["클래스 파일"]
    C -- javap --> R["사람이 읽는 글"]
    C -- 역컴파일 --> D["원본에 가까운 코드"]
    O["난독화 · 이름을 지운다"] --> C

자바 말고도 이 파일을 만드는 언어

가상 머신이 읽는 것은 클래스 파일이지 자바 소스가 아닙니다. 그래서 문법이 다른 언어라도 컴파일 결과를 이 포맷으로 내놓으면 같은 가상 머신 위에서 돕니다.

코틀린 컴파일러 kotlinc가 그렇게 합니다. 코틀린으로 쓴 클래스가 클래스 파일이 됩니다. 자바 쪽에서는 그것을 자바 클래스처럼 부릅니다. 반대 방향도 됩니다. 이 공통 포맷이 두 언어를 한 프로젝트에서 섞어 쓸 수 있는 바탕입니다.

실행 전에 받는 검증

클래스 파일은 컴파일러만 만들 수 있는 것이 아닙니다. 바이트를 직접 써서 만들 수도 있고, 이미 있는 파일을 고칠 수도 있습니다. 그래서 가상 머신은 읽어 들인 파일을 그대로 믿지 않습니다.

클래스를 처음 쓸 때 클래스 로더가 파일을 읽어 오고, 가상 머신이 그 내용을 검사합니다. 명령어가 스택을 규칙에 안 맞게 쓰지는 않는지, 가리키는 상수 풀 번호가 있기는 한지 같은 것들입니다. 검사를 통과하지 못한 파일은 실행되지 않습니다.

flowchart TD
    A["클래스를 처음 쓴다"]
    subgraph L["클래스 로더"]
        B["파일을 읽어 온다"]
    end
    subgraph V["가상 머신"]
        C{"검사를 통과하나"}
    end
    A --> B
    B --> C
    C -- 통과 --> D["실행한다"]
    C -- 못 통과 --> E["실행하지 않는다"]

이 검사 덕에 가상 머신은 명령어를 하나씩 실행하면서 매번 안전한지 다시 묻지 않아도 됩니다. 대가는 클래스를 처음 쓸 때 그 검사에 드는 시간입니다.

관련 항목

클래스 파일을 읽어 실행하는 실행 환경

JVM · JRE · 클래스 로더 · 런타임 상수 풀 · JIT 컴파일러 · 가비지 컬렉터

클래스 파일을 만들어 내는 컴파일러와 빌드 도구

javac · kotlinc · scalac · 컴파일러 · JDK · Gradle · Maven

클래스 파일 안에 들어 있는 구성 요소

상수 풀 · 바이트코드 · 디스크립터 · 접근 플래그 · 예외 테이블 · 애너테이션

클래스 파일을 뜯어보고 고치는 도구

javap · 역컴파일 · 난독화 · 바이트코드 조작 · ASM · 자바 에이전트

클래스 파일을 찾고 담아 나르는 경로

클래스패스 · JAR · WAR · 모듈 시스템 · 패키지

클래스 파일로 컴파일되는 언어

자바 · 코틀린 · 스칼라 · 그루비 · 클로저

소스에서 실행까지 클래스 파일이 지나는 단계

컴파일 · 클래스 로딩 · 링킹 · 검증 · 초기화

클래스 파일과 같은 노릇을 하는 다른 중간 표현

중간 표현 · WebAssembly · CIL · LLVM IR · 파이썬 바이트코드

다른 이름: class file · classfile · .class 파일