자바 클래스 파일 분석기
자바 .class 파일은 압축 파일이 아니라 JVM 자체 포맷입니다. 파일 하나에 클래스 하나가 들어가고, 처음부터 끝까지 빅엔디안이며, 이름과 타입과 상수는 모두 파일 앞쪽 상수 풀의 인덱스로 기록됩니다. 이 도구는 그 구조를 브라우저 안에서 읽어 javap -v가 알려주는 내용을 보여줍니다. JDK를 설치할 필요도 없고 파일이 기기 밖으로 나가지도 않습니다. 헤더의 major 번호는 자바 릴리스와 대응합니다. 45는 자바 1.1, 49는 자바 5, 52는 자바 8이고 8 이후로는 릴리스마다 정확히 1씩 늘어나므로 55는 11, 61은 17, 65는 21, 69는 25입니다. UnsupportedClassVersionError가 가리키는 숫자가 바로 이것이며, 이 값은 파일을 읽을 수 있는 가장 낮은 JVM을 뜻하지 컴파일에 사용한 JDK를 뜻하지 않습니다. JDK 21에서 javac --release 8로 빌드하면 major 52 파일이 나옵니다. 접근 플래그를 풀어서 클래스인지 인터페이스인지 열거형인지 애너테이션인지 레코드인지도 알려 줍니다. 레코드는 전용 플래그가 없어 Record 속성으로 판별하고, 열거형은 ACC_ENUM으로 판별합니다. 상수 풀을 태그별로 세어 주기 때문에 생성된 클래스가 왜 이렇게 큰지 한눈에 보입니다. Utf8 항목 수천 개나 람다와 문자열 연결이 만들어 낸 InvokeDynamic 더미가 바로 드러납니다. 필드와 메서드는 디스크립터를 자바 타입으로 풀어 보여 주고, 메서드마다 Code 속성의 max_stack, max_locals, 코드 길이를 함께 표시합니다. 추상 메서드와 네이티브 메서드는 Code가 없으므로 대시로 표시됩니다. 참조 클래스 목록은 자기 자신을 뺀 모든 CONSTANT_Class 항목으로, 클래스 파일 하나가 담을 수 있는 가장 의존성 목록에 가까운 정보입니다. 알 수 없는 것도 분명합니다. 디스크립터는 소거된 타입이라 List<String> 필드는 Signature 속성이 없으면 java.util.List로만 보이고, -g:none으로 컴파일한 클래스에는 SourceFile도 줄 번호도 지역 변수 이름도 없어 스택 트레이스가 비어 보이며, 런타임에 이름으로만 로드되는 클래스는 여기에 나타나지 않습니다.
사용법
- .class 파일을 상자에 놓거나 클릭해서 고르세요. 컴파일된 클래스 하나여야 합니다. .jar는 zip이므로 먼저 풀고 원하는 클래스를 고르세요.
- 개요 줄에서 클래스 파일 버전, 그 버전에 해당하는 자바 릴리스, 타입 종류, 상수 풀 크기를 확인하세요.
- 선언 블록에서 상위 클래스, 인터페이스, 클래스 수준 속성, SourceFile 이름을 확인하세요. SourceFile과 디버그 정보가 모두 없다면 -g:none으로 빌드한 것입니다.
- 메서드 표를 훑어보세요. 최대 스택, 최대 지역 변수, 코드 바이트는 모두 Code 속성에서 오므로 추상, 네이티브, 인터페이스 메서드는 대시로 표시됩니다.
- 코드 양에 비해 클래스가 지나치게 크다면 상수 풀 분해를 펼쳐 보세요. 태그별 개수가 원인을 한 줄로 알려 줍니다.
자주 묻는 질문
- "class file version 65.0"은 무슨 뜻이고 UnsupportedClassVersionError는 왜 나나요?
- 두 숫자는 파일의 5~8번째 바이트에 있는 major와 minor 버전입니다. major 45가 자바 1.1이고 46, 47, 48이 각각 1.2, 1.3, 1.4이며 major 49(자바 5)부터는 major에서 44를 빼면 릴리스 번호가 됩니다. 그래서 52는 자바 8, 55는 11, 61은 17, 65는 21, 69는 25입니다. UnsupportedClassVersionError는 실행 중인 JVM이 파일의 major 버전보다 낮다는 뜻이고, 메시지에 두 숫자가 모두 나옵니다. 해결책은 더 새로운 런타임을 쓰거나 실제 배포 대상에 맞춰 --release를 지정해 다시 컴파일하는 것입니다. minor 65535는 특별한 값으로, 프리뷰 기능을 쓰는 파일임을 뜻하며 그 릴리스에서 --enable-preview로만 로드됩니다.
- 상수 풀은 왜 long이나 double 다음 인덱스를 건너뛰나요?
- 명세가 그렇게 정해 두었기 때문이며, 직접 만든 클래스 리더가 가장 흔하게 틀리는 지점이기도 합니다. CONSTANT_Long과 CONSTANT_Double 항목은 연속된 두 슬롯을 차지하고 두 번째 슬롯은 사용할 수 없습니다. 아무것도 그 슬롯을 가리킬 수 없고 다음 실제 항목은 인덱스가 하나 더 뒤에서 시작합니다. 자바 설계자들도 나중에 잘못된 선택이었다고 인정했지만 지금까지 만들어진 모든 클래스 파일에 그대로 박혀 있습니다. long 뒤에서 인덱스를 하나만 올리는 리더는 그 뒤의 풀 전체가 어긋나서 필드 이름, 메서드 이름, 클래스 참조를 모두 엉뚱한 항목에서 읽습니다. 오류가 아니라 그럴듯하지만 완전히 틀린 목록이 나오는 것이 문제입니다. 이 도구는 둘씩 건너뛰기 때문에 표시되는 풀 개수가 실제 항목 수보다 큽니다.
- 클래스 파일 안의 문자열은 UTF-8인가요?
- 거의 비슷하지만 차이가 있고 그 차이가 문제를 만듭니다. CONSTANT_Utf8은 수정 UTF-8을 씁니다. NUL 문자는 0 바이트 하나가 아니라 C0 80 두 바이트로 인코딩되어 문자열 안에 0 바이트가 절대 들어가지 않고, C 코드가 이름을 널 종료 문자열로 다룰 수 있습니다. 기본 다국어 평면 바깥의 문자는 실제 UTF-8이 쓰는 4바이트 형식이 아니라 UTF-16 서로게이트 쌍으로 쪼갠 뒤 각각을 3바이트 형식으로, 모두 6바이트로 기록합니다. 이 바이트들을 표준 UTF-8 디코더에 넘기면 대체 문자가 나오거나 예외가 납니다. 이 도구는 각 그룹을 UTF-16 코드 단위로 디코딩해 서로게이트 쌍이 다시 합쳐지도록 하며, 이는 JVM이 하는 방식과 같습니다.
- List<String> 필드가 왜 java.util.List로만 보이나요?
- 디스크립터가 소거된 타입이기 때문입니다. field_info 구조체는 디스크립터를 저장하는데 디스크립터에는 타입 인자를 넣을 자리가 없습니다. Ljava/util/List; 가 전부입니다. 제네릭 정보는 선택 속성인 Signature에 따로 보관되며 컴파일러가 제네릭 클래스, 필드, 메서드에 대해 디스크립터와 나란히 기록합니다. 리플렉션과 javap의 선언 출력이 이 값을 씁니다. 평범해 보이는 클래스나 멤버의 속성 목록에 Signature가 자주 등장하는 이유가 그것입니다. 어떤 필드가 원시 List가 아니라 List<String>이라는 사실은 클래스 파일 안에서 오직 Signature 속성만 알려 줍니다.
- 클래스, 열거형, 레코드, 애너테이션은 어떻게 구분하나요?
- 접근 플래그와 속성 하나로 구분합니다. ACC_INTERFACE는 인터페이스를 뜻하고, 애너테이션 타입은 여기에 ACC_ANNOTATION이 더해진 인터페이스입니다. javap가 애너테이션을 java.lang.annotation.Annotation을 확장하는 인터페이스로 출력하는 이유입니다. ACC_ENUM은 열거형을 뜻하며 상위 클래스가 java.lang.Enum이고 상수들은 역시 ACC_ENUM이 붙은 static final 필드로, 합성 $VALUES 배열과 함께 나타납니다. 레코드에는 전용 플래그가 아예 없습니다. java.lang.Record를 확장하는 final 클래스이면서 구성 요소를 나열한 Record 속성을 가지므로 그 속성만이 확실한 판별 근거입니다. ACC_MODULE은 module-info 파일을 뜻하며 필드도 메서드도 없고 애초에 클래스가 아닙니다.
- 참조 클래스 목록이 곧 이 클래스의 의존성인가요?
- 거의 그렇지만 빠지는 부분을 알아 둘 필요가 있습니다. 이 목록은 풀 안의 모든 CONSTANT_Class 항목이고 상위 클래스, 인터페이스, 생성하거나 캐스팅하거나 잡거나 필드와 메서드의 소유자로 쓰인 타입, 바이트코드에 등장하는 배열 타입을 모두 포함합니다. 하지만 디스크립터 안에만 등장하는 타입은 포함하지 않습니다. String을 받고 아무것도 반환하지 않는 메서드는 풀에 (Ljava/lang/String;)V를 Utf8 항목으로 넣을 뿐이고 java/lang/String이 CONSTANT_Class로 아예 나타나지 않을 수도 있습니다. 문자열로만 이름을 적어 Class.forName으로 로드하는 클래스나 런타임에 결정되는 서비스도 볼 수 없습니다. 컴파일 시점의 참조 집합으로 보아야 하며 런타임 전체 폐포로 보면 안 됩니다.
관련 도구
Python .pyc 바이트코드 분석기
__pycache__의 .pyc를 브라우저에서 엽니다. 어떤 CPython이 만들었는지, 무효화 방식, 코드 객체 트리와 디스어셈블리를 보여 줍니다.
gettext .mo 카탈로그 분석기
컴파일된 GNU gettext .mo 카탈로그를 브라우저에서 읽습니다. 바이트 순서, 헤더, 모든 msgid와 번역, 컨텍스트, 복수형, 미번역 항목까지 보여줍니다.
Semver 도구 (파서·비교기·범프)
두 시맨틱 버전 동시 파싱·비교·범프, ^x.y.z·~x.y.z가 실제 어디까지 매치하는지 보여주는 범위 확장기 포함.
파이썬 피클 검사기
.pkl 파일을 브라우저에서 디스어셈블합니다. 오프코드 전체, 메모, 복원된 값, 그리고 불러올 때 코드가 실행되는지까지.
ELF 바이너리 검사기
리눅스 실행 파일·.so·.o를 브라우저에서 엽니다. 아키텍처, 필요한 공유 라이브러리, 빌드 ID, PIE·NX·RELRO 적용 여부까지.
Java .properties 파서
java.util.Properties.load와 똑같이 .properties 파일을 파싱하고, 구분자·이스케이프·이어붙이기에서 놀랄 만한 줄을 짚어 줍니다.