# JVM 예외가 이상하게 보이는 이유? 디컴파일러 시점에서 살펴보는 실전 가이드

*SEO 키워드: **JVM 예외**, **디컴파일러**, **자바 예외 처리***

---

# # 서론 – “왜 예외가 이렇게 동작할까?”

자바 개발자를 하다 보면 “예외가 발생했는데 스택 트레이스가 이해가 안 된다”는 상황을 자주 마주합니다. 특히 **JVM 예외**가 복잡하게 얽혀 있을 때, 디버깅이 어려워지는 경우가 많죠.

그런데 **디컴파일러**를 통해 바이트코드를 다시 자바 소스로 복원하면, 원본 코드와는 다른 형태의 예외 흐름이 드러나는 경우가 있습니다. 이 현상이 왜 발생하는지, 그리고 초보 개발자가 어떻게 대처하면 좋은지 단계별로 정리해 보겠습니다.

---

# # 1️⃣ 바이트코드와 예외 테이블 – JVM 내부 구조 이해하기

## # 1‑1. 예외 테이블이란?

JVM은 각 메서드마다 **Exception Table**이라는 메타데이터를 유지합니다. 여기엔 `try‑catch` 블록의 시작·끝 위치와, 해당 블록이 처리할 예외 타입이 기록됩니다.

```
text

[ start_pc, end_pc, handler_pc, catch_type ]

```

- `start_pc`와 `end_pc` : 예외가 발생할 수 있는 바이트코드 구간

- `handler_pc` : 예외가 잡히는 코드 라인(바이트코드)

- `catch_type` : 잡을 예외 클래스 (null이면 finally)

## # 1‑2. 디컴파일러가 보는 시점

디컴파일러는 **바이트코드**와 **예외 테이블**을 기반으로 원본 자바 코드를 추정합니다. 하지만 예외 테이블은 **정확히** 원본 `catch` 구문을 재현하지 못합니다.

- **인라인 최적화** : 컴파일러가 `try` 블록 안의 코드를 인라인하면, 예외 테이블에 남는 구간이 복잡해져 `catch` 위치가 어색하게 보일 수 있습니다.

- **다중 catch** : 동일한 핸들러가 여러 예외 타입을 처리하면, 디컴파일러는 이를 하나의 `catch` 문으로 합치거나, 반대로 분리해서 보여줄 수 있습니다.

> **핵심 포인트**: 디컴파일된 코드는 **실제 실행 흐름**을 정확히 반영하지만, **원본 소스**와는 다를 수 있습니다.

---

# # 2️⃣ 흔히 마주하는 “이상한” 예외 현상 3가지

| 현상 | 원인 | 디컴파일러가 보여주는 모습 |

|------|------|---------------------------|

| **예외가 전혀 잡히지 않는다** | `finally` 블록에서 `throw`가 발생 → 예외가 재전파됨 | `try`‑`catch`가 사라지고 `finally`만 남음 |

| **같은 예외가 두 번 잡힌다** | 컴파일러가 **중첩된 try‑catch**을 최적화 | 중복 `catch` 구문이 병합되어 보임 |

| **예외 타입이 다르게 표시된다** | **다형성**(다중 상속) 때문에 실제 타입이 상위 클래스 | `catch (Exception e)` 로 축소된 형태로 표시 |

## # 예시 코드

```
java

public void demo() {

    try {

        riskyOperation();          // ← 여기서 NullPointerException 발생 가능

    } catch (NullPointerException | IllegalArgumentException e) {

        System.out.println('잘못된 인자');

    } finally {

        cleanUp();                  // ← 이 안에서 또 예외 발생 가능

    }

}

```

디컴파일러가 만든 결과물은 다음과 비슷하게 보일 수 있습니다.

```
java

public void demo() {

    try {

        riskyOperation();

    } catch (Exception e) {          // 실제로는 두 개의 catch가 합쳐짐

        System.out.println('잘못된 인자');

    }

    cleanUp();                       // finally가 별도 블록이 아님

}

```

---

# # 3️⃣ 초보자를 위한 예외 디버깅 팁

1. **바이트코드 직접 확인**

   - `javap -c -v ClassName` 명령으로 **Exception Table**을 확인하면, 실제 예외 흐름을 파악할 수 있습니다.

2. **디컴파일러 종류 비교**

   - `JD‑GUI`, `CFR`, `Procyon` 등 여러 도구를 사용해 결과를 비교하면, 어느 부분이 최적화된 결과인지 눈에 띕니다.

3. **로그에 스택 트레이스 전체 출력**

   - `e.printStackTrace()` 대신 `java.util.logging`이나 `SLF4J`로 **전체 스택**을 남기면, `catch` 구문이 어디서 발생했는지 명확해집니다.

4. **예외 재발 방지**

   - `finally` 블록에서 **예외를 다시 던지지** 않도록 주의하고, 필요 시 `try‑with‑resources`를 활용해 자동 자원 해제를 구현합니다.

5. **테스트 코드에 예외 시나리오 추가**

   - JUnit의 `assertThrows`를 이용해 다양한 예외 상황을 검증하면, 숨겨진 예외 흐름을 사전에 발견할 수 있습니다.

---

# # 4️⃣ 디컴파일러가 알려주는 “숨은” 최적화

컴파일러는 **코드 크기와 실행 속도**를 최적화하기 위해 다음과 같은 변환을 수행합니다.

| 최적화 기법 | 디컴파일러 결과 |

|------------|-----------------|

| **인라인 메서드** | `try` 구문이 사라지고, 직접 호출된 바이트코드가 삽입 |

| **예외 재사용** | 동일한 핸들러가 여러 구간에 매핑되어 `catch`가 하나로 합쳐짐 |

| **조건부 예외 제거** | 컴파일 타임에 검증 가능한 `NullPointerException`은 제거되어 `catch`가 사라짐 |

이러한 최적화는 **성능 향상**을 목표로 하지만, 디버깅 시에는 **예외 흐름이 왜 이렇게 보이는지**를 이해하는 것이 중요합니다.

---

# # 결론 – 디컴파일러와 친해져서 예외를 마스터하자

- **JVM 예외**는 바이트코드 수준에서 관리되는 복잡한 구조이며, **디컴파일러**는 이를 사람이 읽기 쉬운 형태로 변환하지만 100% 원본과 일치하지 않을 수 있습니다.

- 바이트코드와 예외 테이블을 직접 살펴보고, 여러 디컴파일러 결과를 비교하면 “이상한” 예외 현상의 원인을 정확히 파악할 수 있습니다.

- 초보 개발자는 위에서 소개한 **디버깅 팁**을 활용해 예외 흐름을 명확히 하고, 불필요한 예외 재발을 방지하는 코드를 작성해 보세요.

> **행동 유도**: 지금 바로 프로젝트에 `javap` 명령을 적용해 보세요. 직접 확인한 Exception Table을 기반으로 코드를 리팩터링하면, 예외 처리 로직이 한층 견고해집니다.

---

*이 글이 도움이 되었다면, 댓글로 여러분이 겪은 특이한 JVM 예외 사례를 공유해 주세요! 또, 더 깊이 있는 내용이 궁금하다면 구독과 알림 설정도 잊지 마세요.*
