글을 쓰게 된 이유
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
'Java' 카테고리의 다른 글
파라미터에 Optional은 왜 안티패턴? (0) | 2023.03.25 |
---|---|
unmodifiableList & copyOf & 방어적 복사 (0) | 2023.02.21 |
Generic & Wildcard (0) | 2022.12.29 |
Generic in Java (2) | 2022.12.17 |
JDK ? JRE ? (0) | 2022.09.15 |