코드 한 줄의 기록

Java try-catch-finally와 try-with-resources로 자원 누수 완벽 방지하기 본문

JAVA

Java try-catch-finally와 try-with-resources로 자원 누수 완벽 방지하기

CodeByJin 2025. 10. 15. 07:32
반응형

자바로 개발하면서 가장 중요하지만 종종 놓치기 쉬운 부분이 바로 자원 관리입니다. 파일을 열거나 데이터베이스에 연결할 때, 사용이 끝나면 반드시 닫아줘야 하는데 이를 제대로 하지 않으면 메모리 누수가 발생합니다. 저도 초기에는 이런 실수를 많이 했었는데, 오늘은 여러분과 함께 try-catch-finally와 try-with-resources를 활용한 완벽한 자원 관리 방법을 알아보겠습니다.

자원 누수란 무엇인가?

자원 누수(Resource Leak)는 프로그램이 사용한 시스템 자원을 제대로 해제하지 않아 발생하는 문제입니다. 자바에서 파일, 네트워크 연결, 데이터베이스 연결 등의 외부 자원을 사용할 때 발생할 수 있는 심각한 문제죠.

가비지 컬렉터(GC)가 있다고 해서 모든 자원이 자동으로 해제되는 것은 아닙니다. GC는 메모리만 관리하고, 외부 자원은 명시적으로 해제해야 합니다. 자원을 해제하지 않으면 시스템 리소스가 고갈되어 성능 저하나 프로그램 오작동이 발생할 수 있습니다.

// 잘못된 예제 - 자원 누수 발생 가능

FileInputStream fis = new FileInputStream("example.txt");

// 파일 읽기 작업

// close() 호출 없음 - 자원 누수 발생!

try-catch-finally의 기본 개념과 활용

finally 블록의 역할

finally 블록은 예외 발생 여부와 관계없이 항상 실행되는 코드 블록입니다. 이 특성을 활용해 자원 해제 코드를 작성하면 안전한 자원 관리가 가능합니다.

// 기본 try-catch-finally 구조

FileInputStream fis = null;

try {

    fis = new FileInputStream("data.txt");

    // 파일 읽기 작업

} catch (IOException e) {

    System.err.println("파일 읽기 오류: " + e.getMessage());

} finally {

    if (fis != null) {

        try {

            fis.close(); // 자원 해제

        } catch (IOException e) {

            System.err.println("파일 닫기 오류: " + e.getMessage());

        }

    }

}

다중 자원 관리의 복잡성

여러 자원을 사용할 때는 코드가 더욱 복잡해집니다. 각각의 자원에 대해 null 체크와 예외 처리를 해야 하기 때문입니다.

FileInputStream fis = null;

FileOutputStream fos = null;

try {

    fis = new FileInputStream("input.txt");

    fos = new FileOutputStream("output.txt");

    // 파일 처리 로직

} catch (IOException e) {

    e.printStackTrace();

} finally {

    if (fis != null) {

        try {

            fis.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

    if (fos != null) {

        try {

            fos.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

이런 방식의 문제점은 명확합니다.
- 코드의 복잡성: 자원이 많을수록 코드가 길어짐
- 실수 가능성: 자원 해제를 깜빡할 수 있음
- 중첩된 예외 처리: 자원 해제 중 발생한 예외 처리의 어려움

try-with-resources: 자동 자원 관리의 혁신

AutoCloseable 인터페이스의 이해

자바 7에서 도입된 try-with-resources는 AutoCloseable 인터페이스를 기반으로 작동합니다. 이 인터페이스를 구현한 모든 객체는 try-with-resources 구문에서 자동으로 닫힙니다.

public interface AutoCloseable {

    void close() throws Exception;

}

AutoCloseable과 Closeable의 차이점
- AutoCloseable: Exception을 던질 수 있음, 더 일반적인 용도
- Closeable: IOException만 던짐, 주로 I/O 자원용

try-with-resources 기본 사용법

try-with-resources의 기본 구조는 매우 간단합니다.

try (FileInputStream fis = new FileInputStream("example.txt")) {

    // 파일 읽기 작업

    int data = fis.read();

    System.out.println(data);

} catch (IOException e) {

    System.err.println("파일 처리 오류: " + e.getMessage());

}

// 자동으로 fis.close() 호출됨

이 방식의 장점은 명확합니다.
- 자동 자원 해제: try 블록이 끝나면 자동으로 close() 호출
- 예외 안전성: 예외가 발생해도 자원이 확실히 해제됨
- 코드 간결성: finally 블록이 필요 없음

다중 자원 관리

여러 자원을 동시에 관리할 때도 훨씬 간단합니다.

try (FileInputStream fis = new FileInputStream("input.txt");

     FileOutputStream fos = new FileOutputStream("output.txt");

     BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {

    

    String line;

    while ((line = reader.readLine()) != null) {

        fos.write(line.getBytes());

    }

} catch (IOException e) {

    System.err.println("파일 처리 오류: " + e.getMessage());

}

// 모든 자원이 자동으로 역순으로 해제됨

자원은 선언된 순서의 역순으로 해제됩니다. 위 예제에서는 reader → fos → fis 순으로 닫힙니다.

실제 개발에서의 활용 사례

데이터베이스 연결 관리

데이터베이스 작업에서는 Connection, Statement, ResultSet 등 여러 자원을 관리해야 합니다.

String url = "jdbc:mysql://localhost:3306/mydb";

String query = "SELECT * FROM users WHERE age > ?";

try (Connection conn = DriverManager.getConnection(url, "user", "password");

     PreparedStatement pstmt = conn.prepareStatement(query)) {

    

    pstmt.setInt(1, 18);

    

    try (ResultSet rs = pstmt.executeQuery()) {

        while (rs.next()) {

            System.out.println("사용자: " + rs.getString("name"));

        }

    }

} catch (SQLException e) {

    System.err.println("데이터베이스 오류: " + e.getMessage());

}

네트워크 자원 관리

소켓 통신에서도 try-with-resources를 활용할 수 있습니다.

try (Socket socket = new Socket("localhost", 8080);

     PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

     BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

    

    out.println("Hello Server!");

    String response = in.readLine();

    System.out.println("서버 응답: " + response);

    

} catch (IOException e) {

    System.err.println("네트워크 오류: " + e.getMessage());

}

자원 누수 방지를 위한 베스트 프랙티스

항상 try-with-resources 사용하기

AutoCloseable을 구현한 자원은 무조건 try-with-resources를 사용하세요.

// 권장하지 않음

Scanner scanner = new Scanner(System.in);

// 사용 후 close() 호출 잊기 쉬움

// 권장

try (Scanner scanner = new Scanner(System.in)) {

    String input = scanner.nextLine();

    // 자동으로 close() 호출됨

}

사용자 정의 자원 클래스 구현

자신만의 자원 클래스를 만들 때도 AutoCloseable을 구현하세요.

public class MyResource implements AutoCloseable {

    private boolean closed = false;

    

    public void doSomething() {

        if (closed) {

            throw new IllegalStateException("자원이 이미 닫혔습니다.");

        }

        System.out.println("자원 사용 중...");

    }

    

    @Override

    public void close() throws Exception {

        if (!closed) {

            System.out.println("자원 해제 중...");

            closed = true;

            // 실제 자원 해제 로직

        }

    }

}

// 사용 예

try (MyResource resource = new MyResource()) {

    resource.doSomething();

} // 자동으로 close() 호출

예외 상황 고려하기

자원 해제 중 발생하는 예외도 고려해야 합니다.

try (FileInputStream fis = new FileInputStream("nonexistent.txt")) {

    // 파일 처리

} catch (FileNotFoundException e) {

    System.err.println("파일을 찾을 수 없습니다: " + e.getMessage());

} catch (IOException e) {

    System.err.println("I/O 오류: " + e.getMessage());

}

성능과 메모리 관리 측면에서의 고려사항

GC와의 관계

try-with-resources를 사용하면 GC의 부담을 줄일 수 있습니다. 자원이 즉시 해제되므로 GC가 정리할 객체가 줄어들기 때문입니다.

// 메모리 효율적인 파일 처리

public void processLargeFile(String filename) {

    try (BufferedReader reader = Files.newBufferedReader(Paths.get(filename))) {

        reader.lines()

              .filter(line -> line.contains("error"))

              .forEach(System.out::println);

    } catch (IOException e) {

        System.err.println("파일 처리 오류: " + e.getMessage());

    }

    // 파일이 즉시 닫히므로 메모리 효율적

}

대용량 데이터 처리

대용량 파일이나 데이터를 처리할 때는 더욱 중요합니다.

public void processLargeDataset() {

    try (Stream<String> lines = Files.lines(Paths.get("huge-file.txt"))) {

        long errorCount = lines

            .parallel()

            .filter(line -> line.contains("ERROR"))

            .count();

        System.out.println("총 오류 수: " + errorCount);

    } catch (IOException e) {

        System.err.println("파일 처리 실패: " + e.getMessage());

    }

    // 스트림이 자동으로 닫히므로 메모리 누수 방지

}

자바에서 자원 관리는 안정적인 애플리케이션 개발의 핵심입니다. try-catch-finally에서 try-with-resources로의 발전은 단순히 코드를 간결하게 만드는 것 이상의 의미가 있습니다. 자원 누수를 방지하고, 예외 상황에서도 안전한 자원 해제를 보장하며, 코드의 가독성을 크게 향상시킵니다.

핵심 포인트를 정리하면

  • try-catch-finally: 전통적인 방식이지만 복잡하고 실수하기 쉬움
  • try-with-resources: 자바 7부터 지원, AutoCloseable 구현 객체 자동 해제
  • 자원 누수 방지: 메모리 효율성과 시스템 안정성 확보
  • 베스트 프랙티스: 항상 try-with-resources 사용, 예외 상황 고려

앞으로 자바 개발을 할 때는 외부 자원을 사용하는 모든 상황에서 try-with-resources를 적극 활용해보세요. 처음에는 익숙하지 않을 수 있지만, 한번 습관이 되면 더 안전하고 깔끔한 코드를 작성할 수 있을 것입니다. 여러분의 자바 개발 여정에 이 글이 도움이 되기를 바랍니다!

Java 예외 처리 마스터하기: 체크/언체크 예외부터 throw/throws까지 완전 정복

안녕하세요! 오늘은 Java 개발자라면 꼭 알아야 할 예외 처리에 대해 이야기해보려고 합니다. 사실 저도 처음 Java를 배울 때 예외 처리가 참 헷갈렸는데요, 이번 기회에 다시 한번 정리하면서 여

byteandbit.tistory.com

반응형