try-with-resources 안에서 쓰이지 않던 stream
Spring Boot의 Gradle plugin을 두고 issue가 올라와 있었습니다. 실행 가능한 jar를 만들 때 같은 파일을 필요한 것보다 한 번 더 열고 있다는 내용이었습니다. 코드를 보면 열어 놓고 아무 데도 쓰지 않는 stream이 하나 있었습니다.
writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false, (entry) -> {
/*
* 여기서 연 in은 아래에서 쓰이지 않는다.
* 아래 줄은 stream을 새로 하나 더 연다.
*/
try (InputStream in = library.openStream()) {
prepareStoredEntry(library.openStream(), false, entry);
}
});try 괄호 안에서 in을 열어 두고도 블록 안에서는 한 번도 쓰지 않은 채 library.openStream()을 또 호출합니다.
왜 stream을 여러 번 여는가
먼저 stream을 한 번만 열면 안 되는 이유부터 봐야 합니다.
jar는 ZIP 형식입니다. ZIP에 파일을 넣는 방식에는 압축해서 넣는 방식과, 압축 없이 그대로 넣는 stored 방식이 있습니다. stored 방식으로 넣으려면 내용을 쓰기 전에 크기와 CRC 값을 먼저 적어야 합니다.
그래서 파일을 두 번 읽게 됩니다.
- 한 번은 크기와 CRC를 계산하려고 읽습니다.
prepareStoredEntry가 이 일을 합니다. - 또 한 번은 실제 내용을 jar에 써 넣으려고 읽습니다.
여기까지는 필요한 일입니다. 그런데 그 사이에 세 번째로 여는 stream이 끼어 있었습니다.
쓰이지 않는 stream이 생긴 이유
try-with-resources는 괄호 안에서 연 자원을 블록이 끝날 때 닫아 줍니다. 이 코드를 쓴 의도는 “prepareStoredEntry에 넘기는 stream을 확실히 닫자"였을 것입니다.
하지만 괄호 안에서 연 in과 prepareStoredEntry에 넘긴 stream은 서로 다른 것입니다. in은 열렸다가 아무 일도 하지 않은 채 닫힙니다. 넘긴 stream은 try-with-resources가 닫는 대상에서 빠져 있습니다.
그래도 prepareStoredEntry가 넘겨받은 stream을 스스로 닫기 때문에 자원이 새지는 않았습니다. 안쪽에서 이 일을 하는 StoredEntryPreparator를 보면 알 수 있습니다.
StoredEntryPreparator(InputStream inputStream, boolean unpack) throws IOException {
this.messageDigest = (unpack) ? sha1Digest() : null;
// 넘겨받은 stream을 여기서 닫는다
try (inputStream) {
// 크기와 CRC를 계산한다
load(inputStream);
}
}닫아 주려고 넣은 블록은 아무 효과 없이 파일만 한 번 더 열고 있었습니다.
고친 내용
writeJarModeLibrary()에서 블록을 걷어 냈습니다.
-writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false, (entry) -> {
- try (InputStream in = library.openStream()) {
- prepareStoredEntry(library.openStream(), false, entry);
- }
-});
+writeEntry(name, ZipEntryContentWriter.fromInputStream(library.openStream()), false,
+ (entry) -> prepareStoredEntry(library.openStream(), false, entry));동작은 같고 library마다 파일을 여는 횟수만 한 번 줄어듭니다.
마치며
try-with-resources는 괄호 안에 선언한 변수만 닫아 줍니다. 이번 코드에서는 블록 안에서 새로 연 자원을 prepareStoredEntry가 대신 닫고 있었습니다.
자원을 닫는 코드를 볼 때는 무엇을 닫고 있는지와 함께 넘겨받은 쪽이 이미 닫고 있지는 않은지도 확인해야 한다는 것을 배웠습니다.