도입
최근에 이펙티브 자바 책을 읽고있다. 책의 객체 생성과 파괴 파트에서 try-with-resources 관련 내용을 볼 수 있었다. 저자는 try-finally 보다는 try-with-resources를 사용을 권장한다.
InputStream, OutputStream, java.sql.Connection 등 close 메서드를 직접 닫아줘야하는 자원에 대해서 이러한 내용을 제시하였다.
저자는 다양한 자원들이 finalizer, cleaner를 안전망으로 활용하고 있으나, 이에 대한 문제점을 제기한다.
- 닫아야할 자원이 여러 개인 경우 try-finally는 중첩으로 인해 지저분한 구조를 갖게 된다.
- finally 블록에서도 예외가 발생할 수 있으므로 finally 블록에서 발생한 예외가 다른 예외를 삼켜버려 close 메서드가 실패할 수도 있다.
이를 해결하기 위해 try-with-resources 사용을 권장한다.
finalizer, cleaner에 대한 문제점도 저자는 try-with-resources 앞쪽 단락에서 설명하였다. 핵심만 언급하자면
- 객체가 가비지 컬렉터에 의해 회수될 때 호출되는데, 가비지 컬렉션의 호출 시점을 예측할 수 없으므로, 즉시 수행된다는 보장이 없다.
- 가비지 컬렉션 과정에서 특별히 처리해야 하므로 심각한 성능 문제를 동반한다. (실제 실험 결과도 제시하였다.)
⇒ 이에 대한 해결 방법으로 try with resources를 제시하였다. 또는 자원 사용자가 close 메소드를 직접 잘 호출해주어야한다.
Resource 예외 처리를 해야하는 이유
DB, Network, File와 같은 resource의 경우 자바 외부에 위치한다.
이러한 프로세스 외부에 위치한 데이터에 어떤 동작을 수행할 때에도 예외가 발생할 수 있다.
resource들에 접근해서 사용하다 예외가 발생하는 경우 아래와 같은 문제로 이어질 수 있다.
- 리소스 누수 : 자원이 제대로 닫히지 않으면, 시스템 리소스를 계속 점유하여 시스템의 성능 저하 또는 시스템의 오류를 초래할 수 있다.
- 데이터 손실 : 데이터가 제대로 저장되지 않거나 손상될 수 있다.
- 에러 추적의 어려움 : 외부 resource에서 발생한 문제에 대해 문제점을 파악하기 어려워질 수 있다.
Try - Finally
try-finally는 예외 처리 시 사용되며 예외 여부와 관계 없이 반드시 실행되어야 하는 코드를 관리하는데 사용된다.
주로 자원을 해제하거나 정리할 때 필요한 로직을 수행할 때 쓰인다.
Try - Finally 특징
- 예외 처리 유연성 : finally 블록이 try 블록을 실행한 후 항상 실행된다. 따라서, 예외 발생 여부와 관계없이 예외처리 동작을 수행할 수 있도록 구현할 수 있다.
- 코드 분리와 관심사의 분리 : 비즈리스 로직과 예외 처리 로직을 명확히 분리할 수 있다.
Try - Finally 단점
- finally 블록에서 예외가 발생하는 경우 이전 예외를 덮어쓰기 때문에 디버깅하기 어려워진다.
- close(), 즉 자원 해제 동작 이전에 예외가 발생하는 경우 close() 동작이 수행되지 않을 수 있다.
- 여러 자원을 다루게 되는 경우 예외 처리에 대한 코드 유지보수가 복잡해진다.
Try - Finally 예제
아래 경우에는 close() 동작 내부에서 예외 덮어쓰기를 보인다.
class CustomFileWriter extends FileWriter { // close 내부에서 문제가 발생하는 것을 보여주기 위한 커스텀 클래스
public CustomFileWriter(String fileName) throws IOException {
super(fileName);
}
@Override
public void write(String str) throws IOException { // write 동작 중 예외 발생
super.write(str);
throw new IOException("Error during write operation");
}
@Override
public void close() throws IOException { // close 동작 중 예외 발생
super.close();
throw new IOException("Failed to close the writer");
}
}
public class Main {
public static void main(String[] args) {
CustomFileWriter writer = null;
try {
writer = new CustomFileWriter("output.txt");
writer.write("Hello, world!"); // 1. write 동작 중 예외 발생
} catch (IOException e) { // 2. write 예외 catch
/* 예외 처리 */
System.err.println("First Catch: " + e.getMessage()); // message : error during write operation
} finally {
if (writer != null) {
try {
writer.close(); // 3. 예외 발생으로 인해 close 수행하려던 중 close 예외 또 발생
} catch (IOException e) {
System.err.println("Second Catch: " + e.getMessage()); // 4. 기존 예외가 아닌 finally block에서 발생한 예외로 덮어씀
for (Throwable sup : e.getSuppressed()) { // 5. 억제된 예외(close 예외) 확인 및 로깅
System.err.println("Suppressed: " + sup.getMessage()); // 6. 억제된 로그에도 존재하지 않음.
}
}
}
}
}
}
====================================================================================
BUILD SUCCESSFUL in 270ms
2 actionable tasks: 2 executed
First Catch: Error during write operation
Second Catch: Failed to close the writer
오전 12:23:35: Execution finished ':Main.main()'.
try-finally 구문을 쓰는 경우 finally 블록에서 발생한 예외가 try에서 발생한 Exception을 덮어쓴다. getSuppressed를 통해 억제된 에러를 가져오려했음에도, 아무 것도 로그에 찍히지 않았음을 확인할 수 있다.
가정이긴 하지만, 실제 상황을 가정했을 때, close() 이전에 롤백이나 플러쉬 동작 등을 수행 중 에러가 발생하는 경우 close() 동작 자체가 이뤄지지 않을 수 있다는 문제점이 존재한다.
Try - With - Resources
try - with - resources는 JDK 7부터 추가된 try catch 문의 변형 문법이다.
try - with - resource는 try에 resource 객체를 전달하면, try 코드 블록이 끝나면 자동으로 자원을 종료해주는 기능이다.
Try - With - Resources 특징
- 자동 자원 해제 : AutoCloseable 인터페이스를 구현한 객체만 사용할 수 있다. 이 인터페이스의 close() 메소드는 try 블록의 실행이 완료되면 자동으로 호출되어, 명시적을 자원을 해제하는 코드를 작성하지 않아도 되며, 자원 누수의 가능성을 줄일 수 있다.
- 코드 간결성 : 코드가 더 간결해지고 읽기 쉬워진다. 자원을 해제하기 위한 별도의 finally 블록이 필요한 경우가 줄어든다.
- 예외 처리 향상 : try블록과 close() 메소드에서 예외가 동시에 발생하면, close() 메소드에서 발생한 예외는 억제되고 try 블록의 예외가 우선적을 처리된다.
- 리소스 확장성 : 여러 자원을 관리해야 하는 경우 , 각 자원을 세미 콜론으로 구분하여 하나의 try with resources 구문에서 처리할 수 있다.
Try - Finally에 비한 이점
- try 블록 종료 시 자동으로 자원을 해제한다.
- 이전 exception에 대해 덮어쓰지 않고, suppressed(억제된) 예외로 처리되어 디버깅을 통해 예외를 추적할 수 있다.
즉, try 블록에서 발생한 Exception을 그대로 가져가며, 추가로 발생한 에러는 suppressed error로 확인할 수 있다. - 여러 자원을 다루게 되는 경우 세미콜론으로 구분하여 여러 자원에 대한 관리가 가능하여, 확장에 용이하다.
Try - With - Resources 예제
아래 경우에는 close() 동작 내부에서 예외를 억지로 발생시킴으로써, 예외 덮어쓰기가 개선됨을 보인다.
Try - With - Resources를 쓰기 위해서는 AutoCloseable을 인터페이스를 구현한 클래스여야하지만,
FileWrite에서 이미 Closeable 인터페이스를 구현하고 있고, Closeable은 AutoCloseable을 확장하고 있기 때문에 추가적으로 구현하지 않았다.
FileWriter → OutpurStreamWriter → Writer → Closeable → AutoCloseable
class CustomFileWriter extends FileWriter { // close 내부에서 문제가 발생하는 것을 보여주기 위한 커스텀 클래스
public CustomFileWriter(String fileName) throws IOException {
super(fileName);
}
@Override
public void write(String str) throws IOException { // write 동작 중 예외 발생
super.write(str);
throw new IOException("Error during write operation");
}
@Override
public void close() throws IOException { // close 동작 중 예외 발생
super.close();
throw new IOException("Failed to close the writer");
}
}
public class Main {
public static void main(String[] args) {
try (CustomFileWriter writer = new CustomFileWriter("output.txt")) { // 1.try with resource로 자원 접근, finally 블록을 작성하지 않아도 됨.
writer.write("Hello, world!"); // 2. write에서 예외 발생
} catch (IOException e) { // 3. write 도중 발생한 예외를 처리
System.out.println("Exception: " + e.getMessage()); // 4. write 예외 확인
for (Throwable sup : e.getSuppressed()) { // 5. 억제된 예외(close 예외) 확인 및 로깅
System.out.println("Suppressed: " + sup.getMessage());
}
} finally {
System.out.println("Finally block called");
}
}
}
=====================================================================================
> Task :Main.main()
Exception: Error during write operation
Suppressed: Failed to close the writer
Finally block called
try 블록에서 발생한 Exception을 덮어쓰지 않았으며, close()함수가 호출되어 Failed to close the writer 로그가 찍힌 것을 볼 수 있다.
즉, close() 호출로 자원 해제를 수행하였으며 close() 도중 에러가 발생했음에도 원래 try문의 에러를 덮어쓰지 않았다.
다만, 로그 순서를 보면 try -> close -> catch -> finally 순으로 호출된다는 점을 유의해야한다.
개인적인 생각으로 connection을 가진 채로 예외처리를 수행해야 하는 경우(롤백, 플러쉬 등)은 close() 함수를 재정의 하여 처리하도록 구현하는 것이 좋아보인다. 그렇기 때문에 AutoCloseable의 close() 함수를 interface로하여 무조건 재정의하도록 한 것 같다.
코드 가독성 측면에서 봤을 때 finally에 동작이 들어가지 않고 close()문을 try-with-resources에서 호출해주기 때문에 간결해졌다. 또한, 여러 resource들을 세미콜론을 통해 한번에 관리할 수 있기 때문에 유지보수에 더욱 유리한 형태라고 생각된다.
결론
try-finally , try-with-resources에 관해정리하면서 느낀건 try-with-resources가 try-finally의 자원 관리 특화 버전으로 느껴졌다. finally가 대부분의 예외와 관계없이 무조건 처리해야하는 동작을 커버하지만, try-with-resources는 try-finally가 놓칠 수 있는 자원 관리에 관해 예외와 관계없이 안전하게 처리하기 위한 방법처럼 받아들이게되었다.
코드의 유지보수성도 중요하지만 복잡한 리소스 관리가 요구되는 프로젝트들에서 안정성을 위해서라도 try-with-resources를 적극적으로 사용하는 것이 좋은 것 같다. 그리고 잊지않고 실천하도록 적용해봐야겠다.
'백엔드 > 에러-예외 처리' 카테고리의 다른 글
Java - Null 체크 (+ 안정성, 가독성.....) (9) | 2024.04.01 |
---|---|
예외 VS 에러 (Java) (0) | 2023.06.22 |