[ITEM9] try-finally보다는 try-with-resources를 사용하라

뚝딱이·2024년 1월 7일
0

이펙티브 자바

목록 보기
10/55
post-thumbnail

try-finally

InputStream,OutputStream,java.sql.Connection등은 close 메서드를 호출해 직접 닫아줘야 한다. 전통적으로 우리는 try-finally를 사용해 닫힘을 보장했다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TopLine {
    
    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }
}

어떤가? 평소에 자주 사용하던 방식이다. 이때 자원을 하나 더 사용해보자.

import java.io.*;

public class Copy {
    private static final int BUFFER_SIZE = 8 * 1024;

    static void copy(String src, String dst) throws IOException {
        InputStream in = new FileInputStream(src);
        try {
            OutputStream out = new FileOutputStream(dst);
            try {
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            } finally {
                out.close();
            }
        } finally {
            in.close();
        }
    }

    public static void main(String[] args) throws IOException {
        String src = args[0];
        String dst = args[1];
        copy(src, dst);
    }
}

자원이 하나 더 늘으니 try-finally 문 안에 또 try-finally 문이 들어가 굉장히 지저분해졌다. 하지만 위의 코드는 결점이 있는 코드이다. 예외는 try, finally블록 모두에서 발생할 수 있다.

다시 아래의 코드를 살펴보자.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TopLine {
    
    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            br.close();
        }
    }
}

만약 기기에 물리적 문제가 생긴다면 readLine()에서 예외를 던질 것이고, 같은 이유로 close 메서드도 실패할 것이다. 이런 상황이라면 두번째 예외가 첫 번째 예외를 완전히 집어 삼켜 디버깅이 몹시 어렵다. 왜냐하면 두번째 예외가 첫번째 예외를 집어 삼켰으므로 스택 추적 내역에 첫 번째 예외에 관한 정보가 없어져 최초 발생 예외에 대해 알지 못하기 때문이다.

이는 try-with-resource를 통해 해결할 수 있다.

try-with-resource

이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야한다. 다음은 위의 코드를 try-with-resource를 사용해 재작성한 것이다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TopLine {
    static String firstLineOfFile(String path) throws IOException {
        try (BufferedReader br = new BufferedReader(
                new FileReader(path))) {
            return br.readLine();
        }
    }
}

이전처럼 readLineclose 호출 양쪽에서 예외가 발생하면 close에서 발생한 예외는 숨겨지고, readLine에서 발생한 예외가 기록된다.

import java.io.*;

public class Copy {
    private static final int BUFFER_SIZE = 8 * 1024;

    // try-with-resources on multiple resources - short and sweet (Page 35)
    static void copy(String src, String dst) throws IOException {
        try (InputStream   in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dst)) {
            byte[] buf = new byte[BUFFER_SIZE];
            int n;
            while ((n = in.read(buf)) >= 0)
                out.write(buf, 0, n);
        }
    }
}

코드가 훨씬 짧고 읽기 수월할 뿐만 아니라 문제를 진단하기도 훨씬 좋다.

try-with-resource에서도 catch절을 쓸 수 있다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TopLineWithDefault {
    static String firstLineOfFile(String path, String defaultVal) {
        try (BufferedReader br = new BufferedReader(
                new FileReader(path))) {
            return br.readLine();
        } catch (IOException e) {
            return defaultVal;
        }
    }
}

꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자. 예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다. tyr-finally로 작성하면 실용적이지 못할 만큼 코드가 지저분해지는 경우라도, try-with-resources로는 정확하고 쉽게 자원을 회수할 수 있다.

public class FirstError extends RuntimeException {
}

public class SecondError extends RuntimeException {
}

위와 같이 두가지 에러가 존재한다고 하자.

public class MyResource implements AutoCloseable {

    public void doSomething() throws FirstError {
        System.out.println("doing something");
        throw new FirstError();
    }

    @Override
    public void close() throws SecondError {
        System.out.println("clean my resource");
        throw new SecondError();
    }
}

그리고 MyResource에서 위에서 살펴보았던 예제와 같이 어떤일을 행하는 메서드와, 자원을 닫는 close 메서드 둘 다에서 error가 발생한다고 해보자.

MyResource myResource = new MyResource();
myResource.doSomething();
myResource.close();

그렇다면 위와 같이 사용하려고 코드를 작성할 수 있다. 하지만, 이렇게 작성하면 안된다. doSomething에서 에러가 나면 close가 되지 않고 즉, 정리가 되지 않고 끝나기 때문이다. 따라서 고전적인 방법으로 다음과 같이 작성할 수 있다.

MyResource myResource = new MyResource();
try{
	myResource.doSomething();
} finally {
	myResource.close();
}

하지만 위 코드에는 문제가 있다. 우리는 보통 에러가 났을 때 최초의 에러 원인을 알고자한다. 하지만 이를 실행하면 doSomething에서 에러가 나고, finally로 가 close에서 또 에러가 나므로 두번째 에러가 첫번째 에러를 잡아 먹게 된다. 따라서 결과가 아래와 같이 뜬다.

doing something
clean my resource
Exception in thread "main" SecondError
	at MyResource.close(MyResource.java:11)
	at Main.main(Main.java:7)

따라서 FirstError에 대해선 디버깅 하지 못한다. 그렇다면 try-with-resources를 사용해보자.

public class Main {
    public static void main(String[] args) {
        try (MyResource myResource = new MyResource()) {
            myResource.doSomething();
        }
    }
}

코드가 간결해졌다. 그렇다면 결과는 어떨까?

doing something
clean my resource
Exception in thread "main" FirstError
	at MyResource.doSomething(MyResource.java:5)
	at Main.main(Main.java:4)
	Suppressed: SecondError
		at MyResource.close(MyResource.java:11)
		at Main.main(Main.java:3)

최초의 원인인 FirstError에 대한 로그를 확인할 수 있다. 이로인해 디버깅이 훨씬 쉬워진다.

자원을 두개쓸 때도 훨씬 간결하다.

public class Main {
    public static void main(String[] args) {
        try (MyResource myResource = new MyResource(); MyResource myResource1 = new MyResource()) {
            myResource.doSomething();
            myResource1.doSomething();
        }
    }
}

결과는 아래와 같다.

doing something
clean my resource
clean my resource
Exception in thread "main" FirstError
	at MyResource.doSomething(MyResource.java:5)
	at Main.main(Main.java:4)
	Suppressed: SecondError
		at MyResource.close(MyResource.java:11)
		at Main.main(Main.java:3)
	Suppressed: SecondError
		at MyResource.close(MyResource.java:11)
		at Main.main(Main.java:3)

첫번째 doSomething에서 예외가 터졌지만 close는 둘 다 해주는 것을 볼 수 있다.

출처

이펙티브 자바 3/E
[이팩티브 자바] #9 Try-with-Resource-백기선

profile
백엔드 개발자 지망생

0개의 댓글

관련 채용 정보