자원이란?

Java를 사용하다 보면 컴퓨터의 구성 요소 중 다양한 부분들을 사양하게 됩니다. JVM에 대한 정보가 있는 메모리부터 데이터베이스 다른 소프트웨어에서 제공 받는 기능들, 그리고 영구적으로 데이터를 할 수 있는 보조 기억 장치까지요. 저희는 이렇게 소프트웨어, 하드웨어적으로 한정된 기능을 제공받는 것들을 자원 이라고 합니다.

자바에서 가장 흔하게 볼 수 있는 자원 관련 객체는 InputStream, OutputStream 그리고 java.sql.Connection 등이 있으며 이 객체들 모두 앞서 말한 요소들에 관한 기능과 연관이 있습니다.

자원 해제의 중요성

컴퓨터의 다양한 기능들을 사용하는 것은 좋지만, 해당 상황들은 우리가 만드는 프로그램 뿐만이 아니라 각각의 자원에 대해서도 영향을 미치게 됩니다.

 

1. JVM에서 차지하는 메모리

외부 자원을 사용하게 되는 것도 JVM에서 필시 메모리가 차지하게 됩니다. 이는 우리의 프로그램이 GC를 통해서 처음부터 끝까지 관리해주는 객체들과 달리 다른 자원과 연결되어있습니다. 때문에 GC를 믿기보단 직접 해제를 해줘야 합니다.

 

2. 관련 객체 오작동

자원에서 필요한 부분들을 다 사용하고 나서 반납을 해주지 않으면 다른 프로세스나 쓰레드에서 해당 객체를 사용했을 때의 예상치 못한 문제가 발생 할 수 있습니다.

 

이러한 이유 때문에 GC를 믿기보다는 해당 자원을 직접적으로 해제해주는 것이 좋습니다.

 

기존의 방법들

자원에 관련된 객체 이해를 돕기 위해 임의로 하나의 객체를 정의 해보겠습니다.

 

public Resource {

	public void run() throws ResourceExcpetion {
    	//해당 자원에 관련된 로직을 실행하는 메소드
    }
    
    public void close() throws ResourceExcpetion {
    	//해당 자원을 다 사용하고 난 다음에 해제하는 메소드
    }
}

가장 원시적으로 사용하게 되는 방법은 자원을 사용하고 난 다음에 일일히 메소드로 닫아주는 형태입니다.

 

public class Main {
	public static void main(String[] args) {
    	Resource resource = new Resource(); //자원 할당
        resource.run(); 					//자원 사용
        resource.close();					//자원 사용 해제
    }
}

 

굉장히 단순해 보이지만 문제가 있습니다. 만약 run이라는 메서드를 사용하는데, 프로그램 내부의 로직 문제든 외부 자원에서 발생한 문제이든 Exception 이 발생 될 수가 있을 것입니다. 그러면 자원 사용을 해제하는 close()에 대한 메소드는 실행되지 않을 것입니다. 

이러한 예상되는 문제점들을 해결하기 위해서 사람들이 활용한 것이 try-catch-finally로 대표되는 자바의 예외 처리 구문을 사용하기로 하였습니다.

 

public class Main {
	public static void main(String[] args) {
    	Resource resource = new Resource(); //자원 할당
        try {
        	resoure.run();					//자원 사용
        } finally {
        	resource.close();				//자원 사용 해제
        }
    }
}

해당 예시처럼 try 블록을 사용한 다음에 메소드가 작동 여부에 상관 없이 finally 블록에 진입하여 자원을 닫아주게 됩니다.

하지만 이러한 부분도 문제가 크게 두 가지가 존재합니다.

 

1. 깊어지는 Depth의 문제

try - finally 방법을 사용하게 된다면, 자원의 수에 맞게 try-finally 블록이 계속해서 발생하게 됩니다. 만약 우리가 사용할 자원이 두개 이상이 되면, 사용하는 자원의 수인 2개 이상만큼 try - finally를 사용하는 것이 이상적입니다.

 

public class Main {
	public static void main(String[] args) {
    	Resource resource = new Resource(); //자원 할당
        SecondResource secondResource = new SecondResource(); 
        try {
        	resoure.run();					//자원 사용
            try { 
            	secondResource.run();		//자원 사용
            } finally {
            	secondResource.close();		//자원 사용 해제
            }
        } finally {
        	resource.close();				//자원 사용 해제
        }
    }
}

 

하지만 이런 방향은 코드의 Depth를 늘리게 되고, 이는 객체 지향 프로그래밍에 맞지 않은 결과를 초래할 뿐만이 아니라 가독성이 떨어지는 문제점이 생기게 됩니다.

 

2. 삼켜져 버린 예외 처리

만약 run()이라는 메소드에서 예외가 발생한다면,이 예외가 로그에 기록되는 것이 우리가 바라는 방향입니다. 하지만 try - finally를 사용하게 된다면, run()에서 발생한 예외보단 finally에서 발생한 예외가 기록이 됩니다. 이는 디버깅이나 기능을 구현할 때 예외 처리에 대한 로그 기록이 남지 않으므로 문제가 생길 수 있습니다.

 

개선된 방법 - try with resource

Try with Resource를 사용한 코드를 먼저 보고 가도록 하겠습니다.

public class Main {
	public static void main(String[] args) {
    	try(Resource resource = new Resource();){ //자원 할당
        	resoure.run();						//자원 사용
        }
    }
}

앞서 보여드렸던 기존의 코드와 다른점은 2가지가 있습니다.

첫 번째로는 try 블록안에 자원 할당을 하는 코드가 들어갔습니다.

두 번째로는 resource를 닫아주는 close() 메소드가 사라지게 되었습니다.

 

이로 인해서 저희는 Depth에 관한 문제와 예외 처리에 관한 문제를 동시에 해결할 수가 있습니다.

만약 여기서 발생한 예외에 대해서 잡고 싶다면

try 뒤의 블록에 

} catch (ResourceException resourceException) {
	//예외 처리 로직
}

을 진행해주시면 됩니다.

 

하지만 이렇게 좋아 보이는 try-with-resource 방법은 자원에 대한 객체면 다 쓸 수 있는 것이 아닙니다.

특정 인터페이스를 구현해주어야하는데 그것이 바로 다음에 언급할 AutoCloseable입니다.

 

 

Interface AutoCloseable, Method close

 

//java.lang의 패키지라 따로 import할 필요없습니다.

public Resource implements AutoCloseable {

	public void run() throws ResourceExcpetion {
    	//해당 자원에 관련된 로직을 실행하는 메소드
    }
    
    @Override
    public void close() throws ResourceExcpetion {
    	//해당 자원을 다 사용하고 난 다음에 해제하는 메소드
    }
}

AutoCloseable의 경우에는 close를 추상 메소드로 가지고 있는 인터페이스 입니다.

이 close를 오버라이딩을 통해서 구현해주면 try-with-resource에서 자원을 자동으로 해제해주는 객체를 만족하게 됩니다.

 

 

 

참고 자료

  • Joshua Bloch, "Effective Java", Insight(2018), 47-50

+ Recent posts