본문 바로가기
Java/Java

try-finally 보다는 try-with-resources를 사용하자

by oneny 2023. 8. 10.

try-finally

Java를 처음 배울때 finally 블록은 예외가 발생하든 발생하지않든 항상 실행이 되기 때문에 finally 블록에서 자원을 반납하라고 배웠다. 하지만 Java 8 이상부터는 try-finally는 더이상 최선의 방법이 아니다. try-with-resources를 써야한다.

 

try-finally 예시코드

public class Copy {
  
  private static final int BUFFER_SIZE = 8 * 1024;
  
  static void copy(String src, String dst) throws IOException {
    FileInputStream in = new FileInputStream(src);
    
    try {
      FileOutputStream 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();
    }
  }

  public static void main(String[] args) throws IOException {
    String src = args[0];
    String dst = args[1];
    copy(src, dst);
  }
}

만약 여러 개의 리소스를 다뤄야 한다면 위 코드처럼 작성해야 안전하게 리소스를 닫을 수 있다. OutputStream과 InputStream을 동시에 finally 블록에서 닫아준다면 먼저 닫아준 메서드가 혹시나 에러가 발생했을 때는 그 뒤에 닫아주려는 메서드가 실행되지 않아 메모리 누수가 발생할 수도 있다. 따라서 out.close()가 문제가 생기더라도 in.close()가 실행될 수 있도록 조금이라도 더 안전하게 닫기 위해서는 위 코드처럼 작성해야 한다. 이러한 코드는 문제가 있는 것은 아니고 가독성이 좋지 못하거나 가장 처음 발생한 예외를 출력할 수 없다는 단점 등이 존재하기 때문에 이 방법 보다는 권장하는 방법이 있다.

 

자바 퍼즐러의 실수

public class Copy {

  private static final int BUFFER_SIZE = 8 * 1024;

  static void copy(String src, String dst) throws IOException {
    FileInputStream in = new FileInputStream(src);
    FileOutputStream 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 이렇게 하면 되는거 아닌가?
      }
    }
  }

  public static void main(String[] args) throws IOException {
    String src = args[0];
    String dst = args[1];
    copy(src, dst);
  }
}

이펙티브 자바를 보면 이펙티브를 작성한 조슈아 블로크도 다른 자바 퍼즐러라는 책에서 위 같은 실수를 저질렀고, 수년간 아무도 눈치채지 못했다고 한다. 위처럼 작성하면 안되는 이유가 무엇일까? 만약 close() 메서드를 호출해서 실행하는데 만약 IOException이 아니라 다른 RuntimeException이 발생하면 어떻게 될까? 이러한 경우때문에 안전하지 않다고 하는 것이다.

이제는 이러한 실수를 하지 않도록 해주는 try-with-resources라는 아주 강력한 무기가 있다.

 

try-with-resources

try-with-resources는 try에 자원 객체를 전달하면, try 코드 블록이 끝나면 자동으로 종료해주는 기능이다. 예시 코드를 살펴보면서 해당 기능에 대해 좀 더 자세히 살펴보자.

 

try-with-resources 예시 코드

public class Copy {

  private static final int BUFFER_SIZE = 8 * 1024;

  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);
      }
    }
  }

  public static void main(String[] args) throws IOException {
    String src = args[0];
    String dst = args[1];
    copy(src, dst);
  }
}

위처럼 try-with-resources를 여러 리소스를 다뤄야할 때 빛을 발한다. try-with-resources를 사용하면 더 이상 finally 블록을 사용할 필요가 없고 OutputStream과 InputStream에서 Closeable 인터페이스를 구현하여 close() 메서드를 재정의했기 때문에 개발자가 직접 close() 메서드를 호출해서 닫는 일을 하지 않아도 된다.

이렇게 코드를 간결하게 작성할 수 있다는 장점이 있는데 이것보다 더 중요한 장점이 있다.

 

try-with-resources의 큰 장점(예외를 잡아먹지 않는다.)

public class BadBufferedReader extends BufferedReader {
  public BadBufferedReader(Reader in, int sz) {
    super(in, sz);
  }

  public BadBufferedReader(Reader in) {
    super(in);
  }

  @Override
  public String readLine() throws IOException {
    throw new CharConversionException();
  }

  @Override
  public void close() throws IOException {
    throw new StreamCorruptedException();
  }
}

BadBufferedReader는 BufferedReader를 상속받아 readLine()을 호출할 때는 CharConversionExcetion 예외를, close()를 호출할 때는 StreamCorruptedException 예외가 발생하도록 오버라이딩했다.

 

public class TopLine {

  static String firstLineOfFile(String path) throws IOException {
  
    BufferedReader br = new BadBufferedReader(new FileReader(path));
    try {
      return br.readLine();
    } finally {
      br.close();
    }
  }

  public static void main(String[] args) throws IOException {
    System.out.println(firstLineOfFile(System.getProperty("user.dir") + separator + "new.iml"));
  }
}

TopLine 클래스에서 BadBufferedReader를 사용하는 경우에 br.readLine();을 호출하면서 CharConversionException 예외가 발생하고, br.close();을 호출하면서 StreamCorruptedException 예외가 발생할 것이다. 그러면 실제 코드를 실행했을 때 어떤 에러가 보일까? 위에서 출력된 것을 보면 확인할 수 있는데 가장 나중에 발생한 StreamCorruptedException만 보이는 것을 알 수 있다. 디버깅하다보면 가장 처음에 발생한 예외가 무엇인지 중요한데 가장 나중에 발생한 예외가 자기 이전에 예외를 먹어버리게 된다.

 

static String firstLineOfFile(String path) throws IOException {
  try (BufferedReader br = new BadBufferedReader(new FileReader(path))) {
    return br.readLine();
  }
}

하지만 try-with-resources를 사용하면 어떻게 될까? 코드는 당연히 간결해졌고 위처럼 예외가 발생하게 되면 readLine() 메서드를 호출하면서 CharConversionException이 발생할텐데 해당 예외가 잘나올 수 있을까?

출력된 결과를 보면 알 수 있듯이 가장 먼저 발생한 예외인 CharConversionException이 나오고, 그리고 후속으로 발생한 StreamCorruptedException도 StackTrace에서 같이 보인다. try-catch-finally로 위처럼 처음 발생한 예외부터 출력되게 만들기 위해서는 코드가 많이 지저분해지는데 try-with-resources는 개발자가 그런 코드를 작성하지 않아도 되게 만들어준다.

 

 

try-with-resources 바이트코드

try-with-resources는 어떻게 close() 메서드를 호출하면서 첫 번째 예외가 보존이 될까? 이는 바이트코드를 열면 정답을 알 수 있다.

IntelliJ의 힘을 빌어 out 파일의 컴파일된 바이트코드를 보면 우리가 재해석할 수 있는 형태로 만들어줘 우리가 읽을 수 있다. 바이트 코드를 살펴보면 addSuppressed() 메서드가 있는데 이 메서드를 통해 후속해서 발생하는 예외를 계속 담아 가장 처음 발생한 예외부터 출력할 수 있는 것이다.

 

 

핵심 정리

  • try-finally는 더이상 최선의 방법이 아니다(자바7부터)
  • try-with-resources를 사용하면 코드가 더 짧고 분명하다.
  • 만들어지는 예외 정보도 훨씬 유용하다.

 

 

출처

자바 최적화 : 가장 빠른 성능을 구현하는 검증된 10가지 기법

이펙티브 자바 Effective Java 3/E - Item.9 try-finally보다는 try-with-resources를 사용하라

이펙티브 자바 완벽 공략 1