어리바리 신입 개발자의 얼렁뚱땅 개발 기록 ✨
[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기7 / 첨부파일 다운로드 구현하기(HttpSevletResponse / ResponsEntity / 바이너리 데이터 ) 본문
REST API
[ Java / SpringBoot / MVC / RESTful] RESTful한 게시판 만들기7 / 첨부파일 다운로드 구현하기(HttpSevletResponse / ResponsEntity / 바이너리 데이터 )
낫쏘링 2023. 9. 11. 20:02728x90
[ 다운로드를 구현하기 전에 알아둘 것들 ]
- HttpSevletRquest
사용자로부터 들어오는 모든 요청 정보를 담고 있다.
- HttpSevletResponse
사용자에게 전달할 데이터를 담고 있다.
즉, 사용자에게 전달할 데이터를 원하는 상태로 설정해서 전달 가능하게 한다.
- ResponseEntity
HTTP 응답의 상태 코드, 헤더, 본문 / RESTful 웹 서비스에서 응답을 보다 세밀하게 제어하기 위해 사용
- HttpSevletResponse VS ResponseEntity
둘 다 HTTP 응답을 처리하기 위해 사용된다.
HttpServletResponse가 더 세밀한 응답 조작이 가능하지만,
ResponseEntity를 사용하면 높은 수준의 추상화와 간결한 코드를 만들 수 있다.
낮은 수준의 추상화 : 컴퓨터 메모리에서 직접 바이트를 읽고 쓰는 작업
메모리 구조, 주소, 바이트 조작 등에 대한 깊은 지식 필요
높은 수준의 추상화 : 파일을 열고 텍스트를 읽는 작업
단순히 '파일 열기'와 '텍스트 읽기' 명령만 사용하면 되기 때문에
실제 동작 방식에 대해 알 필요 없다.
즉, ResponseEntity는 코드의 재사용성, 유지 관리성, 확장성을 향상 시켜 준다.
하지만, HTTP 응답에 대해 세밀한 제어가 필요할 경우 HttpServletResponse를 사용한다.
- 바이너리 데이터(Binary Data) : 데이터!!!
컴퓨터는 이진수(1과 0의 집합)로 데이터를 저장하고 표현하고, 1과 0이 들어있는 자리를 Binary digit이라고 한다.
줄여쓰면 우리가 흔히 들어본 비트(bit)가 된다.
우리가 실제로 쓰는 숫자는 십진수라고 하고, 컴퓨터는 십진수를 이진수로 변환해서 다룬다.
하지만 우리가 컴퓨터가 다루길 바라는 데이터는 숫자만 존재하는게 아니다.
문자열, 이미지 등 다양한 형식의 데이터를 다루고자 할때 컴퓨너는 이런 데이터 역시 이진수로 변환한다.
즉, 사람은 읽을 수 없지만 컴퓨터는 읽을 수 있는 데이터를 바이너리 '데이터'라고한다.
따라서 컴퓨터는 모든 데이터들을 바이너리 데이터로 저장하고 처리한다.
비트(bit)는 바이너리 데이터를 처리하는 단위!!! / 8비트 = 1 바이트!
- 바이트(byte) : 단위!!!
데이터의 기본적인 저장 및 전송 단위(1byte = 8bit)
메모리의 크기나 파일의 크기를 나타내는 기본 적인 저장 및 전송 '단위'
최소 단위인 비트가 있는데 바이트가 최소 단위가 된 이유는 최소 단위의 숫자, 영문, 특수 문자 등을 표현하려면
최소 8비트가 필요하기 때문이다. (실제로 영문 a를 표현하려면 8비트가 필요)
- 그러면 왜 경로를 바이트 배열에 넣어주는건데?
앞에서 HttpSevletResponse에 대해 이야기 했다.
파일 다운로드 구현시 사용자에게 다운로드할 파일에 대한 데이터를 전달해야하는데
HTTP 프로토콜은 바이트 스트림을 기반이며, 자바에서 스트림 처리를 할 때 가장 기본적인 단위가 바이트 배열이기 때문이다.
또, 파일을 스트림으로 읽어 바이트 배열 형태로 처리하면, 서버의 메모리 사용량을 최소하하면서 대용량 파일도 효율적으로 처리할 수 있다.
// boardFile.getStoredFilePath()는 파일이 저장된 경로다. // readFileToByteArray는 FileUtils에서 제공하는 메서드로 파일을 바이트 배열로 읽어온다. byte[] files = BoardFileService.readFileToByteArray(new File(boardFile.getStoredFilePath()));
하지만 굳이 직접 바이트 배열로 넣어주지 않아도 UrlResourse 객체를 활용할 수 있다.
실제로 구현할 때 위의 코드를 사용하지 않을 예정이다.
- UrlResourse
파일, HTTP, FTP 대상 등에 접근하여(특정 URL에 접근하여) 정보를 바이트 배열로 읽어 클라이언트에 전송
URL은 특정한 접두어를 갖는데 이 접두어가 접근하려는 리소스의 종류를 뜻한다.
파일의 경로에 접하기 위해서는 접두어로 "file:"을 사용한다. (HTTP는 "https:")
UrlResource 객체는 UrlResource 생성자(new UrlResource()) 를 통해 생성되지만 종종 이 접두어를 통해 묵시적으로 생성될 수 있다.
"classpath:"는 ClassPathResource가 "file:"은 FileSystemResource가 생성된다. 인식되지 않는 접두어를 사용할 경우 표준 URL 문자로 추측하여 UrlResource 객체가 생성된다.
그러면 그냥 처음부터 FileSystemResource 객체를 생성하면 안되나 싶지만 나중에 참조하는 URL이 HTTP 등으로 변경될 수 있기 때문에 유연성을 위해 UrlResource를 사용해줄 예정이다.
- URLEncoder.encode() vs UriUtils.encode()
/board/write에서 board와 write가 경로 세그먼트다. 하지만 이 경로 세그먼트에는 특별한 예약 문자들이 존재한다.
예를 들어, URL의 경로 세그먼트에서는 "+"기호가 실제 "+"기호로 해석되어야한다.
- URIEncoder : 주로 쿼리 매개변수 값을 인코딩하기 위해 설계됐기 때문에 공백 문자를 "+" 기호로 변환하다.
- UriUtils : URL의 다양한 세그먼트를 올바르게 인코딩하기 위한 도구 (스프링 프레임워크 제공)
[ 1. dao(쿼리) 작성 ]
- 클라이언트 측에서 다운로드를 실행했을 때 받아오는 정보 : idx(첨부파일번호) / boardInx(게시판번호)
- 파일 다운로드 할 때 필요한 데이터 : originalFileName(파일이름) / storedFilePath(저장경로)
<select id="selectFileInfo" parameterType="BoardFileDto" resultType="BoardFileDto"> /* 게시글 번호에 해당하는 파일 정보 가져오기 */ <![CDATA[ SELECT original_file_name, stored_file_path, file_size FROM t_file WHERE idx = #{idx} AND board_idx = #{boardIdx} AND deleted_yn = 'N' ]]> </select>
[ 2. Mapper 작성 ]// 하나의 게시글에서 다운 받고자하는 파일 정보 조회 BoardFileDto selectFileInfo(BoardFileDto file) throws Exception;
[ 3. BoardController 작성 ]
- HttpServletResonse 사용
@GetMapping("/board/file") public void downloadFile(BoardFileDto file, HttpServletResponse response) throws Exception{ BoardFileDto boardFile = boardFileService.selectFileInfo(file); //boardFile은 따로 만든 dto 클래스이기 때문에 MultipartFile처럼 isEmpty 메서드가 없으니까 ObjectUitls.isEmpty메서드 사용 if(!ObjectUtils.isEmpty(boardFile)) { String fileName = boardFile.getOriginalFileName(); int fileSize = (int)boardFile.getFileSize(); // 파일의 경로를 바이트배열로 읽어온다. UrlResource resource = new UrlResource("file:" + boardFile.getStoredFilePath()); // 파일 이름 URL 주소로 사용하기 전에 인코딩하기 String encodedFileName = UriUtils.encode(fileName, StandardCharsets.UTF_8); // Content-Disposition: HTTP 응답의 처리 방식, 브라우저가 반환된 내용을 화면에 표시할 것인지 파일로 다운로드할 것인지 결정한다. // attachment : 반환된 내용을 파일로 다운로드 해라 String contentDisposition = "attachment; filename=\"" + encodedFileName + "\""; response.setHeader("Content-Disposition", contentDisposition); // ContentType : 응답의 본문(body)의 미디어 타입 (데이터의 종류, 포맷에 대한 정보 제공) // application/octet-stream : 바이너리 데이터 response.setContentType("application/octet-stream"); // 파일의 크기 설정(바이트 단위) response.setContentLengthLong(fileSize); // 파일의 바이트 수 가져오기 위해서 인풋 스트림 생성 InputStream inputStream = resource.getInputStream(); // 다운로드 파일 데이터를 클라이언트에 전달하기 위해 response에 출력할 출력 스트림을 생성 I OutputStream outStream = response.getOutputStream(); // 파일을 바이트 배열로 가져오기 byte[] byteFile = resource.getContentAsByteArray(); int byteLength; // inputStream.read(byteFile) : 바이트 배열의 길이 // 바이트 배열을 루프를 돌면서 하나의 바이트씩 읽는다. 더 이상 읽을 바이트가 없으면 -1 리턴 while ((byteLength = inputStream.read(byteFile)) > 0) { // write(바이트배열, 바이트배열에서 데이터의 시작 위치, 바이트배열의 테이터 길이] // 바이트 배열에서 시작 위치 부터 데이터 길이까지 데이터를 출력 스트림으로 보관 outStream.write(byteFile, 0, byteLength); } outStream.flush(); inputStream.close(); outStream.close(); } }
- ResponseEntity 사용/** 첨부파일 다운로드 * ResponseEntity : HTTP 응답의 상태 코드, 헤더, 본문 * RESTful 웹 서비스에서 응답을 보다 세밀하게 제어하기 위해 사용 * Resource : UrlResource의 내용을 body로 보내주기 위해서 사용 */ @GetMapping("/board/file") public ResponseEntity<Resource> downloadFile(BoardFileDto file) throws Exception{ BoardFileDto boardFile = boardFileService.selectFileInfo(file); //boardFile은 따로 만든 dto 클래스이기 때문에 MultipartFile처럼 isEmpty 메서드가 없으니까 ObjectUitls.isEmpty메서드 사용 if(!ObjectUtils.isEmpty(boardFile)) { String fileName = boardFile.getOriginalFileName(); int fileSize = (int)boardFile.getFileSize(); // 파일의 경로를 바이트배열로 읽어온다. UrlResource resource = new UrlResource("file:" + boardFile.getStoredFilePath()); // 파일 이름 URL 주소로 사용하기 전에 인코딩하기 String encodedFileName = UriUtils.encode(fileName, StandardCharsets.UTF_8); // Content-Disposition: HTTP 응답의 처리 방식, 브라우저가 반환된 내용을 화면에 표시할 것인지 파일로 다운로드할 것인지 결정한다. // attachment : 반환된 내용(인코딩된 파일 이름)을 파일로 다운로드 해라 String contentDisposition = "attachment; filename=\"" + encodedFileName + "\""; // ResponseEntity.ok() : ResponseEntity의 메서드 상태 코드 200(OK)를 가진 ResponseEntity 객체 생성 // header() : HTTP 응답의 헤더 // HttpHeaders.CONTENT_DISPOSITION : HTTP 헤더 중 Content-Disposition이라는 헤더의 이름 // 헤더에 contentDisposition이라는 변수를 넣어준다. (다운로드할 것을 명시하는 attachment와 파일이름) // body(resource) : HTTP 응답의 본문 / 위에서 생성한 UrlResource 객체 - 실제 파일의 내용 return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,contentDisposition).body(resource); } }
[ 4. BoardFileService 작성 ]
// 하나의 게시글에서 다운 받고자하는 파일 정보 조회 // 서비스에서는 다운로드에 필요한 파일의 정보만 가져올 뿐 다운로드에 직접적인 관련이 없다. public BoardFileDto selectFileInfo(BoardFileDto file) throws Exception{ BoardFileDto boardFile = boardMapper.selectFileInfo(file); return boardFile; }
728x90