비즈니스 로직을 작성하면서 정상 로직을 생각하는 것도 중요하지만, 예외 상황과 엣지 케이스에 대한 방어로직을 작성하는 데 신경을 더 쓰는 것은 당연할 것이다.
try-catch문으로 예외를 잡아서 처리하는 것 뿐만 아니라, 커넥션, 메모리 등의 자원을 반환하거나 초기화 등을 수행하기 위해 finally 블럭을 사용하게 된다.
김영한님의 중급 자바 강의를 들으면서 추가적으로 Java7에 도입된 try-with-resources 기능이 있음을 알게되었고, 추후에 업무에 사용할 수 있도록 차이점과 장점을 기록하는 것이 이번 포스팅의 목적이다.
# 예시 시나리오
차이점과 장점을 알아보기 위해 간단한 시나리오를 작성한다.
Reader
- 파일을 쓰기 위한 기본 기능 제공하는 메소드
- 파일 열기, 쓰기, 닫기 메소드 존재
ReaderService
- Reader 클래스를 사용하기 쉽게 writeFile() 메소드로 제공
public class Reader {
public void openFile() {
System.out.println("==== 파일을 엽니다. ====");
}
public void writeFile(String text) {
if (text.contains("error")) {
throw new RuntimeException("error");
}
System.out.printf("Write : %s\n", text);
}
public void closeFile() {
System.out.println("==== 파일을 닫습니다. ====");
}
}
Reader 클래스는 간단하게 파일을 열고, 쓰고, 닫는 기능을 갖는다.
파일을 쓰는 시나리오는 다음으로 가정한다.
파일을 여는 과정이 선행되어야 하고, 이후 입력 받은 텍스트를 파일에 쓰고, 작업이 완료되면 파일을 닫는다.
테스트의 편의를 위해 파일을 쓸 때 "error" 라는 문자가 포함되어 있으면 RuntimeException을 던지도록 작성했다.
public class ReaderService {
void writeFile(String text) {
Reader reader = new Reader();
try {
reader.openFile();
reader.writeFile(text);
} catch (RuntimeException e) {
System.out.println("[Error]" + e.getMessage());
e.printStackTrace(System.out);
} finally {
reader.closeFile();
}
}
}
ReaderService에선 reader의 기능들을 시나리오에 맞게 호출하게 되고, 발생하는 에러를 잡기 위해 try-catch 문과 정상 흐름 & 예외 흐름 모두 파일을 닫기 위해 finally를 사용했다.
추가적으로, finally와 try-with-resources의 차이를 분명하게 하기 위해 스택 트레이스를 콘솔창에서의 System.err를 System.out에 찍히도록 바꿔주었다.

만약 catch문이 없다면 main에서 RuntimeException을 그대로 던지게 되는데, 마치 try-catch-finally에서 에러 발생 시 바로 close를 닫아주는 것처럼 확인된다.

위는 RuntimeException을 catch으로 잡고 System.out으로 출력하게 한 결과이다. (제공한 ReaderService)
위 결과를 확인해보면, reader.close()가 실행되는 시점(실제로 파일을 닫는 시점)은 catch에서 처리가 되고 난 후이다.
그런데, "파일을 닫는 작업이 catch 문을 거치고 실행될 필요가 있을까?" 라는 의문이 존재한다.
RuntimeException 인스턴스가 생성되고 나서 catch 문에 들어온 시점에서는 파일을 더 이상 사용할 수 없는 상황이니, 굳이 catch 구문 이후에 파일을 닫기보단 에러가 발생한 직후에(catch 문에 도달하기 전) 파일을 닫는 것이 더 명확하다.
추가적으로, finally를 통해 매번 자원 반납을 진행해야하는 귀찮음(?)도 남아있다.
# try-with-resources (AutoClosable)
finally를 통해 자원을 반납하는 부분에서 하기 2가지 개선점이 존재한다.
- 자원의 반납 시점이 catch문 이후이다.
- 명시적으로 자원 반납을 진행해야한다.
Java7은 try-with-resources을 통해 이러한 부분을 개선할 수 있다.
try-with-resources를 사용하기 위한 방법은 AuthClosable 인터페이스를 구현해야한다.
위치는 try 문에서 자원을 반납해야하는 클래스이다.

AuthClosable 인터페이스에는 위와 같이 close() 메소드만 존재하는 것을 알 수 있다.
설명의 주요 내용은 다음과 같다.
close 메소드는 try-with-resources 구문에 의해 자동으로 실행된다.
인터페이스에서 close 메소드는 Exception을 던지도록 작성되어 있지만, 가능하면 자원을 해제하는 작업에서 예외가 발생하지 않아야한다.
close() 메소드는 멱등성을 갖도록 만드는 것을 권장한다.
자원을 해제하는 작업에서 또 다른 Exception이 발생하면 또 거기에 따른 try-catch 문이 필요해지기 때문에, 관련된 내용들을 꼭 지킬 수 있도록 신경써야할 것 같다.
앞선 Reader 클래스에서 AuthClosable을 구현해보면,
public class Reader implements AutoCloseable {
public void openFile() {
System.out.println("==== 파일을 엽니다. ====");
}
public void writeFile(String text) {
if (text.contains("error")) {
throw new RuntimeException("error");
}
System.out.printf("Write : %s\n", text);
}
public void closeFile() {
System.out.println("==== 파일을 닫습니다. ====");
}
// close() 오버라이딩
@Override
public void close() {
closeFile();
}
}
close() 메소드를 오버라이딩하면 된다.
애초에 닫는 메소드를 close()로만 사용해도 무방할 것 같다.
AutoClosable을 구현하면 try-with-resources를 사용할 준비가 완료된 것이고, 이제 ReaderService에서 이를 사용해보면 다음과 같다.
public class ReaderService {
void writeFile(String text) {
try (Reader reader = new Reader()) { // try-with-resources
reader.openFile();
reader.writeFile(text);
} catch (RuntimeException e) {
System.out.println("[Error]" + e.getMessage());
e.printStackTrace(System.out);
}
}
}
reader 인스턴스가 try 문에 소괄호안에서 생성되도록 변경되었고, finally 문도 필요가 없어졌다. (AutoClosable이 처리해주므로)
이렇게 try-with-resources 기능을 사용할 수 있고, 이를 통해서 reader에서 파일을 읽고, 닫는 과정이 try 블록 안에서 이루어진다.
즉, 자원의 반납이 catch 문을 거치기 전에 일어나게 된다.

실행 결과를 확인해보면 위와 같은데, 앞서 finally에서 자원을 해제할 때와는 다르게 파일을 닫는 위치가 스택 트레이스 출력보다 먼저이다.
즉, 자원의 해제가 try 문에서 에러가 발생한 시점 이후 catch 문을 거치기 전 이루어지게 된다.
이를 통해 자원의 해제 시점을 에러 발생 시점으로 더 명확하게 가져갈 수 있다.
# 장점
try-with-resources를 통해서 얻을 수 있는 장점은 다음과 같다.
- 코드가 간결해지고, 자원 해제에 대한 실수 가능성을 낮출 수 있다. - finally 문 제거
- 자원의 Scope를 try 문으로 좁힐 수 있다.
# ETC
실행에 사용한 Main 코드는 다음과 같다.
import java.util.Scanner;
public class ReadMain {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
ReaderService readerService = new ReaderService();
while (true) {
System.out.print("문자를 입력하세요: ");
String input = scanner.nextLine();
if (isExit(input)) break;
readerService.writeFile(input);
System.out.println();
}
}
private static boolean isExit(String input) {
if (input.equals("exit")) {
System.out.println("프로그램을 종료합니다.");
return true;
}
return false;
}
}