트랜잭션 트레이스를 들여다보다 보면, 가끔 모르는 클래스가 호출 스택에 떠 있을 때가 있습니다. `com.fasterxml.jackson.databind.ser.std.NumberSerializer$Long` 같은 이름이 나타나면, "이게 왜 여기서 응답 시간 200ms를 잡아먹고 있지?" 하고 잠깐 멈추게 됩니다.
이런 순간 가장 확실한 답은 그 클래스를 직접 들여다보는 것입니다. 다행히 Java는 디컴파일이 가능한 언어라, 소스가 없어도 라이브러리 안에서 무슨 일이 일어나는지 확인할 수 있습니다.
이 글은 Java가 왜 디컴파일이 되는지, JD-GUI를 어떻게 쓰는지, 그리고 운영 중 어떤 순간에 이 도구가 결정적인지를 실습과 함께 정리합니다.
Java는 `.java` 파일을 컴파일러로 바이트코드(`.class`)로 바꿔 실행합니다. 이 바이트코드는 특정 CPU나 OS가 아니라 JVM이 읽는 중간 형태라, 구조가 잘 보존돼 있습니다. 그래서 `.class` 파일을 거꾸로 돌려 다시 `.java` 형태에 가깝게 복원하는 일이 가능합니다. 이 과정이 디컴파일(역컴파일)입니다.
운영자 입장에서 중요한 점은 하나입니다. 소스 코드를 받지 못한 라이브러리라도, JAR만 있으면 내부 동작을 직접 확인할 수 있다는 것입니다. 운영 환경에서는 소스를 열어 볼 수 없는 상황이 더 많기 때문에, 디컴파일이 사실상 유일한 확인 수단이 되는 경우가 잦습니다.
JD-GUI는 가장 널리 쓰이는 GUI 기반 Java 디컴파일러입니다. Windows, Linux, macOS를 모두 지원하고, 내려받아 바로 실행할 수 있어 진입 장벽이 낮습니다.
기본 흐름은 단순합니다.
1. JD-GUI 실행
2. `파일 → JAR 열기`로 분석할 라이브러리 선택
3. 패키지 트리에서 클래스 클릭
4. 디컴파일된 Java 코드 확인
특정 클래스가 어떻게 동작하는지 궁금할 때, IDE에서 선언부로 바로 이동할 수 있습니다. Eclipse는 `F3`, IntelliJ는 `Cmd + B`(Windows는 `Ctrl + B`)입니다. 내가 작성한 `.java`라면 소스로 이동하지만, 소스가 없는 라이브러리라면 아래처럼 바이트코드만 표시됩니다.

JD-GUI로 열면 읽을 수 있는 코드로
같은 `.class`를 JD-GUI로 열면 Java 코드 형태로 복원돼 보입니다. 예를 들어 어떤 `Print` 클래스의 `print` 메서드가 `"HelloWorld"`와 현재 시각을 함께 출력하도록 작성됐다는 사실을 바로 확인할 수 있습니다. 더 깊이 들어가고 싶다면 `System.out.println` 내부까지 연쇄적으로 따라 내려가는 것도 가능합니다.

개별 `.class`뿐 아니라 JAR 파일을 통째로 열 수도 있습니다. JAR을 열면 내부 클래스들이 패키지 단위로 디컴파일되어 트리로 표시됩니다. 밑줄이 있는 클래스는 클릭하면 해당 소스로 이동하고(JAR 내부 클래스 간 이동), 디컴파일된 코드를 대상으로 필터링 검색도 할 수 있어 원하는 메서드를 빠르게 찾을 수 있습니다.

디컴파일을 알아두면 좋은 이유는 개발이 아니라 장애 상황에서의 속도입니다. 다음 세 장면에서 특히 그렇습니다.
트랜잭션 트레이스를 따라가다 보면 내 코드가 아니라 라이브러리 메서드가 병목으로 잡히는 경우가 있습니다. 메서드 이름만으로 동작을 추측하기 어렵다면, 해당 JAR을 JD-GUI로 열어 메서드 내부를 직접 확인하는 것이 가장 빠릅니다. 락을 잡고 있는지, 내부에서 외부 호출이 일어나는지 확인하면 다음 조치로 바로 이어집니다.
취약점(CVE)이 공개됐을 때 버전 정보만으로 영향 여부를 단정하기 어려운 경우가 있습니다. 특정 메서드 경로에서만 취약하다면, 디컴파일로 실제 코드 흐름을 확인하는 편이 가장 정확합니다. 문서만 보고 안전하다고 판단했다가 실제로는 취약 경로를 타고 있었던 사례도 드물지 않습니다.
결제나 인증 같은 외부 SDK가 문서와 다르게 동작할 때, 설명서만으로는 답이 안 나오는 경우가 많습니다. 이럴 때 디컴파일로 실제 구현을 들여다보면 예상과 실제의 차이를 바로 좁힐 수 있습니다.

소스 공개를 막으려고 난독화를 적용한 코드도 디컴파일은 됩니다. 하지만 클래스명과 변수명이 `a`, `b`, `c` 같은 의미 없는 문자열로 치환돼 있어 흐름은 보여도 의도를 읽기는 어렵습니다.
참고로 사람이 일부러 읽기 어렵게 짠 코드의 극단적인 예로 국제 난독화 C 코드 대회(IOCCC) 출품작들이 있습니다. 실제로 컴파일되고 동작하지만 사람이 해석하기는 거의 불가능한 코드들입니다. 난독화된 디컴파일 결과도 이와 비슷한 막막함을 줍니다.
정리하면 이렇습니다.
> 디컴파일은 가능하지만, 항상 이해 가능한 것은 아니다.
디컴파일한 코드를 다시 컴파일하려는 경우라면, 컴파일 당시 참조하던 라이브러리 의존성을 반드시 함께 확인해야 합니다. 클래스 코드를 거의 그대로 복원했더라도 필요한 의존성이 빠지면 컴파일 오류가 납니다.
- Java 8 람다 표현식 등 일부 최신 문법은 정확히 복원되지 않을 수 있습니다.
- 난독화된 코드의 가독성은 기대하기 어렵습니다.
- 디컴파일 결과가 원본과 100% 동일하지는 않습니다.
- 다시 컴파일하려면 원본 의존성이 필요합니다.
- 개발 중 자주 역컴파일해야 한다면 standalone 도구보다 IDE 디컴파일 플러그인이 더 편할 수 있습니다.
운영 환경에서 디컴파일은 매일 쓰는 도구는 아닙니다. 하지만 한 번 필요한 순간에는 다른 방법으로 대체하기 어렵습니다. 트레이스에서 모르는 클래스가 잡혔을 때 그 코드가 실제로 무엇을 하는지 빠르게 확인할 수 있으면, 문제 해결 속도가 크게 달라집니다.
도구의 구조와 동작을 이해하는 데는 매우 유용하지만, 디컴파일로 들여다본 다른 개발자의 코드를 무분별하게 가져다 쓰는 것은 지양해야 합니다.
트레이스에서 잡힌 느린 라이브러리 호출을 메서드 단위로 추적하고 싶다면, WhaTap APM으로 트랜잭션 흐름을 직접 확인해 볼 수 있습니다.