티스토리 뷰
[Java] try-with-resources란? try-with-resources 사용법 예시와 try-with-resources를 사용해야 하는 이유
망나니개발자 2022. 3. 7. 10:00이번에는 오랜만에 자바 문법을 살펴보고자 합니다. Java7부터는 기존의 try-catch를 개선한 try-with-resources가 도입되었는데, 왜 try-catch가 아닌 try-with-resources를 사용해야 하는지, 어떻게 사용하는지 알아보도록 하겠습니다.
1. try-with-resources란? try-with-resources 사용법 예시
[ try-with-resources란? try-with-resources 사용법 예시 ]
try-with-resources를 살펴보기 전에 try-catch-finally로 자원을 반납하는 경우를 먼저 살펴보도록 하자.
두 가지를 모두 보고 비교함으로써 왜 try-with-resources를 사용해야 하는지 더욱 납득할 수 있을 것이다.
Java7 이전의 try-catch-finally
사용 후에 반납해주어야 하는 자원들은 Closable 인터페이스를 구현하고 있으며, 사용 후에 close 메소드를 호출해주어야 했다.
Java7 이전에는 close를 호출하기 위해서 try-catch-finally를 이용해서 Null 검사와 함께 직접 호출해야 했는데, 대표적으로 파일의 내용을 읽는 경우를 다음과 같이 구현할 수 있다.
public static void main(String args[]) throws IOException {
FileInputStream is = null;
BufferedInputStream bis = null;
try {
is = new FileInputStream("file.txt");
bis = new BufferedInputStream(is);
int data = -1;
while((data = bis.read()) != -1){
System.out.print((char) data);
}
} finally {
// close resources
if (is != null) is.close();
if (bis != null) bis.close();
}
}
문제는 이러한 과정이 여러가지 단점을 가지고 있다는 것이다.
- 자원 반납에 의해 코드가 복잡해짐
- 작업이 번거로움
- 실수로 자원을 반납하지 못하는 경우 발생
- 에러로 자원을 반납하지 못하는 경우 발생
- 에러 스택 트레이스가 누락되어 디버깅이 어려움
그래서 이러한 문제를 해결하기 위해 try-with-resources라는 문법이 Java7부터 추가되었다.
Java7 부터의 try-with-resources
Java는 이러한 문제점을 해결하고자 Java7부터 자원을 자동으로 반납해주는 try-with-resources 문법을 추가하였다.
Java는 AutoCloseable 인터페이스를 구현하고 있는 자원에 대해 try-with-resources를 적용 가능하도록 하였고, 이를 사용함으로써 코드가 유연해지고, 누락되는 에러없이 모든 에러를 잡을 수 있게 되었다.
public static void main(String args[]) throws IOException {
try (FileInputStream is = new FileInputStream("file.txt"); BufferedInputStream bis = new BufferedInputStream(is)) {
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
}
}
Closeable과 AutoCloseable의 관계
여기서 한가지 재미있는 사실이 있는데, 기존의 Closeable에 부모 인터페이스로 AutoCloesable을 추가했다는 점이다.
즉, Cloesable에 부모 인터페이스인 AutoCloesable을 새로 추가한 것이다.
상식적으로 먼저 만들어진 인터페이스를 자식 클래스나 인터페이스에 사용하는 것이 일반적일 것이다. 하지만 Java 개발자들은 먼저 만들어진 Cloesable 인터페이스에 부모 인터페이스인 AutoCloesable을 추가함으로써 하위 호환성을 100% 달성함과 동시에 변경 작업에 대한 수고를 덜었다. 만약 Cloesable을 부모로 만들었다면 기존에 만들어준 클래스들이 모두 Cloesable이 아닌 AutoCloesable를 구현(implements) 하도록 수정이 필요했을 것이다.
하지만 이러한 구조 덕분에 기존에 구현된 자원 클래스들 모두 try-with-resources가 사용가능해졌다.
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
public interface AutoCloseable {
void close() throws Exception;
}
2. try-catch-finally가 아닌 try-with-resources를 사용해야 하는 이유
[ 에러 스택 트레이스가 누락되는 경우 ]
try-catch-finally를 이용하면 에러가 발생해도 에러 스택 트레이스가 누락되는 경우가 발생할 수 있다. 이는 디버깅 및 원인 파악을 매우 어렵게 만드는데, 예를 들어 다음과 같은 Resource 클래스가 있다고 하자.
public class MangKyuResource implements AutoCloseable {
@Override
public void close() throws RuntimeException{
System.out.println("close");
throw new IllegalStateException();
}
public void hello() {
System.out.println("hello");
throw new IllegalStateException();
}
}
해당 경우 재현을 위해 hello와 close 호출 시에 런타임 예외 중 하나인 IllegalStateException이 발생하도록 하였다. 그러면 이제 위의 자원을 각각 try-catch-finally와 try-with-resources로 반납해보도록 하자.
try-catch-finally로 반납하는 경우 (에러 스택 트레이스 누락 O)
먼저 try-catch-finally로 자원을 반납해보도록 하자. 코드는 꽤나 심플하다. 자원을 사용해주고 finally 구문에서 null이 아닌 경우를 검사하여 반납해주고 있다.
public void temp1() {
MangKyuResource mangKyuResource = null;
try {
mangKyuResource = new MangKyuResource();
mangKyuResource.hello();
} finally {
if (mangKyuResource != null) {
mangKyuResource.close();
}
}
}
문제는 위에서 발생한 에러 트레이스가 hello 호출 시에서 찍힌 부분은 누락되고, close 호출 시에 찍힌 부분만 남는다는 것이다.
위의 코드를 실행하면 나오는 에러 코드는 다음과 같다.
hello
close
java.lang.IllegalStateException
at com.example.testing.composition.MangKyuResource.close(MangKyuResource.java:8)
at com.example.testing.composition.SteakTest.temp2(SteakTest.java:49)
hello에서 발생한 에러 트레이스가 정상적으로 찍히지 않은 것이다. 만약 이러한 문제가 운영 중에 발생했다면 원인을 파악하는데 상당히 많은 시간을 들여야 할 것이다. 어쩌면 찾지 못할수도 있다.
하지만 try-with-resources를 이용한다면 누락되는 에러 트레이스 없이 모두 남길 수 있다.
try-with-resources로 반납하는 경우 (에러 스택 트레이스 누락 X)
이번에는 try-with-resources로 자원을 반납하도록 수정해보도록 하자. 기존의 코드는 다음과 같이 수정된다.
public void temp2() {
try (MangKyuResource mangKyuResource = new MangKyuResource()) {
mangKyuResource.hello();
}
}
위의 코드를 실행하면 실행 결과는 다음과 같다.
hello
close
java.lang.IllegalStateException
at com.example.testing.composition.MangKyuResource.hello(MangKyuResource.java:13)
at com.example.testing.composition.SteakTest.temp4(SteakTest.java:67)
... 생략
Suppressed: java.lang.IllegalStateException
at com.example.testing.composition.MangKyuResource.close(MangKyuResource.java:8)
at com.example.testing.composition.SteakTest.temp4(SteakTest.java:68)
에러가 발생하는 hello와 close 모두 에러 트레이스가 남게된 것이다. 이러한 이유로 우리는 반드시 try-catch-finally가 아닌 try-with-resources를 사용해야 한다. 또한 try-catch-finally를 사용하면 자원이 반납되지 않을 수 있는데, 해당 내용도 살펴보도록 하자.
[ 에러로 자원을 반납하지 못하는 경우 ]
try-with-resources를 사용해야 하는 이유로는 편리함과 실수 방지도 있지만, 누락없이 모든 자원을 반납할 수 있다는 점도 있다.
try-catch-finally를 이용하는 경우에는 자원 반납이 누락되는 경우가 발생할 수 있는데, 코드로 살펴보도록 하자.
예를 들어 다음과 같이 AutoCloesable를 구현한 자원이 있다고 하자. (기존에서 hello에 예외가 발생하지 않도록 하였다.)
public class MangKyuResource implements AutoCloseable {
@Override
public void close() throws RuntimeException{
System.out.println("close");
throw new IllegalStateException();
}
public void hello() {
System.out.println("hello");
}
}
그리고 위의 자원을 각각 try-catch-finally와 try-with-resources로 반납해보도록 하자.
try-catch-finally로 반납하는 경우 (자원 반납 누락)
먼저 try-catch-finally로 자원을 반납해보도록 하자. 코드는 다음과 같은데, 자원을 사용하고 finally 구문에서 null이 아닌 경우를 검사하여 반납해주고 있다.
public void temp3() {
MangKyuResource resource1 = null;
MangKyuResource resource2 = null;
try {
resource1 = new MangKyuResource();
resource2 = new MangKyuResource();
resource1.hello();
resource2.hello();
} finally {
if (resource1 != null) {
resource1.close();
}
if (resource2 != null) {
resource2.close();
}
}
}
resource1을 close하는 경우에 IllegalStateException이 발생할텐데, 우리는 그래도 resource2가 반납되기를 기대할 것이다.
그리고 코드를 실제로 호출해보도록 하자. 그러면 다음과 같은 결과가 나온다.
hello
hello
close
두 번째 자원인 resource2가 정상적으로 반납되지 않은 것이다. 이러한 문제가 발생한 이유는 resource1을 close하는 finally 구문 안에 IllegalStateException를 catch하는 코드가 없기 때문이다. 이러한 문제를 해결하기 위해서는 resource1과 resource2를 반납하는 부분을 각각 try-catch-finally로 묶어야하는데, 이는 코드를 더욱 복잡하게 만든다.
하지만 try-with-resources를 이용한다면 이러한 별도의 처리 없이 모두 자원을 반납할 수 있다.
try-with-resources로 반납하는 경우 (모든 자원 반납)
이번에는 try-with-resources로 자원을 반납하도록 수정해보도록 하자. 기존의 코드는 다음과 같이 수정된다.
public void temp4() {
try (MangKyuResource resource1 = new MangKyuResource(); MangKyuResource resource2 = new MangKyuResource()) {
resource1.hello();
resource2.hello();
}
}
우선 코드를 실행하기 전에 코드를 먼저 살펴보도록 하자. 누가 보아도 코드가 훨씬 간결해졌다.
그리고 위의 코드를 실행하면 실행 결과는 다음과 같다.
hello
hello
close
close
누락없이 정상적으로 2가지 자원이 모두 반납된 것이다. 이렇게 된 이유는 단순하다. Java 파일이 Class 파일로 컴파일 될 때 try-with-resources에서 누락없이 모든 경우를 try-catch-finally로 변환해주기 때문이다. 즉, 위에서 직접 구현해주어야 했던 부분을 컴파일러가 처리해준 것이다.
정리하면 우리는 다음과 같은 이유로 반드시 try-catch-finally가 아닌 try-with-resources를 사용해주어야 한다.
- 코드를 간결하게 만들 수 있음
- 번거로운 자원 반납 작업을 하지 않아도 됨
- 실수로 자원을 반납하지 못하는 경우를 방지할 수 있음
- 에러로 자원을 반납하지 못하는 경우를 방지할 수 있음
- 모든 에러에 대한 스택 트레이스를 남길 수 있음
앞으로는 자원을 반납해주어야 하는 경우를 만나면 try-catch-finally가 아닌 try-with-resources를 사용해주도록 하자.
'Java & Kotlin' 카테고리의 다른 글
[Java] Private 메소드를 테스트하는 방법과 이를 지양해야 하는 이유 (2) | 2022.04.09 |
---|---|
[Java] 부모 클래스의 메소드 오버라이딩이 더 큰 범위의 접근 제어자만 가능한 이유 or 더 좁은 범위로 변경할 수 없는 이유 (0) | 2022.03.23 |
[Java] 언제 Optional을 사용해야 하는가? 올바른 Optional 사용법 가이드 - (2/2) (34) | 2022.01.02 |
[Java] 메소드 오버라이딩/ 메소드 오버로딩을 통한 상속 다형성에 대한 이해와 Self 참조 (0) | 2021.10.15 |
[Java] Map보다 DTO 클래스를 사용해야 하는 이유 (13) | 2021.06.27 |