이펙티브 자바 완벽 공략 1부

아이템 9. try-finally 보다 try-with-resouces를사용하라

아이템 9. try-finally 보다 try-with-resouces를사용하라


핵심 정리

  • Java 7부터(Java 8 이상을 사용 중이라면 더욱) 자원 회수에 try-finally는 더 이상 최선의 방법이 아니다
  • try-with-resources

    • 코드가 더 짧고
    • 더 안전하며
    • 예외 정보까지 더 정확하게 보존한다

try-finally 방식의 한계

단일 자원일 때조차 불편하다

// 코드 9-1 try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! (47쪽)
static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        br.close();
    }
}
  • 코드 자체는 단순해 보이지만
  • 자원이 늘어나는 순간 복잡도가 급격히 증가한다

자원이 둘 이상이면 코드가 급격히 지저분해진다

// 코드 9-2 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! (47쪽)
static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    try {
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            out.close();
        }
    } finally {
        in.close();
    }
}
  • 자원이 늘어날수록 중첩 try-finally가 증가
  • 가독성 급격히 저하
  • 실수 가능성 증가

중첩을 피하려다 더 위험해지는 경우

아래 코드는 겉보기엔 깔끔해 보이지만 위험한 코드다.

static void copy(String src, String dst) throws IOException {
    InputStream in = new FileInputStream(src);
    OutputStream out = new FileOutputStream(dst);
    try {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
    } finally {
        in.close();
        out.close();
    }
}
문제점
  • in.close()에서 예외가 발생하면
  • out.close()절대 호출되지 않는다
  • 결과적으로 자원 누수(resource leak) 발생 가능

👉 try-finally는 조심해서 써야 하는 패턴이지, 안전한 기본값이 아니다.


try-with-resources: 자원 회수의 최선책

단일 자원

// 코드 9-3 try-with-resources - 자원을 회수하는 최선책! (48쪽)
static String firstLineOfFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(
            new FileReader(path))) {
        return br.readLine();
    }
}
  • 코드가 훨씬 짧다
  • 자원 반납이 명확하다
  • 실수할 여지가 거의 없다

복수 자원

// 코드 9-4 복수의 자원을 처리하는 try-with-resources - 짧고 매혹적이다! (49쪽)
static void copy(String src, String dst) throws IOException {
    try (InputStream   in = new FileInputStream(src);
         OutputStream out = new FileOutputStream(dst)) {
        byte[] buf = new byte[BUFFER_SIZE];
        int n;
        while ((n = in.read(buf)) >= 0)
            out.write(buf, 0, n);
    }
}
  • 선언 순서의 역순으로 자동 close
  • 모든 자원이 반드시 닫힌다
  • 중첩 없음, 실수 없음

예외 정보가 훨씬 유용하다

이 부분이 try-with-resources의 가장 큰 숨은 장점이다.

try-finally의 문제

  • 가장 나중에 발생한 예외만 남는다
  • 최초 예외는 완전히 사라질 수 있다
  • 디버깅 시 가장 중요한 정보가 손실

특히,

  • 첫 번째 예외가 원인
  • 이후 예외는 후속 증상인 경우가 많다

하지만 try-finally에서는 👉 첫 번째 예외가 먹혀버릴 수 있다


try-with-resources의 장점

  • 가장 처음 발생한 예외가 유지된다
  • close() 중 발생한 예외는

    • suppressed exception으로 함께 보존된다
  • 결과적으로 문제의 원인을 정확히 파악 가능

catch 절과 함께 사용하기

try-with-resourcestry-catch-finally 구조를 그대로 지원한다.

// 코드 9-5 try-with-resources를 catch 절과 함께 쓰는 모습 (49쪽)
static String firstLineOfFile(String path, String defaultVal) {
    try (BufferedReader br = new BufferedReader(
            new FileReader(path))) {
        return br.readLine();
    } catch (IOException e) {
        return defaultVal;
    }
}
  • 필요하다면 catch 추가 가능
  • 필요하다면 finally도 추가 가능
  • 표현력은 유지하면서 안전성만 높아진다

이 섹션의 핵심 메시지

  • try-finally는 이제 권장되는 기본 패턴이 아니다
  • try-with-resources는

    • 코드가 짧고
    • 자원 누수를 막아주며
    • 예외 정보까지 온전히 보존한다
  • Java 7 이후라면 👉 자원 관리의 기본값은 항상 try-with-resources다

완벽 공략

  • p48, 자바 퍼즐러 예외 처리 코드의 실수
  • P49, try-with-resources 바이트코드

자바 퍼즐러: 예외 처리 코드의 실수

자바 퍼즐러(Java Puzzlers)는 조슈아 블로크(Joshua Bloch)가 집필한 책으로, 자바 개발자들이 자주 빠지는 언어적·개념적 함정을 퍼즐 형식으로 설명한다.

그중 하나가 바로 예외 처리 코드에서의 치명적인 실수다.


문제의 코드

public class Copy {
    private static final int BUFFER_SIZE = 8 * 1024;

    static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        OutputStream out = new FileOutputStream(dst);
        try {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                // TODO 이렇게 하면 되는거 아닌가?
            }

            try {
                in.close();
            } catch (IOException e) {
                // TODO 안전한가?
            }
        }
    }
}

겉보기에는 상당히 “조심스럽게 작성된 코드”처럼 보인다.

  • 각 자원을 개별적으로 close
  • close 중 발생하는 예외도 try-catch로 감쌈
  • 자원 누수는 없어 보임

하지만 이 코드는 여전히 안전하지 않다.


왜 문제가 되는가?

1. close()에서 RuntimeException이 발생할 수 있다
  • close()IOException만 던진다고 기대하지만
  • 실제 구현체에서는

    • RuntimeException
    • Error
    • 서드파티 라이브러리 내부 예외 가 발생할 수 있다

이 경우:

  • 해당 예외는 잡히지 않고 전파
  • 다른 자원의 close()는 실행되지 않을 수 있음
  • 결국 자원 누수 또는 비정상 종료로 이어질 수 있다

2. 최초 예외가 완전히 사라질 수 있다
  • try 블록에서 발생한 원래 예외
  • finally 블록에서 발생한 close 예외

이 둘이 충돌하면:

  • finally 예외가 원래 예외를 덮어쓴다
  • 디버깅 시 가장 중요한 원인 정보가 손실

이게 바로 자바 퍼즐러가 지적한 핵심 문제다.


핵심 메시지 (자바 퍼즐러의 결론)

try-finally 기반 예외 처리는 아무리 신중하게 작성해도 본질적으로 취약하다.

그래서 이 문제는 “개발자가 더 잘 짜면 해결되는 문제”가 아니라 언어 차원에서 해결해야 하는 문제였다.


try-with-resources 바이트코드

try-with-resources는 단순한 문법 설탕이 아니다. 컴파일러가 예외 처리 규칙을 강제하는 구조로 변환한다.


컴파일러가 보장하는 것

try-with-resources를 사용하면 컴파일러는 다음을 자동으로 생성한다.

  1. 가장 처음 발생한 예외를 주 예외(primary exception)로 유지
  2. close() 중 발생한 예외는
  • Throwable.addSuppressed()로 보존
    1. 모든 자원이 반드시 역순으로 close
    2. 어떤 상황에서도 예외 정보가 유실되지 않음

즉,

  • 개발자가 직접

    • try-catch 중첩을 관리할 필요 없음
    • suppressed exception을 수동 처리할 필요 없음
  • 컴파일러가 항상 올바른 예외 흐름을 생성


왜 바이트코드까지 봐야 하는가?

바이트코드를 보면 분명해진다.

  • try-with-resources는

    • “편한 문법”이 아니라
    • 정확한 예외 처리 의미론을 강제하는 구조
  • 그래서 try-finally로 완전히 동일한 동작을 흉내 내려면

    • 코드가 길어지고
    • 실수 가능성이 폭증한다

이 섹션의 핵심 요약

  • try-finally는

    • 예외를 숨길 수 있고
    • close 중 예외에 취약하며
    • 아무리 조심해도 완전하지 않다
  • try-with-resources는

    • 언어 차원에서
    • 예외 보존 규칙을 강제하고
    • 자원 반납을 보장한다

그래서 try-with-resources는 “권장”이 아니라 현대 자바에서의 “기본값”이다.



© 2020. All rights reserved.

SIKSIK