본문 바로가기
java/기타

java - InputStream& OutputStream close

by 권성호 2021. 9. 28.

1. 서론

회사 업무 중 클라이언트로부터 전송되는 파일을 수신하고 저장하기 위한 파일 서버를 개발하게 되었다.

(참고로, 회사의 서버 개발 환경은 spring boot + tomcat)

서버 입장에서 파일을 수신하기 위해 생각한 방식은 2가지가 있었다.

  1. multipart/form 기반
  2. octet-stream 기반

multipart/form의 경우 컨트롤러에서 MultipartFile로 요청 데이터를 수신하면 tomcat이 알아서 파일을 수신해 준다.

    @PostMapping("")
    public ResponseEntity upload(@RequestPart MultipartFile file) throws Exception {
        //file 파라미터에 파일 객체가 알아서 들어온다(was level에서 처리해줌)
        return ResponseEntity.ok("test");
    }

그렇다면, 알아서 어떻게 수신해 주는 것일까?

  1. multipart/form 형식의 프로토콜을 보면 딜리미터 방식으로 파일 데이터를 전송하는데, tomcat은 이러한 딜리미터를 하나하나 파싱 해서 파일 데이터를 수신한다.
  2. 수신한 파일 데이터를 임시 폴더 아래에 저장한다.
  3. 파일 저장이 다 끝나면 그때 비로소, 컨트롤러를 호출하며 MultipartFile 파라미터를 넘긴다.
  4. 컨트롤러 로직이 끝나면 임시폴더에 저장된 파일을 삭제한다.

알아서 수신하는 부분이 생각보다 복잡하다고 느꼈다.

굳이 multipart로 전송해서 딜리미터를 파싱 할 필요가 있을까?

굳이 임시폴더에 파일을 별도로 저장할 필요가 있을까?(임시 폴더에 저장된 파일이 삭제되지 않아 문제가 되는 경우도 많은데..?)

 

이런 불편함 뿐 아니라, 대용량 파일을 전송해야 하는 요구 사항과 맞물려 파일을 이어 전송 할 수 있도록 해달라는 요구가 있었는데, multipart 방식으로 구현하기에는 유연성이 너무 떨어져 어렵다고 판단이 들었다.

 

따라서, multipart를 버리고 octet-stream 기반 위에 별도의 프로토콜을 만들기로 했다!

 

octet-stream 기반으로 파일을 수신하기 위한 예시 코드는 아래와 같다.

    @PostMapping("uploadTest")
    public ResponseEntity uploadTest(HttpServletRequest request){
        String ROOT_PATH = "C:\\test";
        String fileName = "dummyFile";
        Path filePath = Paths.get(ROOT_PATH).toAbsolutePath().normalize();
        try {
            if(!Files.exists(filePath))
                Files.createDirectories(filePath);
            FileOutputStream outStream = new FileOutputStream(filePath.resolve(fileName).normalize().toFile(), true);
            InputStream inStream = request.getInputStream();
            int BUFFER_SIZE = 1024;
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead = -1;
            while (true) {
                bytesRead = inStream.read(buffer);
                if(bytesRead == -1)
                    break;
                outStream.write(buffer, 0, bytesRead);
            }
            outStream.close();
            inStream.close();
        }catch(IOException ex) {
            ex.printStackTrace();
        }
        return ResponseEntity.ok("always true");
    }

이렇게 구현하고 보니 의문 하나가 생겼다. 

클라이언트로부터 요청 데이터를 읽기 위해 input stream을 만들고 읽은 데이터를 파일로 저장하기 위해 fileoutputstream을 만들었는데 내가 짠 코드에서 close가 적절하게 이루어지고 있는 것인가?

 

그래서 이 부분에 대해 공부했다.

 

2. 본론

우선 아래 링크를 보면 java에서 InputStream의 close와 관련해 잘 설명되어 있다.

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=websearch&logNo=221930525766 

 

Java 언어의 InputStream 을 생성한 후, close 하지 않으면 파일 file handle leak 이 생기는가?

Java 언어의 InputStream 을 생성한 후, close 하지 않으면 파일 file handle leak 이 발생하는지 궁금...

blog.naver.com

링크 내용을 요약하면 다음과 같다.

  1. InputStream을 생성 후 close하지 않으면 기본적으로 leak이 발생한다.
  2. 하지만 java의 경우 gc가 발생할 때 close하지 않은 객체가 gc의 대상이 되면 삭제되면서 close까지 같이 호출된다.(찾아보니 AutoCloseable를 상속받은 클래스에 대해 객체가 삭제될 때 자동으로 close 메서드가 호출된다. 즉, AutoCloseable을 상속받지 않았다면, gc 시점에도 close가 안될 수 있다.)
  3. gc가 되는 시점에서 close가 호출 되지만, 이 시점은 언제가 될지 모르며 웹 서비스를 담당하는 서버의 경우 그 사이에 트래픽이 몰리게 되면 순간적으로 file handle이 부족해질 수도 있다.

결국, 코드 수준에서 확실하게 close 해 주는 것이 바람직 하다!

 

그런데, 위에 있는 내 코드를 보면 try부분에서만 close를 해 주고 있다. 이러면 exception 발생 시 close가 되지 않을 수 있다고 생각해서 아래와 같이 코드를 수정했다.

 

try 속에 있던 close를 finally 부분으로 옮겨 항상 close를 시키겠다는 생각이 담긴 코드였다. 

그런데...? 빨간 줄이?

 

어떠한 경우에도 close를 하기 위해 finally 구문에 close를 넣었는데, close 자체도 exception이 날 수 있어서 이걸 또 처리하라고?

 

그럼 close를 또 catch 해서 close 하는 코드를 넣어야 하는 건가?라는 순환 논증에 빠지게 되었다.....

무슨 방법이 있을까 고민하던 중 try-with-resources 키워드를 알게 되었다. 자세한 내용은 아래 링크 참고

https://codechacha.com/ko/java-try-with-resources/

 

Java - Try-with-resources로 자원 쉽게 해제하기

try-with-resources는 try(...)에서 선언된 객체들에 대해서 try가 종료될 때 자동으로 자원을 해제해주는 기능입니다. 객체가 AutoCloseable을 구현하였다면 Java는 try구문이 종료될 때 close()를 호출해 줍니

codechacha.com

 

try-with-resource를 사용할 때에도 해당 객체의 클래스가 AutoCloseable을 상속받아야 정상 동작한다. 

개발자가 직접 close를 위한 로직을 엄밀하게 작성하는 것보다 java 언어 차원에서 제공하는 문법을 활용하는 게 생산성이 좋고 안정적일 것이라 판단한다.

최종적인 코드는 아래와 같다.

    @PostMapping("uploadTest")
    public ResponseEntity uploadTest(HttpServletRequest request){
        String ROOT_PATH = "C:\\test";
        String fileName = "dummyFile";
        Path filePath = Paths.get(ROOT_PATH).toAbsolutePath().normalize();
        try (
                FileOutputStream outStream = new FileOutputStream(filePath.resolve(fileName).normalize().toFile(), true);;
                InputStream inStream = request.getInputStream();){
            if(!Files.exists(filePath))
                Files.createDirectories(filePath);
            int BUFFER_SIZE = 1024;
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead = -1;
            while (true) {
                bytesRead = inStream.read(buffer);
                if(bytesRead == -1)
                    break;
                outStream.write(buffer, 0, bytesRead);
            }
        }catch(IOException ex) {
            ex.printStackTrace();
            return ResponseEntity.internalServerError().body("fail!");
        }
        return ResponseEntity.ok("always true");
    }

 

'java > 기타' 카테고리의 다른 글

[Collection Framework] 정리  (0) 2022.01.04
Thread pool vs Reactive  (0) 2021.12.01
JDBC Connection pool 모니터링 지표에 대한 이해  (0) 2021.12.01
java - Exception과 logging이 성능에 미치는 영향  (0) 2021.10.08
Java - Enum  (0) 2021.09.24

댓글