#java

자원을 자동으로 해제, try-with-resource

우리는 자바로 프로그램을 짜면서 시스템에 있는 자원들을 사용하게 됩니다. 자원 자체를 사용하는 것뿐만 아니라 사용이 끝났을 때 해제하는 것도 매우 중요한 일입니다. 자원 해제를 잊어버리거나, 예외 처리 과정 중에 해제가 이루어지지 않을 수 있습니다. Java의 예외 처리 기능인 try-with-resource 와 함께 자바에서의 자원을 사용 이후에 자동으로 해제하보도록 하겠습니다.


Resource

자원은 시스템을 운영하는 데 있어서 메모리나 입출력 장치 등 하드웨어, 소프트웨어 형태로 존재하는 구성요소를 의미합니다. 오늘 자원이라고 지칭할 개념은 자바의 외부자원으로서 JVM 바깥에 메모리 이외의 자원을 지칭하겠습니다.
자바에서 자원을 사용하고 나면 해제를 해주어야 합니다. 자원을 해제하지 않으면 메모리 누수 및 특정 프로그램의 독점으로 인해 해당 객체가 올바르게 작동하지 않을 수 있습니다. GC(Garbage Collection) 라는 자동으로 관련 메모리가 해제되는 기능이 있긴 합니다. 하지만 GC 는 메모리를 해제하는 시간을 예측하기 힘들기 때문에 할당받은 자원의 해제 시점 또한 예측하기가 힘듭니다. 따라서 GC 에서 해제하는 것을 기다리기보다는, 자원에 대한 사용이 끝나면 직접 해제해주는 편이 좋습니다. 자주 사용하는 자원 관련 객체는 InputStream , OutputStream , 그리고 java.sql.Connection 이 있습니다. 각각의 객체들은 자원과 연결하여, 외부에서 가져온 기능들을 사용할 수 있게 해줍니다. 앞서 언급한 객체보단 오늘은 이해를 쉽게 돋기 위해서 임의로 2가지 자원에 관련된 객체를 정의해보겠습니다.

public class JavableBook {
    public String page(int pageNumber) throws IOException {
        //책에 관련된 로직
    }
    
    public void close() throws IOException {
        //책에 대한 자원을 해제하는 로직
    }
}
public class JavableVideo {
    public String scene(int time) throws IOException {
        //장면에 대한 로직
    }
    
    public void close() throws IOException {
        //비디오에 대한 자원을 해제하는 로직
    }
}

각 자원들은 Javable System에 하나 밖에 존재하지 않으며, 인기가 매우 많아 많은 사용자들이 대기하고 있습니다. GC 가 자동적으로 해제해주는 것을 기다리기보단 다른 사용자들을 위해 빨리 해제하는 방향으로 구현하기로 하였습니다.

기존의 자원을 사용하는 순서

public void play() throws IOException {
    JavableBook book = new JavableBook(); // ----(1)
    book.page(200);                       // ----(2)
    book.close();                         // ----(3)
}

자원을 사용할 때에는 먼저 (1)의 과정인 해당 자원과 객체를 연결해주어야 합니다. 그다음에는 (2)의 과정처럼 해당 자원을 사용하여, 필요한 로직들을 실행합니다. 마지막으로 (3)의 과정처럼 해당 자원을 해제하면 됩니다. 간단해 보이지만 이곳에서 문제가 생길 수가 있습니다. 만약 예외가 발생한다면, 중간의 자원이 해제되지 않을 가능성이 있기 때문입니다. JavableBook이라는 책의 페이지가 150쪽이라고 가정을 하게 되면 (2)의 로직을 처리하는 과정에서 예외가 발생이 될 것입니다. (2)에서 예외가 발생하면 (3)까지 가지 못하고 예외 처리 로직으로 이동하게 됩니다. 예외의 발생으로 인하여 자원이 해제되지 않는 결과가 발생합니다. 이 문제점을 해결하기 위해서 나온 방법이 try-finally 방법입니다.

예외 처리를 고려한 기존의 자원 해제 방법 try - finally

try-finally 는 기존의 블록 단위 예외 처리 방법입니다. 원래 try-catch-finally 블록으로 이루어집니다. try 는 처음에 실행하고 싶은 블록, catch 는 예외가 발생 시에 처리하고 싶은 블록입니다. 마지막으로 finally 는 예외 발생 여부에 상관없이 최종적으로 처리하고 싶은 블록을 담게 됩니다. 이때, 예외 발생 여부에 상관없이 최종적으로 finally 블록을 처리한다는 점을 활용하여 자원을 사용한 다음에 해제하도록 하겠습니다.

public void play() throws IOException {
    try {
        JavableBook book = new JavableBook(); // ----(1)
        book.page(200);                       // ----(2)
    } finally {
        book.close();                         // ----(3)
    }
}

위의 예시처럼 try 에서 자원을 할당받는 (1) 이나 자원을 사용하여 로직을 수행하는 (2)에서 어떠한 오류가 발생하더라도, (3)에서 최종적으로 자원을 해제하게 되므로 저희는 자원 해제를 보장할 수 있습니다. 하지만 try-finally 의 단점은 무엇이 있을까요? JavableBook뿐만이 아니라 다른 자원에 관련된 객체인 JavableVideo를 사용하는 예시를 보겠습니다.

public void play() throws IOException {
    try {
        JavableBook book = new JavableBook();        //----(1)
        try {
            JavableVideo video = new JavableVideo(); //----(1)
            book.page(150);                          //----(2)
            video.scene(150);                        //----(2)
        } finally {
            video.close()                            //----(3)
    } finally {
        book.close()                                 //----(3)
    }
}

갑자기 기하급수적으로 indent가 증가한 것을 볼 수가 있습니다. 만약 자원을 더 사용하게 된다면, 너무나도 많은 try-finally 절이 추가될 것이라는 것을 예상할 수가 있습니다. 이는 코드가 점점 길어지고 indent가 깊어지며 지저분해지는 것을 뜻합니다.

try-with-resource

이러한 점들을 만족시키는 것이 바로 try-with-resource 라는 방법입니다. Java 7부터 추가된 이 방법은 앞서 언급한 문제점들을 대해서 해결할 수 있는 점이 장점이 있습니다.

public void play() throws IOException {
    try (JavableBook book = new JavableBook();
         JavableVideo video = new JavableVideo();){ //----(1)
         book.page(150);
         video.scene(150);                          //----(2)
    }
}

위의 예시 코드를 보게 된다면,

  • try 바로 다음 소괄호에서 자원을 할당받게 되는 (1)의 단계를 진행하게 됩니다.
  • 중괄호에서 로직을 실행하게 되는 (2)의 단계를 밟게 됩니다.

하지만 이 때까지 방법과 다르게 (3)의 단계가 보이지 않습니다. try-with-resource 에서는 구절이 모두 끝나게 된다면 자동으로 자원을 반납하게 됩니다.
그러나 아무런 자원이나 반납이 가능한 것이 아닙니다. 사용자가 임의로 정의한 자원을 할당 받는 객체는 사용하는 현재 구현에서는 try-with-resource 가 작동하지 않을 것으로 예상이 됩니다. try-with-resource 를 통하여 해제할 자원을 가진 객체는 AutoCloseable 이라는 Interface 를 구현을 해야합니다.

Interface AutoCloseable && Method close

이해를 돕기 위해 try-with-resource 에서 자동으로 해제되는 자원인 FileInputStream 소스 코드 구현의 일부를 예시로 보도록 하겠습니다.

//Class FileInputStream 소스 코드 일부
public class FileInputStream extends InputStream

//Abstract Class InputStream 소스 코드 일부
abstract class InputStream implements Closeable

//Interface Closeable  소스 코드 일부
public interface Closeable extends AutoCloseable {
   public void close() throws IOException;
}

//Interface AutoCloseable 소스 코드 일부
public interface AutoCloseable {
   void close() throws Exception;
}

FileInputStream 에서는 Closeable 를 구현하고 있습니다. Closeable 의 경우에는 try-with-resource 를 사용할 수 있게 해주는 AutoCloseable 를 상속받아서 사용하고 있습니다. 이 때문에 try-with-resource 에 필수적인 close Method 가 각각의 자원에 관련된 객체의 특성에 맞게 구현이 되어있습니다. 다음과 같이 저희가 만든 객체에도 AutoCloseable 라는 Interface 의 close Method 를 구현하게 되면, try-with-resource 를 사용할 수 있게 됩니다.

//try-with-resource 사용을 위한 JavableBook 추가 구현
public class JavableBook implements AutoCloseable {
    @Override
    public void close() throws IOException {
        //책에 대한 자원을 해제하는 로직
    }
}

결론

코드가 복잡해질수록, 자원을 사용 방향 및 방법뿐만 아니라 해제 및 예외 처리에 대해서도 고민이 많아 지게 됩니다. 이전의 방법보다는 try-with-resource 방법과 함께 자원을 자동으로 해제하고, catch 에서 예외 처리를 해주면 깔끔하면서도 더 견고한 코드로 나아갈 수 있습니다.

참조