1. 자원 정리(Resource Cleanup)의 중요성

자바에서 파일, 네트워크 소켓, 데이터베이스 연결 같은 자원은 사용 후 반드시 정리(close) 해야 한다. 그렇지 않으면 메모리 누수(memory leak) 또는 리소스 잠금(locking issue) 같은 문제가 발생할 수 있다.

자원을 정리하는 일반적인 방법은 try-finally 를 사용하는 것이다. 하지만 예외 처리를 제대로 하지 않으면 예상치 못한 문제들이 발생할 수 있다.


2. 문제 상황

아래 코드는 두 개의 리소스를 생성하고, 하나의 메서드에서 호출하는 과정에서 예외가 발생할 경우를 다룬다.

package network.tcp.autocloseable;

public class ResourceCloseMainV2 {

    public static void main(String[] args) {
        try {
            logic();
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            e.printStackTrace();
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            e.printStackTrace();
        }
    }

    private static void logic() throws CallException, CloseException {
        ResourceV1 resource1 = null;
        ResourceV1 resource2 = null;

        try {
            resource1 = new ResourceV1("resource1");
            resource2 = new ResourceV1("resource2");
            resource1.call();
            resource2.callEx(); // CallException 발생
        } catch (CallException e) {
            System.out.println("ex: " + e);
            throw e; // CallException 다시 던짐
        } finally {
            if (resource2 != null) {
                resource2.closeEx(); // CloseException 발생
            }
            if (resource1 != null) {
                resource1.closeEx();
            }
        }
    }
}

이 코드에서 발생할 수 있는 주요 문제들을 살펴보자.


3. 문제점 분석

❌ 1. null 체크 문제

  • finally 블록을 사용하면 예외가 발생해도 자원 정리 코드가 실행된다.
  • 하지만, 객체를 생성하기 전에 예외가 발생하면 해당 객체는 null 이 된다.
  • 따라서 null 체크를 하지 않으면 NullPointerException 이 발생할 수 있다.

❌ 2. 자원 정리 중 예외 발생 문제

  • finally 블록에서 closeEx() 메서드를 호출하는데, 이 과정에서 예외가 발생하면 그 이후 코드가 실행되지 않는다.
  • 즉, 하나의 리소스 정리 중 예외가 발생하면 다른 리소스는 정리되지 못할 수 있다.

❌ 3. 핵심 예외(CallException)가 사라짐

  • 코드의 핵심 예외는 callEx()에서 발생한 CallException이다.
  • 하지만 finally 블록에서 추가로 CloseException이 발생하면 핵심 예외가 덮어씌워지는 문제가 발생할 수 있다.
  • 최초 발생한 예외가 유지되지 않으면 디버깅이 어렵다.

4. 해결 방법

✅ 1. try-with-resources 사용 (권장)

  • AutoCloseable을 구현하면 try-with-resources 문법을 활용할 수 있다.
  • 자원 정리 과정에서 발생하는 예외를 자동으로 관리하고, null 체크도 필요 없다.
public class ResourceCloseMainV3 {
    public static void main(String[] args) {
        try (ResourceV1 resource1 = new ResourceV1("resource1");
             ResourceV1 resource2 = new ResourceV1("resource2")) {
            resource1.call();
            resource2.callEx(); // CallException 발생
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            e.printStackTrace();
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            e.printStackTrace();
        }
    }
}

✅ 2. suppressed 예외 활용하기

  • try-with-resources리소스 정리 중 발생한 예외를 억제(suppressed)하여 관리할 수 있다.
  • getSuppressed()를 사용하면 숨겨진 예외를 확인할 수 있다.
catch (Exception e) {
    Throwable[] suppressed = e.getSuppressed();
    for (Throwable t : suppressed) {
        t.printStackTrace();
    }
}

✅ 3. finally 블록에서 여러 개의 예외를 안전하게 처리하기

  • try-catch를 추가하여 한 리소스의 정리 실패가 다른 리소스 정리에 영향을 주지 않도록 한다.
finally {
    try {
        if (resource2 != null) resource2.closeEx();
    } catch (Exception e) {
        e.printStackTrace();
    }
    try {
        if (resource1 != null) resource1.closeEx();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

5. 정리

try-with-resources를 사용하면 자원 정리가 자동화되고, 예외 처리도 안전하게 관리 가능

getSuppressed()로 숨겨진 예외까지 추적 가능

finally 블록에서 여러 개의 자원을 정리할 때는 개별 try-catch를 사용하여 다른 자원의 정리 실패를 방지해야 함


자원 관리를 안전하게 처리하는 것은 안정적인 애플리케이션 개발의 핵심이다. try-with-resources와 적절한 예외 처리를 활용하여 더 견고한 코드를 작성하자! 🚀

profile
배움을 추구하는 개발자

0개의 댓글