JDK 7부터 try-finally는 더 이상 최선의 방법이 아니다.
try-catch-finally, try-finally 구문은 try 블럭에서 일어난 예외처리를 진행하는 문법이다.
1개의 try는 반드시 catch 또는 finally 가져야 구문이 완성된다.
catch 블럭은 여러 종류를 만들 수 있고, catch 블럭의 순서도 중요하다.
// 0으로 나눌 경우 ArithmeticException이 발생한다.
public static void divisionTest() {
try {
double result = 100 / 0;
} catch ( ArithmeticException e ) {
System.out.println( "Entered ArithmeticException routine" );
e.printStackTrace();
} catch ( Exception e ) {
System.out.println( "Entered Exception routine" );
e.printStackTrace();
}
}
실행 결과
Entered ArithmeticException routine
java.lang.ArithmeticException: / by zero
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.divisionTest(TryCatchTests.java:7)
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.main(TryCatchTests.java:18)
위에 예제 코드에서 catch Exception
과 catch ArithmeticeException
의 순서가 바뀌는 경우
컴파일러는 실행을 허용해주지 않는다.
또한 두개 이상의 자원을 회수하는 try-finally 블럭은 코드가 복잡하고 지저분해진다.
// 2개 이상의 자원을 회수하는 try-finally는 코드가 복잡하고 지저분하다.
static void rightCopy( String src, String dest ) throws IOException {
InputStream in = new FileInputStream( src );
try {
OutputStream out = new FileOutputStream( dest );
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();
}
}
정상적인 코드지만, 코드를 이해하기 복잡하고 지저분하다.
이러한 문제로 가독성을 좋게 만들어 보기 위해 아래와 같이 코드를 변경하는 경우를 종종 볼 수 있다.
// 잘못된 회수 방식 (in.close 회수 당시 에러 발생하면 Leak 발생)
static void wrongCopy( String src, String dest ) throws IOException {
InputStream in = new FileInputStream( src );
OutputStream out = new FileOutputStream( dest );
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()
실행 시 예외가 발생하면 문제가 발생한다.
finally에서 두 개의 close를 할떄 예외가 발생하면 밑에 자원이 회수되지 않는다.
이는 Leak을 유발하는 위험한 코드가 된다.
finally 구문은 try 과정에서 어떠한 Exception
이 발생하더라도 무조건 실행되는 구문이다.
public static void finallyTest() {
try {
throw new Exception( "throw Exception" );
} catch ( ArithmeticException e ) {
System.out.println( "Entered ArithmeticException routine" );
e.printStackTrace();
} catch ( Exception e ) {
System.out.println( "Entered Exception routine" );
e.printStackTrace();
} finally {
System.out.println( "Entered finally routine" );
}
}
실행 결과
Entered Exception routine
Entered finally routine
java.lang.Exception: throw Exception
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.finallyTest(TryCatchTests.java:19)
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.main(TryCatchTests.java:33)
finally 실행 예제
JDK 7부터 try-with-resoureces가 등장했다, 이는 아래와 같이 여러가지 장점을 갖고 있다.
// try-with-resources 를 활용한 간결하고 안전한 코드 생성
static void copy( String src, String dest ) throws IOException {
try (InputStream in = new FileInputStream( src );
OutputStream out = new FileOutputStream( dest )) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ( ( n = in.read( buf ) ) >= 0 ) {
out.write( buf, 0, n );
}
}
}
try-with-resources 를 활용한 간결하고 안전한 코드 생성
별도의 catch
, finally
를 명시하지 않아도 사용할 수 있고 중첩 예외문을 간결하게 표기할 수 있다.
또한 AutoCloseable
인터페이스를 통한 자원 회수를 안정적으로 진행하여 준다.
또한 예외 발생을 누적하여 명시적으로 표기할 수 있는 장점이 있다.
// BufferedReader를 상속받는 강제 Exception 발생 코드
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 IOException();
}
@Override
public void close() throws IOException {
throw new UnsupportedOperationException();
}
}
// try-finally를 이용한 Exception 강제 발생
public static void badBufferedByTryFinallyTest( String path ) throws IOException {
BufferedReader br = new BadBufferedReader( new FileReader( path ) );
try {
String str = br.readLine();
System.out.println( str );
} finally {
br.close();
}
}
실행 결과
Exception in thread "main" java.lang.UnsupportedOperationException
at com.ntigo.study.effectivejava3rd.item09.BadBufferedReader.close(BadBufferedReader.java:23)
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.badBufferedByTryFinallyTest(TryCatchTests.java:42)
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.main(TryCatchTests.java:56)
try-finally를 이용한 강제 예외 발생 예제 코드
우리는 BadBufferedReader
를 통해 2가지의 예외를 발생시켰지만, 나중에 발생된 UnsupportedOperationException
의 내용만 확인할 수 있지만, try-with-resources를 활용할 경우
public static void badBufferedByTryWithResourcesTest( String path ) throws IOException {
try ( BufferedReader br = new BadBufferedReader( new FileReader( path ) ) ) {
String str = br.readLine();
System.out.println( str );
}
}
실행 결과
Exception in thread "main" java.io.IOException
at com.ntigo.study.effectivejava3rd.item09.BadBufferedReader.readLine(BadBufferedReader.java:18)
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.badBufferedByTryWithResourcesTest(TryCatchTests.java:49)
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.main(TryCatchTests.java:58)
Suppressed: java.lang.UnsupportedOperationException
at com.ntigo.study.effectivejava3rd.item09.BadBufferedReader.close(BadBufferedReader.java:23)
at com.ntigo.study.effectivejava3rd.item09.TryCatchTests.badBufferedByTryWithResourcesTest(TryCatchTests.java:48)
... 1 more
try-with-resources를 이용한 강제 예외 발생 예제 코드
발생된 예외 2가지 내용을 모두 확인할 수 있다.
예외 처리는 마지막에 발생된 내용보단 우선 최초 발생이 단서가 되는 경우가 즐비하다.
예외 발생을 누적하여 보여주는 것은 큰 장점 중 하나이다.
책에서 지나가는 말로 언급되는 자바 퍼즐러
이야기가 있다.
우리가 위에서 잘못된 회수 방식을 아래와 같이 고친다면 결과는 어떻게 될까?
// puzzlers 예제의 회수 방식
static void puzzlersCopy( String src, String dest ) throws IOException {
InputStream in = new FileInputStream( src );
OutputStream out = new FileOutputStream( dest );
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ( ( n = in.read( buf ) ) >= 0 ) {
out.write( buf, 0, n );
}
} finally {
try {
in.close();
} catch ( IOException e ) {
}
try {
out.close();
} catch ( IOException e ) {
}
}
}
Java Puzzlers와 비슷한 유형의 예제 코드
위 코드는 정상적으로 동작할까?
정답은 아니다
in.close()를 호출하는 당시 IOException이 아닌 케이스가 나오는 경우
동일한 누수를 경험할 수 있다. try-with-resources를 적극 활용하여 코드를 작성할 수 있도록 하자.