Item 9. try-finally보다는 try-with-resources를 사용하라

다람·2025년 2월 26일
0

Effective Java

목록 보기
9/13
post-thumbnail

1. 자바에는 직접 닫아야 하는 자원들이 많다

자바 라이브러리와 프레임워크에는, 직접 자원을 닫아야 하는 클래스들이 많이있다.

  • InputStream, OutputStream, Reader, Writer (IO 관련)
  • java.sql.Connection, Statement, ResultSet (JDBC 관련)

JPA(Hibernate 등)를 사용할 때도 DB 커넥션을 잘 닫지 않으면

  • 그 커넥션이 반환되지 않아 풀에 남은 사용 가능한 커넥션 수가 줄어들게 되고 반복해서 여러 요청이 동시에 들어오면 커넥션 풀이 바닥나서 새로운 DB 접근이 불가능해지거나 대기 시간이 길어지게 된다.
  • 이럴 때 성능 문제Deadlock(교착상태, 프로세스 또는 트랜잭션이 서로 점유한 자원을 기다리다가 영원히 기다리게 되는 상태이다.)이 발생할 수 있다.

자원 닫기를 놓치게 되면 예측할 수 없는 오류로 이어지기 때문에 빠트리면 안되는 중요한 작업이다.

2. 자원 닫는 방법

2-1. 전통적인 방법: try-finally

과거에는 다음처럼 try 블록에서 자원을 사용하고, finally 블록에서 자원을 닫았다.

자원이 한 개인 경우

static String firstLineOfFile(String path) throws IOException {
	BufferedReader br = new BufferedReader(new FileReader(path));
	try {
		return br.readLine();
	} finally {
		br.close();
	}
}
  • br.readLine()예외가 발생해도, finally 블록이 실행되며 br.close()를 보장(실행하는 것을 보장하는 것이지 실행하면 반드시 성공한다는 것을 보장하지는 않는다.)한다.

자원이 여러 개인 경우

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();
	}
}
  • try 블록이 중첩되면서 코드가 복잡해진다.

예외가 2개 이상 발생하게 되는 경우

public static String firstLineOfFile(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine(); // 여기서 예외 발생 가능
    } finally {
        // finally는 *꼭* 실행은 됨. 하지만 close()가 또 실패할 수 있음
        br.close(); // 여기서도 IOException 가능
    }
}
  • 만약 장치이상으로 br.readLine()에서 예외가 발생하게 된다면, 장치가 이미 이상하기 때문에 닫는 과정에서 또 다른 IO 예외 close()에서도 예외가 발생할 수가 있다.
  • 이런 경우에 close()를 시도하는 경우 발생한 두 번째 예외가 첫 번째 예외를 덮어써버리기 때문에 디버깅이 어려워진다.
  • 두 번째 예외 대신 첫 번째 예외를 기록하도록 만들 수도 있지만 코드가 지저분해진다.
두 번째 예외가 첫 번째 예외를 덮어쓰는 경우 예시
package item9;

import java.io.IOException;

class Resource implements AutoCloseable {
    private final String name;

    Resource(String name) {
        this.name = name;
    }

    @Override
    public void close() throws IOException {
        System.out.println(name + " closing...");
        throw new IOException("close()에서 발생한 예외: " + name);
    }

    void doSomething() throws Exception {
        // 첫 번째 예외
        throw new Exception("doSomething() 예외: " + name);
    }
}

public class SuppressedExceptionFinallyDemo {

    public static void main(String[] args) {
        try {
            testFinally();
        } catch (Exception e) {
            System.out.println("메인에서 잡은 예외: " + e);
            // getSuppressed()를 출력해봐도 suppressed 예외가 없을 것
            // 왜냐하면 try-finally 구조에선 첫 번째 예외가 덮어써지기 때문
            for (Throwable t : e.getSuppressed()) {
                System.out.println("숨겨진(suppressed) 예외: " + t);
            }
        }
    }

    static void testFinally() throws Exception {
        Resource2 r = null;
        try {
            r = new Resource2("MyResource");
            r.doSomething(); // 첫 번째 예외 발생
        } finally {
            if (r != null) {
                r.close(); // 두 번째 예외
            }
        }
    }
}


실행 흐름
1. r.doSomething()가 첫 번째 예외(Exception("doSomething() 예외..."))를 던진다.
2. finally 블록이 실행되며 r.close()에서 두 번째 예외(IOException("close()에서 발생한 예외..."))가 발생한다.
3. 자바는 두 번째 예외를 던져버리며, 첫 번째 예외 정보가 없어지게 된다(덮어써짐).

2-2. 자바 7 이후 방법: try-with-resources

자바 7부터는 try-with-resources가 도입되면서, 위 문제들이 깔끔하게 해결되었다.

AutoCloseable 인터페이스

  • 자원을 닫아야 하는 클래스는 AutoCloseable을 구현해야한다.
    - AutoCloseableclose() 메서드 하나만 정의되어 있는 인터페이스다.
  • 자바 표준 라이브러리나 서드파티 라이브러리 중 대부분이 이미 AutoCloseable을 구현해두었다.

서드파티 라이브러리란, 자바 표준 라이브러리(공식 JDK) 외부에서 제공되는 라이브러리이다. 예를 들어서 org.apache.*, org.springframework.* 등을 말한다.

자원이 한 개인 경우

static String firstLineOfFile(String path) throws IOException {
	BufferedReader br = new BufferedReader(new FileReader(path));
	try (BufferedReader br = new BufferedReader(
					new FileReader(path))) {
		return br.readLine();
	}
}
  • try( ... ) 안에 AutoCloseable 구현체를 생성한다.
  • 블록이 끝나면 자동으로 close()가 호출되어 자원 해제가 이루어진다.

자원이 여러 개인 경우

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-finally가 없어서 코드가 훨씬 짧고 읽기 쉽게 되었다.

예외가 2개 발생해도 첫 번째 예외가 사라지지 않는다

  • try-with-resources첫 번째 예외를 보존하고 두 번째 예외를 suppressed 상태로 보관한다.
  • 디버깅할 때 모든 예외 정보를 볼 수 있게 되어서 문제 원인 파악이 쉬워졌다.
  • Throwable.getSuppressed() 메서드로 프로그램에서 얻게 할 수도 있게 되었다.
모든 예외 정보 볼 수 있는 예시
package item9;

import java.io.IOException;

class Resource2 implements AutoCloseable {
    private final String name;

    Resource2(String name) {
        this.name = name;
    }

    @Override
    public void close() throws IOException {
        System.out.println(name + " closing...");
        throw new IOException("close()에서 발생한 예외: " + name);
    }

    void doSomething() throws Exception {
        throw new Exception("doSomething() 예외: " + name);
    }
}

public class SuppressedExceptionDemo {
    public static void main(String[] args) {
        try {
            testSuppressed();
        } catch (Exception e) {
            // 여기서 예외를 잡아서 확인
            System.out.println("메인에서 잡은 예외: " + e);
            // 숨겨진(suppressed) 예외들도 확인
            for (Throwable t : e.getSuppressed()) {
                System.out.println("숨겨진(suppressed) 예외: " + t);
            }
        }
    }

    static void testSuppressed() throws Exception {
        try (Resource2 r = new Resource2("MyResource")) {
            r.doSomething(); // 첫 번째 예외 발생
        }
    }
}


실행 흐름
1. r.doSomething()에서 첫 번째 예외가 발생한다(예: Exception: doSomething() 예외: MyResource).
2. 블록이 끝나면서 r.close() 호출 → 두 번째 예외(IOException: close()에서 발생한 예외: MyResource)가 발생한다.
3. try-with-resources는 첫 번째 예외를 주 예외로 삼고 두 번째 예외를 suppressed로 붙여서 던진다.
4. main에서 잡은 뒤 getSuppressed()를 보면 숨겨진 예외로 두 번째 예외 정보를 볼 수 있다.

2-3. catch 절 함께 사용 가능

static String firstLineOfFile(String path, String defaultVal) {
	try (BufferedReader br = new BufferedReader(
					new FileReader(path))) {
		return br.readLine();
	} catch (IOException e) {
		return defaultVal;
	}
}
  • 예외가 터져도 catch 블럭에서 처리가 가능하다. try 블록을 중첩하지 않아도 여러 예외 처리를 깔끔하게 할 수 있다.

3. 결론

  1. try-finally는 자원을 닫을 수 있지만, 코드가 복잡해지고 예외가 여러 번 발생하면 디버깅이 어렵다.
  2. try-with-resources를 사용하면 코드가 깔끔해지고, 다중 예외도 간단하게 처리할 수 있다.
    => 자원을 닫을 땐 try-with-resources를 사용하자.
profile
개발하는 다람쥐

0개의 댓글