자바 라이브러리에서는 close()
메서드를 호출해 직접 닫아줘야 하는 자원이 많다. 그러나 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다. 전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally
가 쓰였다. 예외가 발생하거나 메서드에서 반환되는 경우를 포함해서 말이다.
public class FirstError extends RuntimeException{
}
public class SecondError extends RuntimeException{
}
public class MyResource implements AutoCloseable{
public void doSomething(){
System.out.println("Do something");
throw new FirstError();
}
@Override
public void close() throws Exception {
System.out.println("Close My Resource");
throw new SecondError();
}
}
public class AppRunner {
public static void main(String[] args) throws Exception {
/**
* 고전적인 예외 처리 방법
*/
MyResource myResource=new MyResource();
try {
myResource.doSomething();
}finally {
myResource.close();
}
}
try-finally
를 제대로 사용한 위의 코드 예제조차 미묘한 결점이 있다. 예외는 try
블록과 finally
블록 모두에서 발생할 수 있다. 위 코드 try
블록에서 myResource.doSomething()
을 호출하면 FirstError
가 발생한다. 그러면 finally
블록으로 간다. 그러나 finally
블록에서 myResource.close()
를 실행하면 SecondError
가 발생한다. 이런 상황이면 아래의 출력 결과로 알 수 있듯이 두 번째 예외가 첫 번째 예외를 집어삼켜 버린다.
Do something
Close My Resource
Exception in thread "main" Item09.SecondError
at Item09.MyResource.close(MyResource.java:12)
at Item09.AppRunner.main(AppRunner.java:21)
그러면 스택 추적 내역에 첫 번째 예외에 관한 정보가 남지 않게 되어, 실제 시스템에서의 디버깅을 몹시 어렵게 한다. 왜나하면 당연히 문제 발생을 진단하려면 처음 발생한 예외를 보고 싶기 떄문이다.
또한, 자원을 하나 더 사용하게 되면 코드가 많이 지저분해진다.
public class AppRunner {
public static void main(String[] args) throws Exception {
/**
* 고전적인 예외 처리 방법
*/
MyResource myResource=new MyResource();
try {
myResource.doSomething();
MyResource newMyResource = null;
try{
newMyResource=new MyResource();
newMyResource.doSomething();
}finally {
if(newMyResource!=null) {
newMyResource.close();
}
}
}finally {
myResource.close();
}
}
}
이러한 문제들은 자바 7에서 추가된 try-with-resources
로 모두 해결할 수 있다. 이 구조를 사용하려면 해당 자원이 AutoCloseable
인터페이스를 구현해야 한다. 단순히 void
를 반환하는 close()
메서드를 하나만 덩그러니 정의한 인터페이스다. 닫아야하는 자원을 뜻하는 클래스를 작성한다면 AutoCloseable
을 반드시 구현하길 바란다.
public class FirstError extends RuntimeException{
}
public class SecondError extends RuntimeException{
}
public class MyResource implements AutoCloseable{
public void doSomething(){
System.out.println("Do something");
throw new FirstError();
}
@Override
public void close() throws Exception {
System.out.println("Close My Resource");
throw new SecondError();
}
}
public class AppRunner {
public static void main(String[] args) throws Exception {
/**
* try-with-resource
*/
try(MyResource myResource=new MyResource()) {
myResource.doSomething();
}
}
try-with-resources
가 위에서의 try-finally
를 사용한 것보다 코드가 훨씬 간결하고 일기 수월할 뿐만 아니라 문제를 진단하기도 훨씬 좋다.
Do something
Close My Resource
Exception in thread "main" Item09.FirstError
at Item09.MyResource.doSomething(MyResource.java:6)
at Item09.AppRunner.main(AppRunner.java:28)
Suppressed: Item09.SecondError
at Item09.MyResource.close(MyResource.java:12)
at Item09.AppRunner.main(AppRunner.java:27)
위의 try-finally
코드에서 발생한 예외 상황과 똑같은 예외 상황이다. 즉,myResource.doSomething()
에서 FirstError
와 myResource.close()
에서 SecondError
가 발생한 상황이다. 이 경우 출력 결과를 보면 close()
에서 발생한 예외는 숨겨지고 doSomething
에서 발생한 첫 번째 예외가 기록된 것을 알 수 있다. SecondError
의 경우 숨겨지는데 숨겨진다고 버려지는게 아니라, 스택 추적 내역에 숨겨졌다(suppressed)
라는 꼬리표를 달고 출력된다. 또한 자원을 여러 개 쓰더라도 코드가 지저분해지지 않는다.
public class AppRunner {
public static void main(String[] args) throws Exception {
/**
* try-with-resource
*/
try(MyResource myResource1=new MyResource();
MyResource myResource2=new MyResource()) {
myResource1.doSomething();
myResource2.doSomething();
}
}
}
보통의 try-finally
에서처럼 try-with-resources
에서도 catch
절을 쓸 수 있다. catch
절 덕분에 try
문을 더 중첩하지 않고도 다수의 예외를 처리할 수 있다.