[교재 EffectiveJava] 아이템 9. try-finally 보다 try-with-resouces를 사용하라.

반응형
728x90
반응형

Before. try~finally

자바 라이브러리에는 close 메서드를 호출하여 직접 닫아줘야하는 자원이 많다. 전통적으로 자원을 제대로 닫힘을 보장하는 수단으로 try~finally가 쓰였다.

 

더이상 자원을 회수하는 최선의 방책이 아니다.

public class TopLine {
    // 코드 9-1 try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! (47쪽)
    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }

    public static void main(String[] args) throws IOException {
        String path = args[0];
        System.out.println(firstLineOfFile(path));
    }
}

 

자원을 하나 더 사용한다면?

코드가 지저분해진다.
public class Copy {
    private static final int BUFFER_SIZE = 8 * 1024;

    // 코드 9-2 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! (47쪽)
    static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        try {
            OutputStream out = new FileOutputStream(dst);
            
            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();
            out.close();
        }
    }

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

 

 

문제점

try~finally문을 제대로 사용한 앞의 두 코드에 결점이 존재한다. 예외는 try 블록와 finally 블록 모두에서 발생할 수 있다. 만약 기기에 물리적인 문제가 생긴다면 firstLineOfFile 메서드 안의 readLine 메서드가 예외를 던지고, 같은 이유로 close 메서드도 실패할 것이다. 이런 상황이라면 두번째 예외가 첫번째 예외를 집어삼켜 버린다. 그러면 스택 추적 내역에 첫번째 예외에 관한 정보는 남지 않게 되어, 실제 시스템에서의 디버깅을 어렵게한다. 

 

두번째 예외가 첫번째 예외를 삼켜버리는 상황

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

 

클라이언트 코드
public class TopLine {
    // 코드 9-1 try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! (47쪽)
    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            // readLine() 오류 발생 -> finally 실행 -> close() 오류 발생
            // readLine() 오류를 삼켜버려서, 가장 마지막에 발생한 close() 오류'만' 보인다.
            // 디버깅때는 가장 처음에 발생한 예외가 중요한데, 이는 오류 찾기를 어렵게만든다.
            br.close();
        }
    }

    public static void main(String[] args) throws IOException {
        String path = args[0];
        System.out.println(firstLineOfFile(path));
    }
}
상황
1) br.readLine() 오류 발생 
2) finally 문 실행
3) br.close() 오류 발생
1)번의 오류는 무시되고, 3)번의 오류만 보이게된다.

 

try~finally

아래와 같은 방법으로 해결할 수는 있다. 그치만 코드가 여전히 복잡하다.

public class Copy {
    private static final int BUFFER_SIZE = 8 * 1024;

    // 코드 9-2 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! (47쪽)
    static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        try {
            OutputStream 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);
    }
}

 

 

with~resources

해당 자원이 AutoCloseable 인터페이스를 구현해야한다.

 

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

 

닫아야하는 자원을 뜻하는 클래슬르 작성한다면 반드시 AutoCloseable을 구현해야한다.

 

try~with~resources 예제

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

    public static void main(String[] args) throws IOException {
        System.out.println(firstLineOfFile("pom.xml"));
    }
}

try~with~resources 버전이 짧고 읽기 수월할 뿐 아니라 문제를 진단하기도 훨씬 좋다. 만약 readLine과 close 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다. 

public class BufferedReader extends Reader {
...
public abstract class Reader implements Readable, Closeable {
...
public interface Closeable extends AutoCloseable {

참고로, BufferedReader 클래스는 AutoCloseable를 구현하고 있기 때문에 try~with~resources문을 사용할 수 있다.

 

try~finally 사용 때의 두번째 예외가 첫번째 예외를 삼켜버리는 상황이 발생하지 않는다.

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

 

클라이언트 코드
public class TopLine {
    // 코드 9-1 try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! (47쪽)
    static String firstLineOfFile(String path) throws IOException {
        // readLine() 오류 발생 -> finally 실행 -> close() 오류 발생 일때
        // 이때는 가장 먼저 발생한 readLine() 오류 발생 후, close() 오류도 보인다.
        // 바이트코드 보면 var9.addSuppressed(var6); 이런식으로 add 해주기 때문임
        try(BufferedReader br = new BadBufferedReader(new FileReader(path))) {
            return br.readLine();
        }
    }

    public static void main(String[] args) throws IOException {
        System.out.println(firstLineOfFile("pom.xml"));
    }
}
상황
1) br.readLine() 오류 발생 
2) finally 문 실행
3) br.close() 오류 발생
1)번의 오류와 3)번의 오류 모두 보인다.

 

IntelliJ 내의 .class 코드를 보자.
public class TopLine {
    public TopLine() {
    }

    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BadBufferedReader(new FileReader(path));

        String var2;
        try {
            var2 = br.readLine();
        } catch (Throwable var5) {
            try {
                br.close();
            } catch (Throwable var4) {
                var5.addSuppressed(var4);
            }

            throw var5;
        }

        br.close();
        return var2;
    }

    public static void main(String[] args) throws IOException {
        System.out.println(firstLineOfFile("pom.xml"));
    }
}

1) addSuppressed()

addSuppressed() 메서드를 사용해서 첫번째 예외에 이어 두번째 예외를 추가하여, 모든 오류를 디버깅할 수 있도록 한다.

 var5.addSuppressed(var4);

 

Throwable 클래스
public class Throwable implements Serializable {
    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -3042686055658047285L;

    ...
    
    private List<Throwable> suppressedExceptions = SUPPRESSED_SENTINEL;

    ...
    
    /**
     * A shared value for an empty stack.
     */
    private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
    
    public final synchronized void addSuppressed(Throwable exception) {
        if (exception == this)
            throw new IllegalArgumentException(SELF_SUPPRESSION_MESSAGE, exception);

        if (exception == null)
            throw new NullPointerException(NULL_CAUSE_MESSAGE);

        if (suppressedExceptions == null) // Suppressed exceptions not recorded
            return;

        if (suppressedExceptions == SUPPRESSED_SENTINEL)
            suppressedExceptions = new ArrayList<>(1);

        suppressedExceptions.add(exception);
    }
    
    ...
}

List<Throwable> 타입인 suppressedException에 exception을 add() 하고있다.

 

 

반응형

Designed by JB FACTORY