이펙티브 자바의 첫 시작은 객체를 생성하고 파괴하는 것에 대한 고찰이다.
"2장 - 객체의 생성과 파괴" 는 다음과 같은 기준으로 맥락을 잡고 있다.
- 객체를 만들어야 할 때는 언제인가
- 객체를 만들지 말아야 할 때는 언제인가
- 올바른 객체 생성 방법은 무엇인가
- 객체의 불필요한 생성을 피하는 방법은 무엇인가
- 객체를 제 때에 파괴시키는 방법은 무엇인가
- 파괴 전에 수행해야 할 정리 작업을 관리하는 요령이 있는가
위와 같은 맥락을 계속 기억하며 공부하자.
Item9는 사실상 Item8의 연장선이다.
Item8가 객체를 소멸하는 일반적인 방법의 문제점을 말하고 있다면,
Item9는 그 상황에 대한 해결책을 제시하고 있다고 생각하면 된다.
앞서 말했듯이 분배되었던 자원을 효율적으로 수거하는 방법은,
AutoClosable 하게 객체를 구현한 후 클라이언트에서 close로 닫아주는 것이다.
문제는, 클라이언트가 close로 자원을 닫아주는 것만큼 확실한것도 없지만,
그만큼 놓치기 쉬운 부분이 될 수도 있다.
인스턴스를 사용한 후 close 해야겠다는 생각을 항상 하고있는 프로그래머가 세상에 얼마나 되겠는가..
close를 안하게 될 경우 결국 자원 회수가 안되기 때문에 성능 저하로 연결될 수가 있다.
Item8에서는 이런 경우를 위한 안정망 보험용으로 finalizer와 cleaner를 이야기 하고 있지만,
이미 말했듯 딱히 믿음이 가는 방법은 아니다.
이제부터, 자원이 제대로 닫혔는지를 보장하기위해 사용되는 것들을 알아보려고 한다.
전통적으로 사용되는 방법은 바로 try-finally 구문이였다.
다음과 같은 예시를 보자.
public class TopLine { static String firstLineOfFile(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { br.close(); } }
public static void main(String[] args) throws IOException { String path = args[0]; System.out.println(firstLineOfFile(path)); } }
사용자에게 입력을 받기 위한 아주 간단한 예시이다.
try 문을 통해 입력을 받고,
finally 구문에서 입력 객체를 close 한다.
단순히 위의 예시만 본다면 군더더기 없다.
하지만 사용하는 자원이 두개가 된다면 어떻게 할까?
다음의 예시를 보면 느낌이 확 올 것이다.
public class Copy { private static final int BUFFER_SIZE = 8 * 1024; static void copy(String src, String dst) throws IOException { InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutputStream(dst); try { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } finally { out.close(); } } finally { in.close(); } }
public static void main(String[] args) throws IOException { String src = args[0]; String dst = args[1]; copy(src, dst); } }
어떤가? 위와는 다르게 이번엔 입력과 출력을 둘 다 하고 싶은 코드이다.
그래서 입력 객체와 출력 객체, 두개의 자원을 사용하고 있다.
또 자원이 두개이기 때문에 중첩된 try-finally를 사용하고 있다.
중첩하여 사용한다는 것 자체가 안된다는 것은 아닌데 일단 두가지 문제가 발생한다.
첫번째는 순식간에 코드가 난잡해졌다는 것이다.
가독성도 떨어진다.
두번째는 세밀한 예외 처리가 불가능하다는 것이다.
만약 try문과 finally문 둘 다에서 예외가 발생할 경우,
두번째 예외가 첫번째 예외를 집어삼키게 된다는 결점이 있다.
이럴 경우, 정확한 예외 추적이 불가능해지면서 디버깅 자체가 난항을 겪을 수 있다.
위의 try-finally 문제를 해결한 것이 바로 자바 7에서 등장한
try-with-resources이다.
앞서 봤던 예시를 바꿔본 것이다.
public class TopLine { static String firstLineOfFile(String path) throws IOException { try (BufferedReader br = new BufferedReader( new FileReader(path))) { return br.readLine(); } }
public static void main(String[] args) throws IOException { String path = args[0]; System.out.println(firstLineOfFile(path)); } }
차이점이 보이는가?? 바로 try 안에 파라미터로 자원을 넘겨주는 방식이다.
자원이 한개라서 크게 와닿지 않는 사람들을 위해 다음의 예시도 보자
public class Copy { private static final int BUFFER_SIZE = 8 * 1024; static void copy(String src, String dst) throws IOException { try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } }
public static void main(String[] args) throws IOException { String src = args[0]; String dst = args[1]; copy(src, dst); } }
이제는 조금 명확하게 느껴질 것이다.
try 문 안에 파라미터로 넘기는 방식을 취함으로써,
자원이 몇개던 전체적인 코드는 복잡해지지 않는다.
try-with-resources 절에서도 catch 문을 사용하는 것이 가능하다.
다음의 예시를 보자.
public class TopLineWithDefault { static String firstLineOfFile(String path, String defaultVal) { try (BufferedReader br = new BufferedReader( new FileReader(path))) { return br.readLine(); } catch (IOException e) { return defaultVal; } }
public static void main(String[] args) throws IOException { String path = args[0]; System.out.println(firstLineOfFile(path, "Toppy McTopFace")); } }
위처럼 try 문에서 발생할 수 있는 예외를 처리하도록 catch문을 사용하였다.
Item9에 대한 필자의 코멘트를 마지막으로 글을 마무리한다.
<Item9 정리>
- 꼭 회수해야 하는 자원을 다룰 때는 try-with-resources를 쓰자.
- 예외는 없다. 코드는 더 짧고 분명해진다.
- 만들어지는 예외 정보도 훨씬 유용하다.