소개
try-with-resources 구문과 native 자원의 관계 이해
대상 독자: 초보 Java 개발자
자바 관련 스터디를 하고 있는데, 이번엔 try-with-resources와 관련된 주제로 얘길 나누었습니다.
(이 주제는 이펙티브 자바, 자바의 신에서 꼭 등장하는 내용이기도 합니다)
정말 별 것 아닌 것 같지만, 해당 주제로 대화를 나누기 위해 정말 광범위하고 깊은 키워드들이 등장했었습니다.
GC, JVM, JNI, native 영역, FileDescriptor 등등 기본적인 개념에 대해서는 알고 있었으나, 이들의 관계에 대해 간단하게 살펴보고 왜 try-with-resources를 쓰는지에 대해 좀 더 자세하게 알아보겠습니다.
(FileDescriptor는 다른 포스팅에서 다룰 예정이라 이 포스팅에서는 설명하지 않습니다)
단순히 휴먼 실수를 막기 위해 try-with-resources를 쓰는 줄 알고 계셨다면, 걱정 안하셔도 됩니다. 이 글을 통해 좀 더 많은 정보를 알아 가시면 좋을 것 같습니다.
Native 영역의 개념과 특징
네이티브 메소드의 정의
네이티브 메소드란, Java와 같은 고수준 언어로 작성된 프로그램에서 C/C++와 같은 저수준 언어로 작성된 코드를 호출하여 사용하는 방법을 말합니다. 네이티브 메소드는 Java Native Interface (JNI)라는 툴킷을 사용하여 구현됩니다.
네이티브 메소드의 사용 예시
네이티브 메소드를 사용하는 대표적인 예시로는, 파일 입출력, 네트워크 통신, 그래픽 라이브러리 등이 있습니다. 이러한 기능은 하드웨어와 직접적으로 연관되어 있으며, 저수준 언어로 작성될 때 더욱 효율적으로 동작할 수 있습니다.
아직 저는 실무에서 JNI를 이용한 경험이 없습니다만, 사용하신 경험이 있다면 댓글로 공유해주시면 감사하겠습니다.
예를 들어, 자바에서는 파일 입출력을 위해 java.io 패키지를 제공하고 있습니다. 하지만 이러한 기능은 운영체제와 밀접한 관련이 있기 때문에, 이를 JNI를 이용해 C/C++로 구현하고 이를 자바에서 호출하여 사용할 수 있습니다. 이렇게 하면 운영체제와 밀접한 기능을 더욱 빠르고 안정적으로 사용할 수 있습니다.
메모리 누수 등의 위험성
자바는 가비지 컬렉션(Garbage Collection)을 통해 메모리를 관리하기 때문에 개발자가 직접 메모리를 해제할 필요가 없습니다. 하지만 네이티브 메소드에서는 메모리를 직접 할당하고 해제해주어야 합니다. 메모리를 할당한 후 해제하지 않으면 메모리 누수가 발생하게 됩니다.
이러한 이유로 네이티브 메소드를 사용할 때에는 메모리 누수와 같은 위험성을 고려하여 개발자가 메모리를 수동으로 관리하고, 메모리 누수를 방지할 수 있는 방법을 찾아야 합니다. 또한, 메모리 누수와 같은 문제가 발생할 경우에는 빠르게 대처하여 문제를 해결해야 합니다.
try-with-resources 구문 소개
try-catch-finally 구문과의 비교
try-with-resources 구문과 try-catch-finally 구문은 모두 자원 해제를 위해 사용됩니다. 하지만 두 구문은 사용 방법과 동작 방식에서 차이가 있습니다.
참고로, try-with-resources 구문은 java7에서 등장했습니다.
try-catch-finally 구문은 자원 해제 코드를 finally 블록 안에 작성합니다. 이 구문은 예외 발생 여부에 상관없이 finally 블록이 실행되므로 자원 해제가 보장됩니다. 하지만 finally 블록에서 예외가 발생하면 이 예외가 우선적으로 처리되고, try-catch 블록에서 발생한 예외는 무시됩니다. 또한 자원을 해제하는 코드와 예외 처리 코드가 섞여 있어 가독성이 좋지 않습니다.
try-with-resources 구문은 try 블록 안에서 자원을 생성하고, 이를 try-with-resources 괄호 안에 선언합니다. 자원을 해제할 필요가 없으므로 finally 블록이 필요하지 않습니다. try 블록이 끝날 때 자원이 자동으로 해제됩니다. 또한 try-with-resources 구문은 예외 처리에 대한 가독성과 안전성이 좋습니다.
// try-catch-finally 구문 사용
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("imksh.txt");
// 파일 읽기 로직
} catch (IOException e) {
// 예외 처리
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// 예외 처리
}
}
}
// try-with-resources 구문 사용
try (FileInputStream inputStream = new FileInputStream("imksh.txt")) {
// 파일 읽기 로직
} catch (IOException e) {
// 예외 처리
}
위 코드를 비교해보면, try-with-resources 구문을 사용했을 때 코드가 더 간결해지고 가독성이 좋아집니다. 또한 자원 해제 코드를 별도로 작성하지 않아도 되므로 실수로 자원을 놓치는 일이 없어져 안전성이 높아집니다.
자원 해제를 자동으로 처리하는 방법
try-with-resources 구문을 사용하면, try 블록 내에서 자원을 생성하고, 이 자원을 사용한 이후에는 자동으로 자원을 해제해줍니다. 이 때, 사용한 자원은 AutoCloseable 인터페이스를 구현한 클래스여야 합니다.
FileInputStream도 AutoCloseable 인터페이스를 구현하는 객체라는 점 참고해주세요.
참고: https://docs.oracle.com/javase/7/docs/api/java/io/FileInputStream.html
try-with-resources 구문을 사용하여 파일을 읽는 코드를 작성하면 아래와 같습니다.
try (FileInputStream inputStream = new FileInputStream("imksh.txt")) {
// 파일 읽기 로직
} catch (IOException e) {
// 예외 처리
}
try-with-resources 구문의 장점
사실 장점은 위에서 언급한 것 처럼 명확하기에, 구구절절 설명할 정도는 아닙니다.
우선 장점을 설명하기 전에 계속 언급되었던 try-catch-finally와 관계를 짚고 가야하는데요,
사실 따지고 보면 try-with-resources는 try-catch-finally 입니다..
try-with-resources의 민낯
try (FileInputStream inputStream = new FileInputStream("imksh.txt")) {
// 파일 입출력 수행
}
위와 같은 resources문이 있다고 가정해 봅시다.
이는 컴파일러에 의해 아래와 같이 민낯을 드러내게 됩니다.
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("imksh.txt");
// 파일 입출력 수행
} catch (IOException e) {
// 예외 처리
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// 예외 처리
}
}
}
많이.. 본.. 구문이죠?
네, 사실 try-with-resources는 try-catch-finally와 동일합니다.
하지만 개발자가 직접 작성하는 코드는 try-catch-finally에 비해 명확하게 줄어든 것을 볼 수 있고, 개발자가 실수할 걱정도 줄어드니 명확한 장점이라고 말할 수 있겠습니다.
버전 별 차이점
위에서 살짝 언급 했듯이, try-with-resources 구문은 Java7에서 최초로 등장했습니다.
이후 Java9버전에서 약간의 업데이트가 있었는데요, 해당 내용을 간단하게 알아보겠습니다.
try-with-resources 구문에서 중첩된 리소스를 사용할 수 있게 되었습니다. 이전 버전에서는 try-with-resources 구문에서 중첩된 리소스를 사용할 수 없었지만, Java 9에서는 중첩된 리소스도 사용할 수 있게 되어, 코드의 가독성이 향상되었습니다.
다음으로는 변수에 대한 제한이 완화되어, try-with-resources 구문에서 리소스를 선언하고 초기화할 때, 변수의 타입이 서로 다른 경우도 허용됩니다. 예를 들어, 파일과 네트워크 소켓을 동시에 사용할 수 있게 되었습니다.
try (FileInputStream input = new FileInputStream("imksh.txt");
Socket socket = new Socket("localhost", 8080);
InputStreamReader streamReader = new InputStreamReader(socket.getInputStream())) {
// 파일에서 데이터를 읽어와서 네트워크 소켓을 통해 전송
} catch (IOException e) {
// 에러 메세지 출력
}
원래는 다른 리소스인 경우 try문을 중첩해서 선언해야 했는데, 많이 편리해졌습니다.
마지막으로 final 또는 effectively final이 아닌 리소스를 사용할 수 있게 되었습니다. 이전 버전에서는 try-with-resources 구문에서 사용할 리소스는 final 또는 effectively final 이어야 했습니다. 하지만 Java 9에서는 final 또는 effectively final이 아닌 리소스도 사용할 수 있게 되어, 코드의 유연성이 향상되었습니다.
주의사항과 최적화 방법
try-with-resources 구문 사용 시 주의할 점
- 자원 객체를 생성할 때, 이 객체가 AutoCloseable 인터페이스를 구현하고 있어야 합니다. 그렇지 않으면 컴파일 오류가 발생합니다.
- 생성된 자원 객체는 구문이 끝난 후 자동으로 close() 메소드가 호출되므로, 해당 자원 객체에 대해 직접 close() 메소드를 호출하면 안됩니다.
- 생성된 자원 객체는 구문이 실행되는 동안에만 유효합니다. 즉, try-with-resources 구문이 끝나면 해당 자원 객체는 소멸되며, 이후에는 사용할 수 없습니다.
따라서, try-with-resources 구문을 사용할 때는 AutoCloseable 인터페이스를 구현한 자원 객체를 사용해야 하며, close() 메소드를 직접 호출하지 않아야 합니다. 또한, 자원 객체의 범위는 try-with-resources 구문이 실행되는 동안에만 유효하므로, 이를 고려하여 코드를 작성해야 합니다.
결론
try-with-resources 구문과 네이티브 자원의 관계에 대한 정리
try-with-resources 구문은 자원 해제를 자동으로 처리하는 기능을 제공하는 Java의 문법 중 하나입니다. 이 구문을 사용하면 코드의 가독성과 안전성을 높일 수 있습니다.
네이티브 자원은 Java 외부에서 관리되는 자원으로, Java 코드에서는 직접 해제할 수 없으며 JNI(JVM Native Interface)를 사용하여 해제할 수 있습니다. try-with-resources 구문은 Java 자원에 대해서만 자동으로 자원 해제를 처리할 뿐, 네이티브 자원에 대해서는 직접 해제해야 합니다.
따라서 네이티브 자원을 사용하는 경우에도 try-with-resources 구문을 사용할 수 있지만, 자원이 해제되지 않을 수 있으므로 반드시 예외 처리와 함께 네이티브 자원을 수동으로 해제해야 합니다. 이를 위해 try-catch-finally 구문을 사용할 수 있습니다.
장단점 및 활용 방법 요약
장점
- 자원 해제를 자동으로 처리할 수 있어, 코드의 간결성과 가독성이 좋아집니다.
- 예외처리에 대한 실수를 방지할 수 있습니다.
단점
- try-with-resources 구문이 복잡한 경우, 코드가 더 복잡해질 수 있습니다.
하지만 Java9에서 업데이트 되어 try문이 중첩된다던가 하는 역할은 온전히 컴파일러의 역할이 되었기에, 꽤 완화되었다고 볼 수 있겠습니다.
- try-with-resources 구문에서 처리하는 자원이 많은 경우, 성능이 떨어질 수 있습니다.
자원을 초기화하고 제거하는 작업에도 시간이 들어가는데, 시간이 오래 걸리는 I/O 작업이거나, 네트워크 연결과 같은 외부 자원을 사용하는 경우 성능이 저하될 수 있습니다. 단점으로 구분하기엔 좀 애매하긴 합니다만 개발할 때 이점을 유의해서 구현하면 되겠습니다.
활용 방법
- try-with-resources 구문을 사용하여 자원을 해제하는 경우, 자원이 적은 경우가 적합합니다.
- try-with-resources 구문을 사용할 수 없는 경우, try-catch-finally 구문을 사용하여 자원을 해제하는 것이 좋습니다.
- 네이티브 자원을 해제하는 경우, JNI를 사용하여 자원을 해제해야 합니다.
- try-with-resources 구문에서 처리하는 자원이 많은 경우, 별도의 스레드를 사용하여 처리할 수 있습니다.
스터디 중 나왔던 질문인데,
만약, try-with-resources 수행 중 프로그램(애플리케이션)이 강제 종료되면, close가 실행될 수 있을까요?
된다면 왜, 안된다면, OS에서는 어떻게 처리가 될까요?
'Tech > Java&Spring' 카테고리의 다른 글
멀티스레드 분산 환경에서의 로깅(1) (0) | 2023.05.21 |
---|---|
JPA saveAll이 Bulk INSERT 되지 않았던 이유 (3) | 2023.04.05 |
[SpringBoot] Intellij spring boot 프로젝트 생성 방법 (0) | 2022.01.10 |
[Springboot] 민감정보 숨기기 - Argument 입력 (0) | 2021.11.30 |
Spring 환경에서 Docker run으로 jar에 argument 전달하기 (2) | 2021.11.24 |
인프런 지식공유자로 활동하고 있으며 MSA 전환이 취미입니다. 개발과 관련된 다양한 정보를 몰입감있게 전달합니다.