ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Exception (try-with-resources)
    CS/DS & OOP 2021. 1. 12. 18:25

    ???: close()형도 나가있어...

    Resource 할당/해제 문제

    프로그램은 OS에서 자원을 할당받아 동작합니다.

    자원에는 메모리와 CPU, 입출력 장치, 주/보조 기억장치 등이 있습니다.

    자원은 무한한 것이 아니기 때문에 필요에 따라 설정에 따라 각 프로그램에 분배되어야 합니다.

     

    만약 사용한 자원을 사용 후 닫지(close) 않는다면, 다른 프로그램이 제때 필요한 자원을 사용할 수 없겠죠.

    Input/Output Stream, Connection 등, Java 라이브러리에도 해제해줘야 하는 자원이 존재합니다.

    이러한 자원을 닫아주는 작업은 종종 놓칠 수 있어 성능상의 문제로도 이어지는 경우가 많습니다.

    필요할 때 바로 사용하지 못한다면, 그만큼 지연시간이 발생하고 이에 따라 속도가 느려질 테니까요.

     

    이 중 가장 많은 문제가 발생하는 것이 입출력 자원입니다.

    입출력을 수행하기 위해서는 스트림(데이터를 주고받는 통로)을 열어야 하는데 이것이 입출력 자원입니다.

     

    입출력 (BufferedReader, InputStreamReader) 예제

    이와 같이 입력이 완료된 후 close를 통해 stream을 닫아주시면 됩니다.

    그림 1 - 1. 입력 완료 후 close
    그림 1 - 2. 입력 완료 후 close

     

    그러나 아래와 같이 입력 중간에 close를 호출하면 당연하게도 stream이 닫히고 더 이상 입력을 할 수 없습니다.

    그림 2 - 1. 입력 완료 전 close 호출
    그림 2 - 2. 입력 완료 전 close 호출 결과

     

    이와 같은 방식으로 stream을 닫아주면 됩니다.

     

    이러한 입출력 과정에서 Exception이 발생하는 경우 문제가 발생합니다.

    입출력 도중 예외가 발생하게 됐을 때 제대로 처리를 해주지 않는다면 입출력을 수행하던 스트림이 열린 채로 유지되는데요.

    즉, 예외가 발생할 때마다 close 되지 않은 스트림이 쌓이게 되고 언젠가는 메모리 부족으로 인해 프로그램이 멈추게 됩니다.

    저는 개발을 하는 중에 예외가 발생하지 않았는데도, 스트림을 제때 닫지 않아 프로그램이 다운되는 현상을 겪었습니다.

    (당시에는 원인이 뭔지 몰라 한참을 고생했었습니다. ㅎㅎ)

     

    Try-with-resources 필요성

    따라서, 이를 위해 try-catch-finally 구문을 통해 아래와 같이 처리해줍니다.

    제가 실제 사용했던 코드를 약식으로 구성해 가져왔습니다.

    package com.example.backend.services;
    
    import org.apache.cxf.io.CachedOutputStream;
    import org.apache.tomcat.util.http.fileupload.IOUtils;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.HttpURLConnection;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    public class ServiceImpl {
        public static String requestFoods() {
    
            StringBuilder urlBuilder = new StringBuilder();
            HttpURLConnection conn = null;
            InputStream in = null;
            URL url;
            CachedOutputStream cached = null;
    
            String result = "";
    
            try {
                urlBuilder.append("https://");
    
                url = new URL(urlBuilder.toString());
                conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("POST");
                // content-type setting
    
                cached = new CachedOutputStream();
                in = url.openStream();
                IOUtils.copy(in, cached);
    
            } catch (MalformedURLException malformedURLException) {
                malformedURLException.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (conn != null) {
                        conn.disconnect();
                    }
                    if (in != null) {
                        in.close();
                    }
                    if (cached != null) {
                        cached.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            return result;
        }
    }
    

    HttpURLConnection을 통해 Open-API를 연동하는 과정의 코드입니다.

    그냥 봐도 상당히 복잡한데요. 구조를 간략하게 살펴보겠습니다.

     

    try 구문 내부에서 url를 생성해 URL 객체와 HttpURLConnection을 이용해 연결하는 코드입니다.

    MalformedException은 IOException을 상속하는 java.net 패키지 이하 URL에서 발생하는 checked Exception입니다.

    IOException은 HttpURLConnection, InputStream, IOUtils에서 모두 발생하는 checked Exception입니다.

     

    예외가 발생 여부에 상관하지 않고 stream을 닫아야 하기 때문에 finally에 stream을 닫는 코드를 구성했습니다.

    null이 아닌 경우엔, close를 통해 해제해주는 모습입니다.

    하지만 finally의 in 또한 IOException을 발생시키기 때문에 다시 한번 try-catch로 묶인 모습입니다.

     

    어떻게든 처리는 됩니다만, 코드 자체가 상당히 복잡하고.. 특히 finally의 코드는 상당히 난잡해 보입니다.

    이를 해결해주기 위해 나온 것이 AutoCloseable(v1.7)과 try-with-resource를 통한 방법입니다.

     

     

    Try-with-Resources


    The try-with-resources statement is a try statement that declares one or more resources. A resource is an object that must be closed after the program is finished with it. The try-with-resources statement ensures that each resource is closed at the end of the statement. Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.

    Java docs에서는 try-with-resources를 이와 같이 설명합니다.

    1개 이상의 resource에 사용되는 try 구문이며, AutoCloseable을 구현했거나, Closeable에 포함된 객체들을 resource로 사용한다.

    try-with-resources 구문은 해당 구문이 완료되는 시점에 각각의 resource가 모두 닫히는 것을 보장한다. 

     

    Try-with-resources 구성

    try (Object resource1 = new Object();
    	Object resource2 = new Object();
        ... ) {
            
    } catch () {
    }

     

    위의 코드에서 try-with-resources가 적용 가능한 resource는 아래와 같습니다.

    그림 3. InputStream (Closeable implements)
    그림 4. CachedOutputStream

     

     

    이 두 가지를 try-with-resource로 구성해 finally를 간소화시키는 것이 우리의 목표 정도로 생각하시고, 실제 코드를 보시겠습니다.

    package com.example.backend.services;
    
    import org.apache.cxf.io.CachedOutputStream;
    import org.apache.tomcat.util.http.fileupload.IOUtils;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.HttpURLConnection;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    public class ServiceImpl {
    
        public static String requestFoods() {
            StringBuilder urlBuilder = new StringBuilder();
            String result = "";
    
            HttpURLConnection conn = null;
    
            try {
                URL url = new URL(urlBuilder.toString());
    
                try (CachedOutputStream cached = new CachedOutputStream();
                     InputStream in = url.openStream()){
    
                    conn = (HttpURLConnection) url.openConnection();
                    conn.setRequestMethod("POST");
                    // content-type setting
                    IOUtils.copy(in, cached);
    
                } catch (IOException e) {
                    e.printStackTrace();
                }
                finally {
                    if(conn != null) conn.disconnect();
                }
            } catch (MalformedURLException malformedURLException) {
                malformedURLException.printStackTrace();
            }
    
            return result;
        }
    }
    

    InputStream을 try-with-resources 적용을 위해 URL 객체를 밖으로 빼서 url building을 진행했습니다.

    finally 부분이 기존보다 간소화는 되었지만 try 구문 중첩은 여전히 구조상 아쉬워 보입니다.

    실제 구현은 최대한 간소화하고 직관적인 코드를 구성하기 위해서 HttpURLConnection -> RestTemplate으로 변경했습니다.

    해당 내용은 따로 포스팅으로 정리했으니 참고하시기 바랍니다.

     

     

    위의 HttpURLConnection은 직접 Autocloseable을 구성해 try-with-resources에 포함시키는 것도 좋은 방법입니다.

    즉, RestTemplate 없이는 아래와 같이 변경이 가능합니다.

    package com.example.backend.services;
    
    import org.apache.cxf.io.CachedOutputStream;
    import org.apache.tomcat.util.http.fileupload.IOUtils;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.HttpURLConnection;
    import java.net.MalformedURLException;
    import java.net.URL;
    
    public class ServiceImpl {
    
        public static String requestFoods() {
            StringBuilder urlBuilder = new StringBuilder();
            String result = "";
    
            try {
                URL url = new URL(urlBuilder.toString());
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    
                try (CachedOutputStream cached = new CachedOutputStream();
                     InputStream in = url.openStream();
                     AutoCloseable autoCloseable = () -> conn.disconnect()){
    
                    conn.setRequestMethod("POST");
                    // content-type setting
    
                    IOUtils.copy(in, cached);
    
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } catch (MalformedURLException malformedURLException) {
                malformedURLException.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            return result;
        }
    }
    

     

    이렇게 구성한 경우 AutoCloseable에 의해 Exception catch 구문이 추가됩니다.

    아래는 그 이유입니다. (Stack Overrun)

    The AutoCloseable interface is located in java.lang and is intended to be applied to any resource that needs to be closed 'automatically' (try-with-resources). The AutoClosable must not be an i/o releated resource. So the interface can not make any assumption of a concrete exception.

    AutoCloseable은 java.lang 하위 패키지에서 try-with-resource내의 resource가 자동으로 닫히도록 의도하는 인터페이스다.

    즉, AutoCloseable 자체가 반드시 I/O resource에 관련된 것은 아니며, 어떤 구체적인 예외를 가정할 수 없는 인터페이스이기 때문이다.

     

     

    위에서 말씀드린 바와 같이 Open-API 통신에는 RestTemplate을 추천드리고 싶습니다.

    connection-pool은 customizing이 필요하지만, 꼭 해보시길 바랍니다.

    일반적으로는 try-catch를 쓰시는 게 좋긴 하겠습니다.

    그러나, 코드의 복잡도가 증가한다면 각 resource 특징을 파악하고 try-with-resource 구성도 고려해보시면 좋을 것 같습니다.

     

     

    참고 자료

    Dololak님의 블로그 (dololak.tistory.com/67)

    Java Docs, Oracle (tryResourceClose.htmldocs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html)

    Apache Java docs (cxf.apache.org/javadoc/latest/org/apache/cxf/io/CachedOutputStream.html)

    Stack Overrun (stackoverrun.com/ko/q/7121387)

    반응형

    'CS > DS & OOP' 카테고리의 다른 글

    Map 그리고 merge()  (1) 2022.01.29
    JVM (Java Virtual Machine), GC (Garbage Collection)  (0) 2021.06.22
    Exception (checked, unchecked)  (0) 2021.01.10
    Java 자료형과 String pool  (0) 2020.07.01
    객체지향 5원칙 (SOLID)  (0) 2020.06.24

    댓글

Designed by minchoba.