[Java][Effective Java] item 9. try-finally 보다 try-with-resources를 사용하라

2022. 6. 3. 14:23JAVA/Effective Java

1. try-finally 방식의 용도

자바 라이브러리에는 InputStream, OutputStream, java.sql.Connection 등과 같은 입출력 클래스는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많습니다. 전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally 방식을 사용하여 인스턴스가 실행 도중 예외가 발생하거나 메서드에서 반환되는 경우를 포함하여 자원을 안정적으로 회수할 수 있도록 합니다.

 

다음 예제는 try-finally 방식을 사용하여 일반적으로 자원을 회수하는 방식입니다.

	// try-finally 구문을 활용한 일반적인 자원 회수
	static String firstLineOfFile(String path) throws IOException {
		
		// 회수해야할 자원
		BufferedReader br = new BufferedReader(new InputStreamReader(TryFinallyTest.class.getResourceAsStream(path)));
		
		try {
			return br.readLine();
		}finally {
			br.close();
		}
	}

 

 

2. try-finally보다 try-with-resources 방식을 사용해야 하는 이유

  • try-finally 방식에서 try 블럭과 finally 블럭에서 둘다 예외가 발생하면 try 블럭에서 발생한 예외는 무시되고 finally 블럭에서 발생한 예외만 출력됨
  • close해야 할 자원이 둘 이상이라면 try-finally 구문이 복잡해진다.

다음 예제는 try-finally 방식으로 try블럭과 finally 블럭에서 둘다 예외를 발생시킬 때 예외 클래스가 어떻게 나오는지 확인하는 예제입니다.

	// try 블럭과 finally 블럭에서 예외가 발생시 try 블럭의 예외는 무시된다.
	static String throwDoubleMethod() throws IOException {		
		try {
			throw new IOException("try 블럭에서 예외발생");
		}finally {
			throw new IOException("finally 블럭에서 예외발생");	
		}
	}
	@Test
	void doubleThrowMethodTest() {
		String result = null;
		try {
			throwDoubleMethod();
		} catch (IOException e) {
			result = e.getMessage();
			e.printStackTrace();
		}
		
		assertEquals("finally 블럭에서 예외발생", result);
	}
java.io.IOException: finally 블럭에서 예외발생
	at role9.TryFinallyTest.throwDoubleMethod(TryFinallyTest.java:90)
	at role9.TryFinallyTest.doubleThrowMethodTest(TryFinallyTest.java:45)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    ...

위 실행결과와 같이 try 블럭과 finally 블럭에서 예외가 둘다 발생할 경우 try 블럭의 예외는 무시되고 finally 블럭의 예외만 출력되는 것을 볼 수 있습니다. 이렇게 되면 스택 추적 내역에서 첫번째 예외에 관한 정보는 남지 않게 되어, 실제 시스템에서의 디버깅을 어렵게 만듭니다.

 

다음 예제는 try-finally 방식에서 close 해야할 자원이 둘 이상인 경우 구분이 어떻게 되는지 확인하는 예제입니다.

	// 자원이 둘 이상이면 try-finally 방식은 너무 지저분해진다
	static void copy(String src, String dst) throws IOException {
		
		InputStream in = TryFinallyTest.class.getResourceAsStream(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();
		}
	}

위와 같이 닫아야할 자원이 늘어나면 늘어날수록 try-finally 구문이 중첩되기 때문에 매우 복잡해집니다.

3. try-with-resources 방식

try-with-resources 방식은 try에 자원 객체를 전달하면 try 코드 블록이 종료되면 자동으로 자원을 close하는 방식입니다. 위 예제에서 firstLineOfFile 메서드와 copy 메서드를 try-with-resources 방식으로 개선하면 다음과 같습니다.

	// try-with-resources : 자원을 회수하는 최선책
	static String firstLineOfFile(String fileName) throws IOException {
		try(BufferedReader br = new BufferedReader(new InputStreamReader(
												   TryWithResourceTest.class.getResourceAsStream(fileName)
												   ))
			){
			return br.readLine();
		}
	}
	
	// 복수의 자원을 처리하는 try-with-resources
	static void copy(String src, String dst) throws IOException {
		try(InputStream in = TryWithResourceTest.class.getResourceAsStream(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 블럭이 종료되면 자원 객체 내부에 구현된 close 메서드를 실행합니다.

 

try-with-resources 방식의 장점

  1. 다중 예외가 발생한 경우 무시되지 않고 기록됨
    • 예를 들어 readLine과 close 호출 양쪽에서 예외가 발생하면 close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록됩니다. close에서 발생한 예외는 suppressed라는 꼬리표를 달고 출력됩니다.
  2. finally문을 사용하지 않기 때문에 복잡해지지 않아 가독성이 좋아짐

 

4. try-with-resources 방식을 사용하기 위한 조건

  • try-with-resources 구문을 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 합니다.
  • 자바 라이브러리와 서드파티 라이브러리들의 수많은 클래스와 인터페이스가 이미 AutoCloseable을 구현하거나 확장하였습니다.

다음 예제는 어떤 한 클래스가 AutoCloseable 인터페이스를 구현하여 try-with-resources 방식을 사용하여 자원을 회수하는 예제입니다.

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource(String fileName) {
        InputStream input = CloseableResource.class.getResourceAsStream(fileName);
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}
	@Test
	void tryWithResourceTest() {
		String line = null;
		
		try(CloseableResource cr = new CloseableResource("file.txt")){
			line = cr.readFirstLine();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		assertEquals("hello world", line);
	}
Closed BufferedReader in the close method

위 실행결과를 통해서 try 블럭을 마치면 CloseableResource 인스턴스는 close 메서드를 수행하여 BufferedReader br 참조변수를 close합니다.

 

위와 같이 닫아야 하는 자원을 뜻하는 클래스를 작성한다면 AutoCloseable 인터페이스를 반드시 구현해야합니다.

 

정리하며

  • 반드시 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자

 

References

source code : https://github.com/yonghwankim-dev/effective_java/tree/master/src/role9
effective java 3/E