try-with-resources

최창효·2024년 7월 20일
post-thumbnail

자원 반납

자바에서 사용한 자원을 반납할 때는 try문의 finally에서 수동으로 직접 자원을 반납하는 방법 외에 try-with-resources구문을 사용하는 방법이 있습니다.

try-with-resources의 자원 반납

엄밀히 말해서 try-with-resources 자체가 사용한 자원을 알아서 반납해 주는건 아닙니다. try-wth-resources는 AutoCloseable 인터페이스의 정의된 close메서드를 실행해 줄 뿐입니다.

FileInputStream의 close()

FileInputStream을 예시로 살펴보겠습니다. FileInputStream은 InputStream인터페이스를 구현한 구현체입니다.

InputStream인터페이스는 Closeable인터페이스를 상속받고 있습니다.

Closeable은 java.io패키지에 존재하는 인터페이스로 AutoCloseable을 상속받고 있습니다. Closeable은 IO와 관련된 AutoCloseable로 Exception이 아닌 IOException을 던질 것을 지시하고 있습니다.

FileInputStream의 close는 다음과 같이 오버라이딩 되어 있습니다.

  • channel을 fc라는 변수에 할당한 뒤 직접 close()를 실행해주고 있습니다.

try-with-resources의 컴파일 시점 모습

try-with-resources를 활용하는 간단한 Use코드를 만들어 보겠습니다.

public class Use {

    public void main() throws IOException {
        try(InputStream is = new FileInputStream("file")) {
            System.out.println("Hello World");
        }
    }
}

decompiled된 Use.class파일을 살펴보면 다음과 같습니다.

public class Use {
    public Use() {
    }

    public void main() throws IOException {
        InputStream is = new FileInputStream("file");

        try {
            System.out.println("Hello World");
        } catch (Throwable var5) {
            try {
                is.close();
            } catch (Throwable var4) {
                var5.addSuppressed(var4);
            }

            throw var5;
        }

        is.close();
    }
}
  • is.close()로 close메서드를 호출해 자원을 반납하고 있습니다.

어차피 컴파일 시점에 직접 close()해주고 있기 때문에 try-with-resources는 아래의 try-finally와 크게 다를게 없는 걸까요?

public class Use {

    public void main() throws IOException {
        InputStream is = new FileInputStream("file");
        try {
            System.out.println("Hello World");
        } finally {
            is.close();
        }
    }
}

try-with-resources의 장점

try-finally는 예상치 못하게 자원을 반납하지 못할 수 있다

CloseableClass

public class CloseableClass implements Closeable {
    private String name;

    public CloseableClass(String name) {
        this.name = name;
    }

    @Override
    public void close() throws IOException {
        System.out.printf("close method in %s class", name);

    }
}

CloseableButFailClass

public class CloseableButFailClass implements Closeable {
    private String name;

    public CloseableButFailClass(String name) {
        this.name = name;
    }


    @Override
    public void close() throws IOException {
        System.out.printf("close method in %s class", name);    
        throw new IOException("close fail");
    }
}
  • close메서드 안에서 예외가 발생합니다.

실행

@Test
void closeLeakTest() throws IOException {
    CloseableClass closeableClass = new CloseableClass("A");
    CloseableButFailClass closeableButFailClass = new CloseableButFailClass("B");
    try {
        System.out.println("do works");
    } finally {
        closeableButFailClass.close();
        closeableClass.close();
    }
}
  • closeableClass와 closeableButFailClass를 활용하고 있습니다. 우리는 finally에서 두 자원을 반납하고 있으며 finally에서 closeableClass.close()를 실행했기 때문에 무슨 일이 있어도 closeableClass의 자원이 반납될 것을 기대하고 있습니다. (왜냐하면 'finally에 선언한 코드는 반드시 실행된다'라고 생각하는 경우가 많기 때문입니다)
  • 하지만 closeableClass.close()를 실행하기 전에 closeableButFailClass.close()가 실행되고, 해당 메서드에서는 예외(IOException("close fail"))가 발생합니다.
  • 그 결과 closeableClass.close()가 수행되지 않습니다.

try-finally는 예외 추적이 어렵다

우리는 보통 try블록에서 메인 로직을 실행하고, 거기서 발생한 예외를 통해 문제를 파악합니다.

@Test
void errorTraceTest() {
    try {
        throw new RuntimeException("main error");
    }catch (Exception e){
        throw e;
    } 
}

만약 finally블록에서 예외가 발생하면 다음과 같이 finally의 예외만 출력됩니다.

@Test
void errorTraceTest() {
    try {
        throw new RuntimeException("main error");
    }catch (Exception e){
        throw e;
    } finally {
    	// finally에서 로직(ex-자원 반납)을 진행하다가 에러가 발생
        throw new RuntimeException("finally error");
    }
}

자원을 반납하다 발생한 예외도 중요합니다. 하지만 보다 근본적으로 우리는 try블록의 메인 로직에서 발생한 예외가 무엇인지를 알아야 합니다.
하지만 finally에서 예외가 발생하면 메인 예외는 출력되지 않아 이를 확인할 수 없습니다.

try-with-resources는 이러한 문제를 해결해 줍니다.

MyClass

public class MyClass implements Closeable {

    public void mainLogic() {
        throw new RuntimeException("main error");
    }

    @Override
    public void close() {
        throw new RuntimeException("close error");
    }
}
  • mainLogic메서드에서도 예외가, 그리고 close메서드에서도 예외가 발생합니다.

Use

public class Use {

    public static void main() {
        try(MyClass myClass = new MyClass()) {
            myClass.mainLogic();
        }
    }

}
@Test
void errorTraceTest() {
    Use.main();
}

  • 두 예외(main error와 close error)가 모두 확인됩니다.

try-with-resources를 사용한 Use클래스의 decompiled된 Use.class파일을 살펴봅시다.

public class Use {
    public Use() {
    }

    public static void main() {
        MyClass cc = new MyClass();

        try {
            cc.mainLogic();
        } catch (Throwable var4) {
            try {
                cc.close();
            } catch (Throwable var3) {
                var4.addSuppressed(var3);
            }

            throw var4;
        }

        cc.close();
    }
}
  • addSuppressed라는 메서드를 덕분에 예외가 함께 보여지게 됩니다.

try-with-resources대신 try-finally에서 addSuppressed를 적절히 활용하면 그때는 똑같이 예외를 추적해 주는거 아니냐? 라고 물으면 틀린 말은 아닐 겁니다. 하지만 어차피 똑같다면 이런 번거로운 코드를 직접 설계하기 보다 간편한 try-with-resources를 사용하는게 더 합리적인 선택일 겁니다.

References

profile
기록하고 정리하는 걸 좋아하는 백엔드 개발자입니다.

0개의 댓글