Java

try-with-resources 는 왜 사용해야할까?

자바생 2023. 3. 28. 14:55
728x90

글을 쓰게 된 이유

 

DB 접근 강의를 들으면서 네오가 키워드로 try-with-resources, AutoCloseable 를 던져주었다.

그래서 평상시에 try-with-resources를 그냥 사용하기만 했는데 이 기회에 좀 깊게 공부해보려고 한다.

 

try-with-resources는 무엇인가요?

 

try-catch-finally와 다르게 사용한 리소스에 대해서 명시적으로 close 하지 않아도 자동으로 close 해주는 것을 말합니다.

이펙티브 자바 아이템9 에서도 “try-finally 보다는 try-with-resources를 사용하라”는 말이 있습니다.

왜 try-with-resources를 사용해야 하는지 이번 글을 읽으면 깨달을 수 있습니다.

 

 

try-with-resources는 왜 사용해야하나요?

 

먼저 try-finally을 먼저 보도록 하겠습니다.

아래의 코드는 oracle에 나온 예시 코드입니다.

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    try {
        return br.readLine();
    } finally {
        br.close();
        fr.close();
    }
}

정상적인 흐름이라면

FileReader 생성 → BufferedReader 생성 → try 문 실행 → BufferReader 닫힘 → FileReader 닫힘 과 같은 과정이 일어납니다.

 

“이렇게 close 제대로 해주면 문제없는 거 아니야?”라고 생각하실 수 있습니다.

그러나 이펙티브 자바에서도 실력 좋은 개발자도 close에 대한 처리를 가끔 잊어버릴 때가 있다고 합니다.

또한, close를 하지 않거나, 못하면서 생기는 2가지 문제가 있습니다.

 

첫 번째로, 리소스 누수입니다.

 

try 문에서 예외 발생 후, br.close()에서 예외가 발생한다면 fr.close()가 제대로 실행되지 않습니다.

그렇다면 GC가 리소스를 회수할 때, FileReader는 아직 close가 되지 않았기 때문에 사용되고 있다고 생각할 수 있습니다.

그래서 GC가 리소스를 회수하지 않게 되고, 이를 리소스 누수가 발생했다고 합니다.

 

두 번째로, exception suppressed에 의한 스택 추적 내역이 사라집니다.

try block 예외 → finally block 예외를 통해서 try block 예외가 finally block 예외에 의해 잡아먹히기 때문에 스택 추적 내역에는 첫 번째 예외에 관한 정보가 남지 않게 되면서 디버깅이 어렵게 됩니다.

 

이를 방지하기 위해 try-with-resources 문을 사용해야 합니다.

 

try-with-resources를 어떻게 사용하나요?

 

try-with-resources는 Closeable, AutoCloseable을 구현하는 객체들이 사용할 수 있는 것으로, 사용한 리소스들을 자동으로 반납해 줍니다.

즉, Closeable, AutoCloseable 인터페이스를 구현하는 모든 객체를 ‘리소스’의 대상이 되는 것이지요.

try-with-resources를 사용하기 위해서는 Closeable이나 AutoCloseable 인터페이스를 구현해야 합니다.

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

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

Closeable 은 AutoCloseable을 상속받고 있습니다. 또한, Closeable은 IOException을 던지고, AutoCloseable은 Exception을 던집니다.

 

이렇게 예외가 확장된 것은 더욱 범용적으로 사용하기 위함이라고 생각합니다.

이 생각이 적합하다고 생각한 이유는 기존에 Closeable 패키지는 java.io였지만, AutoCloseable 패키지는 필수적인 클래스를 포함하는 java.lang에 존재하게 됩니다.

 

그래서 Closeable은 1.5, AutoCloseable은 1.7에 나오게 됐습니다.

 

try-finally Test

 

@Test
void test_exception_suppressed() throws Exception {

    Assertions.assertThrows(
            IllegalArgumentException.class,
            () -> exceptionSuppressed()
    );

}

void exceptionSuppressed() throws Exception {
    FileReader fr = new FileReader("src/main/resources/sql/schema.sql");
    BufferedReader br = new BufferedReader(fr);
    try {
        br.readLine();
        throw new IllegalStateException(); //exception
    } finally {
        br.close();
        throwException(); //exception 
        fr.close();
    }
}

void throwException() {
    throw new IllegalArgumentException();
}

try 문에서 예외가 발생하고, finally에서 예외가 발생했기 때문에 fr.close() 메서드가 호출되지 않아 리소스 누수가 생깁니다.

 

이때, try에서 던져진 예외는 suppressed 되고, finally에서 던져진 예외를 메서드가 던지게 됩니다.

 

try-with-resources 예시 코드

 

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

FileReader, BufferedReader는 AutoCloseable 인터페이스를 구현하고 있기 때문에 ‘리소스’의 대상이 됩니다. 또한, try 문이 정상적으로 완료됐는지에 상관없이 close 메서드를 호출합니다.

 

테스트를 통해 확인해 보겠습니다.

@Test
void test_try_with_resources() throws Exception {
    Assertions.assertThrows(
            IllegalArgumentException.class,
            this::tryWithResources
    );
}

void tryWithResources() throws Exception {
    try (FileReader fr = new FileReader("src/main/resources/sql/schema.sql");
         B b = new B();
         A a = new A(); //예외 발생
         BufferedReader br = new BufferedReader(fr)) {

        System.out.println("여기까지 옴");
        br.readLine();
        throw new IllegalArgumentException(); //예외 발생
    }
}

static class A implements AutoCloseable {

    @Override
    public void close() throws Exception {
        throw new IllegalStateException();
    }
}

static class B implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("예외 터져도 실행됨");
    }
}

위 코드에는 try-with-resources와 try block 안 모두 예외가 발생한 상황입니다.

 

try block 안이 실행되고 close 되면서 exception 이 발생합니다.

try-with-resources는 생성할 때의 리소스 반대로 close 메서드를 출력하기 때문에

br → a → b → fr 순서로 close 메서드를 호출하게 됩니다.

 

아래 로그를 보면 신기하게 a에서 예외가 발생해도 b의 close가 제대로 실행됨을 알 수 있습니다.

 

그 이유는 try block에서 발생한 예외를 던지고, try-with-resources에서 발생한 예외는 suppressed 되기 때문입니다.

그래서 try-finally 사용과 반대로 try 문의 예외가 먹히지 않아서 스택 추적 예외에서 사라지지 않기 때문에 디버깅이 전보다 나아집니다.

 

만약에 try-with-resources 문에서 바로 예외가 발생하면 어떻게 될까요?

static class A implements AutoCloseable {

		// 이 부분만 다름
    public A() {
        throw new IllegalStateException();
    }

    @Override
    public void close() throws Exception {
        throw new IllegalStateException();
    }
}

@Test
void test_try_with_resources() throws Exception {
    Assertions.assertThrows(
            IllegalStateException.class,
            this::tryWithResources
    );
}

위 예시 코드 대부분 비슷하고, A를 생성할 때 바로 예외를 던져보도록 하겠습니다.

실행을 시키면 당연히 IllegalStateException 가 발생하게 됩니다.

 

 

신기한 것은 B의 close 메서드가 실행됩니다.

예외가 발생하기 전까지의 try-with-resources의 close 메서드를 호출해 줍니다.

 

이러한 점 때문에 리소스 누수가 생길 수 있는 try-catch-finally 보다 try-with-resources를 사용하라고 합니다.

 

 

REFERENCES

oracle 문서

728x90