문제 상황

public interface Connection {
    void logic() throws Exception;
    void close() throws Exception;
}
public class SqlConnection implements Connection {
    @Override
    public void logic() throws Exception {
        System.out.println("SqlConnection.logic");
        throw new Exception("Something Error");
    }

    @Override
    public void close() throws Exception {
        System.out.println("SqlConnection.close");
        throw new Exception("Close Error");
    }
}

어떤 객체가 사용을 마치고 나면 연결을 끊기 위해 close() 라는 메서드를 호출해야 한다고 가정하자.

public class ConnectionTest {
    @Test
    void tryCatch() throws Exception {
        Connection conn = new SqlConnection();
        conn.logic(); // 에러 발생!!!
        conn.close(); // 실행 되지 않는다.
    }
}

그런데 이 객체에 logic()을 실행하던 도중, 에러가 발생하여 close() 메서드가 호출되지 않고 프로그램이 종료 되어버렸다.
예제 코드에서는 실제 DB와 연결되어 있지 않기 때문에 별 문제가 없지만, 실제 DB와 연결되었다고 가정한다면 DB에서는 Connection이 해제되지 않고 계속 유지되고 있는 것을 확인할 수 있다.
이때, 기존 코드에서는 Try-Catch-Finally 를 이용하여 다음과 같이 해결하였다.

기존의 해결 방법

public class ConnectionTest {
    @Test
    void tryCatchTest() throws Exception {
        Connection conn = null;

        try {
            conn = new SqlConnection();
            conn.logic(); // 에러 발생!!!
        } catch(Exception e) {
            // 예외 발생 상황 확인
            e.printStackTrace();
        } finally {
            if(conn != null) {
                conn.close(); // 프로그램을 닫는다.
            }
        }
    }
}

  • Try: 에러가 발생할 만한 부분을 감싼다.
    • 여기서는 logic 메서드에서 에러를 던졌다.
  • Catch: 에러가 발생하면 아래의 블럭을 실행한다.
    • 에러 메세지와 어디서 발생했는지 나타내주는 로그를 출력시킨다.
  • Finally: 에러가 발생하던 안하던 상관없이 마지막으로 아래의 블럭을 실행한다.
    • 위의 블럭이 모두 종료되면, close 메서드를 이용해 연결을 종료한다.

이렇게 하면, finally 키워드를 이용해 에러가 발생하던 안하던 상관없이 연결을 종료시킬수 있게 된다. 덤으로, 에러 발생으로 인한 프로그램의 종료도 방지할 수 있게 되었다.

기존의 해결 방법의 문제점

하지만, 기존의 방법은 몇가지 문제 사항이 있었다.

  1. 프로그램이 더러워 진다.
  2. 위의 코드는 생성자에서 예외 처리를 해주지 않았지만, 만약 생성자나 초기화 메서드에서 에러가 터진다면 어떻게 될까? 그 부분도 try-catch 문으로 감싸줘야 할 것이다.
  3. 또한, close() 에서도 에러가 발생할 수 있다.
    • logic() 에러와 close() 에러가 각각 따로 출력되어 서로 연관된 에러인지 확인하기 힘들게 된다.

Try-With-Resources

Java 7에서 try-catch-finally를 대체하기 위해 새로 등장한 문법이다.

예제와 함께 확인해보자.

public interface Connection extends AutoCloseable {
    void logic() throws Exception;
    void close() throws Exception;
}

먼저 Connection 인터페이스에 AutoCloseable 인터페이스를 상속한다.

public class ConnectionTest {
    @Test
    void tryWithResourcesTest() throws Exception {
        try(Connection conn = new SqlConnection()) {
            // 에러 발생!!!
            conn.logic();
        } catch (Exception e) {
            // 예외 발생 상황 확인
            e.printStackTrace();
        }
    }
}

try(...) 키워드를 보면 소괄호가 생긴 것을 볼 수 있는데, 그 안에 인스턴스를 생성해줄 수 있게 된다.

그럼, 해당 인스턴스는 try 블럭 안에서만 접근 가능하게 되며, try-catch 구문이 종료하게 되면 자동으로 close() 메서드를 호출하고, 할당된 인스턴스를 해제한다.

또한, close() 메서드에서 오류가 발생했을 경우, e.printStackTrace() 로 확인하면 logic() 에러와 close() 에러가 합쳐져서 출력되며, close() 에러는 logic() 에러의 하위 에러로 포함시켜서 출력을 하기 때문에 서로 연관된 에러인지 확인하기 쉬워진다.

심지어 close()에서 에러가 발생했음에도 불구하고, 프로그램이 종료되지 않고 다음 구문을 실행한다.

참고
e.printStackTrace()를 생략하게 되면 로그 상으로는 아무것도 출력되지 않는다!

AutoCloseable, Closeable

public interface AutoCloseable {
    void close() throws Exception;
}

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

AutoCloseable

AutoCloseable의 원형은 다음과 같이 close() 메서드를 하나 포함 시키고 있는 것을 볼 수 있다.
이를 상속받은 객체는 close() 메서드를 오버라이딩하여 연결을 해제해주는 코드를 작성하면 된다.
또한 이를 통해 try(…) 구문에서 할당이 가능해진다.

Closeable

AutoCloseable이 생기기전, Closeable 이란 인터페이스가 있었는데, 이는 단순히 여러 객체에 대한 동일한 이름의 해제 메서드(close())를 강제하기 위해 만들어진 인터페이스였다. 이를 상속받는 대표적인 객체들은 FileStream, ServerSocket, URLClassLoader, 등등 이 있다.

AutoCloesable을 Closeable에 상속함으로써, 기존에 존재하던 객체의 코드 변경없이 try-with-resources에 적용시킬 수 있도록 확장시켰다.

정리

  1. 코드가 간결해진다.
  2. 번거로운 자원 반납을 하지 않아도 된다.
  3. 실수나 에러에 의한 자원 반납 누락을 방지할 수 있다.
  4. 모든 에러에 대한 스택 트레이스를 남길 수 있다.

정리하자면, AutoCloseable을 상속받은 객체에 대해서는, try-catch-finally 대신 try-with-resources를 사용하자.

참고

profile
백엔드 개발자 지망생

0개의 댓글