티스토리 뷰

자바를 이용해 외부 자원에 접근하는 경우 한가지 주의해야할 점은 외부자원을 사용한 뒤 제대로 자원을 닫아줘야한다는 점이다.


public static void main(String[] args) {
FileInputStream fis = null;
try{
fis = new FileInputStream("");
}catch(IOException e){

}finally {
fis.close();
}
}


이정도로 끝나면 좋겠지만 문제는 저 close() 메서드도 Checked Exception 인 IOException 을 던지므로 이런 코드가 나타난다.


public static void main(String[] args) {
FileInputStream fis = null;
try{
fis = new FileInputStream("");
}catch(IOException e){

}finally {
try {
fis.close();
}catch (IOException ie){

}
}
}


지금 코드에서 fis가 null일 확률은 없지만 try 절에 로직이 더 추가되고 앞단에서 예외가 발생한다면 fis가 null일수도있으므로 그런코드에는 분기문이 하나 더 추가된다.


FileInputStream fis = null;
try{
fis = new FileInputStream("");
}catch(IOException e){

}finally {
try {
if(fis != null) {
fis.close();
}
}catch (IOException ie){

}
}


이런 코드는 장황하고 보기힘들며, 이해하기 어렵고 중첩적이다. 그리고 무엇보다 개발자가 실수하기 쉽다. 그래서 JDK1.7에는 AutoCloseable 인터페이스가 추가됐다.


/**
* @author Josh Bloch
* @since 1.7
*/
public interface AutoCloseable {
void close() throws Exception;
}

(author 를 보니 이펙티브 자바로 유명한 조슈아 블로크가 만들었나보다.)


인터페이스 추가와 함께 소소한 문법적 추가도 이뤄졌는데 try 절에 ()가 들어갈 수 있게됐다.


public static void main(String[] args) {
try(FileInputStream fis = new FileInputStream("")){

}catch(IOException e){

}
}


이런식으로 try(){} 형태로 사용이 가능하며 () 안에 들어올 수 있는건 AutoCloseable 구현체뿐이다. 이런 문법을 try with resources 라고 부른다. 저렇게하면 무슨 이점이 있을까? 매우 직관적인 인터페이스 명칭이나 위 예제를 보고 눈치챌수도있겠지만 try catch 절이 종료되면서 자동으로 close() 메서드를 호출해준다. 이 기능은 참 편리하고 좋은 기능인데 1.7에서 문법적 변화가 없다는 생각과함께 잘 모르는 사람들이 생각보다 많다. 자신의 개발환경이 1.7 이상이라면 유용하게 사용해보자.


나는 이 기능을 유용하게 잘 사용하고있는데 최근 이런 코드를 작성할 일이 생겼다.


try(BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

} catch (IOException e) {

}


try() 안에 중첩적으로 스트림 객체를 생성한것이다. 처음엔 아무생각없이 작성했었는데 보다보니 궁금해졌다. 저럴경우 BufferedReader 외의 자원들은 전부 자동으로 닫힐까?

테스트하기는 어렵지않다. 테스트해보자.


class A implements AutoCloseable{
@Override
public void close() throws Exception {
System.out.println("a call");
}
}

class B implements AutoCloseable{
public B(A a){

}

@Override
public void close() throws Exception {
System.out.println("b call");
}
}


AutoCloseable만 구현해주면 되므로 어려울건 전혀 없다.


public static void main(String[] args) {
try(B b = new B(new A())){

}catch(Exception e){

}
}


저상태 그대로 실행시키면 "b call" 만 실행시키는걸 볼 수 있다. 아.. A는 호출되지않는구나. A도 닫을 수 있게 작성하자. try() 안에는 여러줄의 코드를 삽입할 수 있다.


public static void main(String[] args) {
try(A a = new A();
B b = new B(a)){

}catch(Exception e){

}
}


실행해보면 둘 다 close() 메서드가 실행된걸 볼 수 있다.


try with resources 기능은 아무리 생각해봐도 없는것 보단 있는게 나은 유용한 기능이다. 하지만 변수를 만들지 않은 상태로 사용할 경우 close() 메서드가 호출되지않아 자원이 수거되지않으니 꼭 유념하고 사용하자.


#2017.09.29 추가

포스팅을 유심히 살펴봐주신 한분이 댓글을 달아주셨다. Java io의 Stream 클래스들은 내부변수에 대한 close()를 호출한다는 내용이다. (댓글 달아주신분이 링크걸어주신 내용 https://stackoverflow.com/questions/1388602/do-i-need-to-close-both-filereader-and-bufferedreader)


이글을 보고 생각해본결과 내가 한가지 간과한 구현이 있다는걸 알았다. 위 예제인 A, B 클래스를 보면 알겠지만 try() 문에 들어갈 수 있는 클래스는 특별한 클래스가 아니다. AutoCloseable 인터페이스만 구현해주면 어떤 클래스나 들어갈 수 있다. 그런데 어떻게 스트림은 저런식의 처리를 해줄필요가 없을까? 이 해답은 try() 문에 있는게 아니고 Stream의 구현에 있다. A, B 예제는 B 클래스가 생성자 인자로 A를 받은 후 아무것도 하지않고 있다. 예제코드라 그렇지만 보통 B 클래스같은경우 A를 내부 필드로 보관하게 될것이다.


class B implements AutoCloseable {
private A a;

public B(A a) {
this.a = a;
}

@Override
public void close() throws Exception {
System.out.println("b call");
}
}


그리고 자신의 close()가 호출될때 내부 필드의 대한 close()도 호출하는 식으로 구현한다면 try()가 해주는것과는 별개로 B가 A의 close()를 호출하게된다.


class B implements AutoCloseable {
private A a;

public B(A a) {
this.a = a;
}

@Override
public void close() throws Exception {
this.a.close();
System.out.println("b call");
}
}


결론은 try() 문은 자신의 변수가 아닌 객체의 close()를 호출해주진 않는다. 이것은 위 예제에서 증명할 수 있다. 하지만 io 패키지에서 제공하는 Stream 객체들은 내부 필드에 대한 close()도 호출이 된다. 이는 try() 문이 해주는것은 아니며, 데코레이터 패턴을 활용한 객체 내에서 자신의 필드에 대한 close()도 호출해주기 때문이다. AutoCloseable 의 구현체를 작성할때는 해당부분도 유의해서 작성하자. 

공유하기 링크
TAG
댓글
댓글쓰기 폼