이펙티브 자바 3판 - 아이템 9. try-finally 보다 try-with-resources를 사용하라.

김대협·2023년 2월 28일
0

Effective Java 3rd

목록 보기
9/9

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

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 Exceptioncatch 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 실행 예제

try-with-resources


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가지 내용을 모두 확인할 수 있다.
예외 처리는 마지막에 발생된 내용보단 우선 최초 발생이 단서가 되는 경우가 즐비하다.
예외 발생을 누적하여 보여주는 것은 큰 장점 중 하나이다.

Java Puzzlers 문제


책에서 지나가는 말로 언급되는 자바 퍼즐러 이야기가 있다.
우리가 위에서 잘못된 회수 방식을 아래와 같이 고친다면 결과는 어떻게 될까?

// 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를 적극 활용하여 코드를 작성할 수 있도록 하자.


© 2023.1 Written by Boseong Kim.
profile
기록하는 개발자

0개의 댓글