지난 데브코스 도서 관리 과제를 하던 중 멘토님께 받은 코드 리뷰 피드백이 하나 있었다.
public List<Book> read() {
try (Reader reader = new FileReader(path)) {
Type listType = new TypeToken<List<Book>>() {
}.getType();
List<Book> books = gson.fromJson(reader, listType);
return Optional.ofNullable(books).orElse(new ArrayList<>());
} catch (Exception e) {
e.printStackTrace();
}
return new ArrayList<>();
}
저기 try안에 넣은 구문은 뭐죠? 현재 reader를 close하는 부분이 없는데 그럼 이 코드는 잘못 작성된 것일까요?
해당 코드의 경우 블로그와 문서를 찾아보며 작성한 것이다보니 그런 생각을 깊게 하지 못하고 코드를 작성했다.
멘토님께서 try-with-resouces 라는 키워드를 힌트로 주시며 공부해볼 것을 권해주셔서 이렇게 포스팅하게 된다.
* 기존의 try-catch-finally
파일을 읽는 코드를 보며 이해해보자.
public void writeFile() {
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(filePath);
List<String> fileStrings = convertObjectListsToFileStrings(store.values());
for (String str : fileStrings) {
outputStream.write(str.getBytes());
outputStream.write(System.lineSeparator().getBytes());
}
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
뭔가 벌써부터 어지럽다.
우선 파일을 읽는데 예상치 못한 오류가 발생할 수 있기 때문에 catch 문으로 불러와 예외처리를 해주고 있다.
그리고 해당 리소스를 사용했으면 반드시 닫아주어야하기 때문에 finally문에 close를 해주고 있다.
그런데 여기서 문제가 되는 지점이 생긴다.
ouputStream.close(), 즉 파일을 닫는 동작 코드 자체도 예외가 일어날 수 있는 코드이기 때문에 해당 부분도 예외 처리를 해줘야 한다.
겨우 파일 하나 열고 닫는 것인데 너무 코드가 난잡하고 가독성이 안좋아졌다.
또 다른 코드를 봐보자.
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
해당 코드에서 firstLineOfFile 함수를 봐보자. 시스템 문제로 인해 예외가 try와 finally 부분 모두에서 발생할 수 있다.
이 때 두번째 예외(finally의 close())가 첫번째 예외(try의 readLine())를 완전히 덮어버리게 된다.
이렇게 되면 스택 추적이 어렵게 된다.
* 문제 해결을 위한 try-with-resource
try-with-resource 문법은 자원을 자동으로 반납해주는 기능을 포함하고 있다.
사용법은 try 블록에 ()를 추가해서 파일을 열거나 자원을 할당하는 명령문을 명시하면 된다.
그러면 try 블록이 끝나자마자 자동으로 파일을 닫거나, 할당된 자원을 해제해준다.
우선 해당 구문을 적용해서 바꾼 코드를 봐보자.
public void writeFile() {
try (FileOutputStream outputStream = new FileOutputStream(filePath)) {
List<String> fileStrings = convertObjectListsToFileStrings(store.values());
for (String str : fileStrings) {
outputStream.write(str.getBytes());
outputStream.write(System.lineSeparator().getBytes());
}
outputStream.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
우선 확실히 간결해졌다. close하는 부분을 이제 넣어주지 않아도 되고, 닫기 위해 또 예외처리를 해줄 필요가 없어졌다.
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
또 해당 함수를 수정하고 호출할 시 아까 문제가 발생할 수 있는 예외가 덮어씌워지는 문제도 해결할 수 있다.
try-with-resource가 알아서 close를 호출해주며, close 호출에서 예외가 발생했을 때 close에서 발생한 예외는 숨겨지고 첫번째 예외가 기록된다.
그리고 숨겨진 예외들은 스택 추적 내역에서 suppressed라는 꼬리표를 달고 출력이 된다.
* 결론
꼭 회수하거나 close해야하는 자원을 다룰 때는 try-with-resource를 사용하자.
코드도 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다.
'Java' 카테고리의 다른 글
collect(Collectors.toList()) vs Stream.toList() (0) | 2023.11.27 |
---|---|
SOLID(의존성을 잘 관리해보자!!) (0) | 2023.09.25 |
If문 제거하기 (리펙토링) (0) | 2023.09.21 |
Stream (0) | 2023.09.03 |