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

    들어가기 전

    이 글은 인프런 백기선님의 이펙티브 자바를 공부하며 작성한 글입니다. 


    핵심정리

    • try-finally는 더 이상 최선의 방법이 아니다. (자바 7부터)
    • try-with-resources를 사용하라
      • resources는 closable 인터페이스가 구현된 경우 자동으로 닫아준다. 
      • 가독성이 증가한다.
      • 만들어지는 예외 정보도 훨씬 유용하다. 

    try~finally에서 finally절은 항상 실행되기 때문에 그 안에서 자원을 반납했다. 좋은 방법이지만, 자바 8 이상에서는 Try With Resources가 최선의 방법이 된다. 


    Try ~ finally 코드 살펴보기 : 코드 9-1

    기존에는 try - finally 구문을 이용해서 자원을 회수해왔다. 현재 수준에서 이 코드는 문제가 없다. 예외가 발생하면 finally 절이 실행되고, finally 절에서 자동적으로 자원이 회수되기 때문에 자원의 leak가 발생하지 않게 된다. 

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

    Try ~ finally 코드 살펴보기 : 코드 9-2 (자원이 많아지는 경우)

    간단히 정리해보면 다음과 같다

    • 가독성이 나빠진다. 물론 정상 동작한다.
    • 잘못된 코드 작성으로 자원 누수가 심해진다. 이 경우는 정상동작하지 않는다.

    그렇지만 회수해야 할 자원이 많아지면 다음과 같이 nested 된 try - finally 구문이 생기면서 코드가 매우 복잡해진다. 그래서 코드의 가독성이 매우 나빠지는데, 함께 개발하는데 있어서 이런 부분은 고욕이 아닐 수 없다. 

    public class Copy {
    
        private static final int BUFFER_SIZE = 8 * 1024;
        
        // 코드 9-2 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! (p47)
        static void copy(String src, String dst) throws IOException {
            FileInputStream in = new FileInputStream(src);
            try {
                FileOutputStream 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();
            }
        }
    }

    만약 이 부분을 해결하기 위해서 nested된 try ~ finally를 아래와 같이 해체한다면 한 가지 문제점이 발생한다. Resource의 leak가 발생할 수 있다는 점이다. 

    • 에러가 발생해서 finally 절에서 실행되고 있을 때, out.close()가 처음으로 실행된다.
    • out.close()에서 에러가 발생한다면 finally 절에서도 에러가 던져지기 때문에 in.close()가 실행되지 않게 되고 여기서 resource leak가 발생하게 되는 것이다. 
    static void copyLeak(String src, String dst) throws IOException {
        FileInputStream in = new FileInputStream(src);
        FileOutputStream 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();
            // 이건 실행 안됨.
            in.close();
        }
    }

     

    Try-With-Resources 코드 살펴보기 : 코드 9-3 (자원이 많아지는 경우)

    try-wiht-resources에서 resource에 선언될 객체는 반드시 AutoCloseable 인터페이스를 구현한 객체여야 한다. 그 자원들은 try () 절 안에 넣어주면 해당 절에서 resource를 사용하고, 끝날 때 자동으로 resources를 close 해준다. 

    Try - With - Resources의 장점 중 하나는 아래 코드에서 볼 수 있듯이 가독성이 좋아진다는 점이다. 코드 9-2에서는 굉장히 복잡한 코드였는데, 이곳에서는 읽기 편해진 것을 알 수 있다. 

     

    public class Copy {
    
        private static final int BUFFER_SIZE = 8 * 1024;
    
        // 코드 9-3 : try-with-resources - 자원을 회수하는 최선책! (p48)
        static void copy(String src, String dst) throws IOException {
            try (FileInputStream in = new FileInputStream(src);
                FileOutputStream out = new FileOutputStream(dst)) {
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            }
        }
    }
    

     


    Try - With - Resources 장점 : 예외를 잡아먹지 않는다. 

    Try - Finally로 작성할 경우 첫번째 발생한 예외는 보여주지 않고, 마지막 예외만 보여지는 형태가 된다. 그런데 실제로는 첫번째 발생한 예외가 중요하다. 첫번째 발생한 예외가 후속 예외를 발생시키기 때문에 어떤 지점에서 예외가 발생되었는지를 명확하게 파악해야하기 때문이다. 아래 클래스를 예로 들어보자.

    • BadBufferReader는 readLine() 할 때는 CharConversionException, close() 할 때는 StreamCorruptedException()을 발생시킨다.
    • Topline은 메서드를 실행할 때 try안에서 readline()을 사용한다. 이 때, CharConversionException이 발생한다.
    • Exception이 발생해서 try는 종료되고 finally절이 실행된다. 이 때, br.close()가 실행되는데 close()가 실행되며 StreamCorruptedException이 발생한다. 
    public class TopLine {
    
        // try ~ finally는 한 가지 에러만 보여준다. (에러가 2개가 발생해도)
        static String firstLineOfFile(String path) throws IOException {
            BufferedReader br = new BadBufferReader(new FileReader(path));
            try {
                return br.readLine();
            }finally {
                br.close();
            }
        }
    
        public static void main(String[] args) throws IOException {
            System.out.println(firstLineOfFile("build.gradle"));
        }
    }
    
    // BadBufferReader.java
    public class BadBufferReader extends BufferedReader {
        
        ...
    
        @Override
        public String readLine() throws IOException {
            throw new CharConversionException();
        }
    
        @Override
        public void close() throws IOException {
            throw new StreamCorruptedException();
        }
    }

    위의 방식대로 동작하게 한 후 실제로 실행해보면 실행 결과는 다음과 같다.

    • CharConversionException, StreamCorruptedException 둘다 발생할 것을 기대했다.
    • 실제로는 마지막에 발생한 StreamCorruptedException만 발생한다. 

    try ~ finally를 할 때는 이런 동작을 하기 때문에 실제로 디버깅 할 때 효율적이지 못하다. 반면, 이 부분을 try - resources로 바꾸게 되면 발생한 모든 에러를 확인할 수 있게 된다. 

    static String firstLineOfFileWithResources(String path) throws IOException {
        try(BufferedReader br = new BadBufferReader(new FileReader(path));) {
            return br.readLine();
        }
    }

    위와 같이 코드를 수정한 후, 실행해보면 다음과 같은 결과를 볼 수 있다. 

    • CharConversionException이 있는 것을 확인.
    • Suppressed: StreamCorruptedException이 있는 것을 확인. 

     


    Try - with - resource를 쓰자

    try - with - resources는 다음 두 장점을 가진다.

    • 자원 회수 하는 관점에서 try ~ finally 대비 가독성을 준다. 
    • 발생하는 모든 에러를 suppressed 형태로 보여준다. 

    또한 try ~ catch ~ finally를 사용하던 것처럼 try - with - resources에 catch문을 함께 사용할 수 있다. 따라서 try ~ finally로 작성된 코드가 있다면, try ~ with ~ resources를 사용하도록 수정하는 것이 좋겠다.

    댓글

    Designed by JB FACTORY