Home Item9 (try-finally 보다 try-with-resources를 사용하라)
Post
Cancel

Item9 (try-finally 보다 try-with-resources를 사용하라)

try-finally 보다 try-with-resources를 사용하라

자바 라이브러리에는 close() 메서드를 호출해 직접 닫아주어야 하는 자원이 많다.

  • InputStream
  • OutputStream
  • java.sql.Connection

위와 같은 자원들이 좋은 예시이다.

이러한 자원 닫기는 클라이언트가 놓치기 쉬워 예측할 수 없는 성능 문제로 이어진다. 이런 자원 중 상당수가 안전망으로 finalizer를 활용하고 있지만 확실성이 없다. (Item 8)

try - finally를 써서 자원 닫기

1
2
3
4
5
6
7
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

try {
	String str = br.readLine();
} finally {
	br.close();
}

위의 경우 전혀 문제가 없어 보인다. 그러나 만약 자원을 하나 더 사용하게 된다면?

1
2
3
4
5
6
7
8
9
10
11
12
13
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

try {
	BufferedWriter bw = new BufferedWriter(new OutputStreamReader(System.out));
	try {
		String str = br.readLine();
		bw.write(line);
	} finally {
		bw.close();
	}
} finally {
	br.close();
}

자원이 단 하나만 추가되었는데도 코드가 굉장히 지저분하다.

지저분한 것 이외에도 큰 문제가 있다.

  • try블록과 finally 블록에서 예외가 모두 발생할 수 있다.
    • 만약 try 블록을 실행하던 중 기기에 문제가 생긴다면 br.readLine() 메서드가 정상적으로 실행되지 못하고 예외를 던지게 될 것이다.
    • 기기에 문제가 생겼기 때문에 finally 블록의 close() 메서드도 예외를 발생시킨다.
    • 이런 상황이라면 finally 블록에서 발생한 예외가 try 블록의 예외를 삼켜 처음 발생한 예외에 대한 정보가 발생하지 않는다.

이렇게 되면 디버깅이 힘들다. 물론 예외 발생 시 이전 예외를 기록하도록 코드를 수정할 수는 있지만 코드가 매우 지저분해질 것이다.

종합적으로 try-finally 구문은 가독성을 떨어뜨릴 위험성, 그리고 예외 처리 로직에 대한 결함의 문제가 있다.

try-with-resources의 사용

위와 같은 try-finally 구문의 문제는 자바 7이 제공한 try-with-resources 덕분에 해결되었다.

참고로 try-with-resource를 사용하기 위해서는 사용하는 자원이 AutoCloseable 인터페이스를 구현해야 한다.

1
2
3
public interface AutoCloseable {
	void close() throws Exception;
}

자바 라이브러리와 서드파티 라이브러리들의 수많은 클래스와 인터페이스가 이미 AutoCloseable을 구현하거나 확장해두었다.

만약 우리가 자원을 닫아야하는 클래스를 커스텀해야 한다면 AutoCloseable을 반드시 구현해야 할 것이다.

1
2
3
4
5
6
try (BufferedReader br = new BufferedReader(new InputStream(System.in))) {
	BufferedWriter bw = new BufferedWriter(new OutputStreamReader(System.out));
	
	String str = br.readLine();
	bw.write();
}
  • try 키워드 괄호 안에서 자원을 획득한다.
    • br 획득
    • bw 획득
  • try 블록을 벗어나면 자원들이 자동으로 닫힌다. 이 때 자원들은 선언 순서의 역순으로 닫힌다.
    • bw 닫힘
    • br 닫힘
  • try 블록 내에서 예외가 발생할 경우 catch 블록으로 전달된다. 이 후 자원들은 예외 발생 여부와 관계없이 닫히게 된다.
    • try문을 중첩하지 않고도 다수의 예외를 처리할 수 있다.

위와 같이 가독성이 매우 좋아졌다. 심지어 가독성 뿐만 아니라 readLine과 close 모두에서 예외가 발생하는 경우 close 호출 시 발생하는 예외는 숨겨지고 readLine에서의 예외가 기록된다.

숨겨진 예외의 처리

1
2
3
4
5
6
7
8
9
10
11
static class CustomReader extends BufferedReader {  
  public CustomReader(StringReader reader) {  
	  super(reader);  
  }  
  
  @Override  
  public void close() throws IOException {  
	  super.close();  
	  throw new IOException("close() 호출 예외 발생");
  }  
}
1
2
3
4
5
6
7
8
9
10
11
try (BufferedReader br = new CustomReader(new StringReader("abc"))) {  
	  throw new IOException("try 구문 내에서 예외 발생");  
} catch (Exception e) {  
	  e.printStackTrace();  
	  System.err.println("예외 발생: " + e.getMessage());  
	  
	  Throwable[] suppressed = e.getSuppressed();  
	  for (Throwable t : suppressed) {  
	  System.err.println("Suppressed 예외: " + t.getMessage());  
  }  
}
  • CustomReader를 생성했다.
  • 그런데 try 구문에서 예외를 발생시켰다.
    • try 블록에서 벗어났기 때문에 자원을 close() 해준다.
  • close()할때도 예외가 발생되었다.

stackTrace를 해보면 위와 같이 Suppressed라고 되며 close() 메서드의 예외는 숨겨진 것을 볼 수 있다.

이렇게 suppressed 상태가 된 예외는 Java 7부터 도입된 getSuppressed 메서드를 통해 가져와서 사용할 수 있다.

1
2
3
4
Throwable[] suppressed = e.getSuppressed();  
for (Throwable t : suppressed) {  
   System.err.println("Suppressed 예외: " + t.getMessage());  
}

정리

close()를 통해 자원을 회수해야 하는 객체를 다룰 때는 try-finally보다 try-with-resources를 사용하면 된다. 어떠한 경우에도 코드는 더 짧고 분명해지며 만들어지는 예외 정보도 훨씬 유용하게 사용할 수 있다.

try-with-resources를 사용할 경우 AutoCloseable 인터페이스를 구현해야 한다는 점은 잊으면 안된다.

This post is licensed under CC BY 4.0 by the author.

Item8 (finalizer와 cleaner 사용을 피하라)

Item10 (equals는 일반 규약을 지켜서 재정의하라)