국비교육(22-23)

61일차(1)/Spring Boot(10) : file, gallery 게시판 파일 저장경로, 다운로드 기능 수정 / Boot 기능 활용

서리/Seori 2023. 1. 3. 17:33

61일차(1)/Spring Boot(10) : 파일 저장경로, 다운로드 기능 수정

 

- file 자료실게시판 저장경로, 다운로드 관련 수정

- gallery 게시판 저장경로 수정

 

 

- Spring에서 작업한 프로젝트를 Spring Boot의 추가된 기능을 활용해서 똑같이 구현해보기!

- 호환성을 고려해서 최대한 수정을 적게 하면서 이식함

- 파일경로 옮기는 브랜치는 별도로 두고 master에 merge 하지는 않을 예정!

 

- 각각의 브랜치가 어떤 파일을 만들기 위한 것인지 알아두기!

 

- war는 tomcat으로 가져다 놓고 실행할 수 있었지만, jar파일은 실행방식이 좀 다르다.

- 빨간색이 좀더 Spring Boot 다운 방식이다.

- war파일 버전은 레거시 프로젝트 환경에 가능한 맞춘 것이다.

 

 

- galleryService

 

 

- 필드에 @Value 어노테이션을 붙여놓으면

 application.properties에서 지정한(커스텀한) 값을 읽어와서 이곳에 넣어준다.

- 이 값을 주입받는다는 느낌으로 생각하면 된다.

- 키 값만 일치한다면 이곳에 값이 들어온다. 숫자, 문자, 배열, 리스트 형태 모두 가능!

- ★properties에 명시하는 값들은 서버의 실행환경에 따라서 바뀔 가능성이 큰 값들이다.

 

- 레거시 프로젝트에서도 properties 를 사용하려면 할 수는 있다. 하지만 굳이..?

 

 

- 리눅스였다면 최상위경로가 그냥 / 이다. /data 라고 바꾸게 될 것!

- 상황에 따라 바뀌는 값이라는 점에 유의

 

 

- 이렇게 연결된다. 각각의 위치에 들어갈 이름을 일치시켜주면 된다.

 

 

- 파일 구분자를 넣어서 구성한 것을 뒤에 저장할 파일명을 넣어서 저장한다.

- 그 저장의 결과가 C 드라이브 data 폴더에 저장된다.

 

- 그런데 클라이언트는 원격지서버의 파일시스템 내에 있는 이 파일을 볼 수 없다.

- 이것은 공개된 폴더가 아니니까! webapp안에 있는 폴더가 아니다.

 

- 클라이언트에게 이 이미지를 보여주고 싶다면 컨트롤러가 필요하다.

 


 

GalleryController 메소드 수정

@GetMapping(
        value="/gallery/images/{imageName}",
        produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE, MediaType.IMAGE_GIF_VALUE}
    	)
@ResponseBody
public byte[] galleryImage(@PathVariable("imageName") String imageName) throws IOException {

    String absolutePath=fileLocation+File.separator+imageName;
    //파일에서 읽어들일 InputStream
    InputStream is=new FileInputStream(absolutePath);
    //이미지 데이터(byte)를 읽어서 배열에 담아서 클라이언트에게 응답한다.
    return IOUtils.toByteArray(is);
}

 

 

- 어제 만들었던 메소드 GalleryController에 옮겨주기(이름, 경로 값 /images로 바꿈)

 

- 갤러리-list 에서 이미지를 나타나게 하는 경로도 바꾸기!

 

 

WebConfig 수정

 

- 인터셉터에서 이렇게 하위요청을 하면 로그인하지 않아도 이미지가 보이게 할 수 있다.

 


 

- application.properties의 경우 이름을 바꾸면 안된다. 바꾸면 서버가 동작하지 않는다.

 

- 같은 위치(resources)에 custom properties 라는 이름의 파일 생성!

 

 

- 이렇게 작성하고 실행해 보면 ${file.location}을 못 찾겠다는 에러메시지가 나온다.

 

- 어디에 있는지 알려줘야 한다! 메인 메소드로 가서 @PropertySource 어노테이션 추가.

 

package com.sy.boot07;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.PropertySource;

@PropertySource("classpath:custom.properties") //resources/custom.propeties 로딩하기
@SpringBootApplication
public class Boot07FinalApplication {

	public static void main(String[] args) {
		SpringApplication.run(Boot07FinalApplication.class, args);
	}

}

 

@PropertySource

- classpath:/ 해서 custom properties의 위치를 알려주는 어노테이션. 위치는 resources이다.

- 그러면 spring이 기억하고 있다가 @Value("${ }") 로 적어놓은 필드에 넣어준다.

 

- 필요하다면 얼마든지 이렇게 외부 properties 파일을 만들 수 있다.

- 이것을 java 클래스에서 어노테이션으로 작성할 수 있다.

- 클래스를 수정하지 않고 단순히 텍스트 파일을 수정하는 것만으로도 앱에 변화를 줄 수 있다는 점이 장점!

 


 

- 자세히보기에 들어가도 이미지가 보이려면 detail.jsp 페이지의 경로도 바꿔주어야 한다.

- gallery/images/ 경로만 추가해주면 된다.

 

- 경로의 모양을 보면, 그리고 클라이언트가 느끼기에는 webapp 안에 이미지가 있는것처럼 보이지만,

  실제로는 서버의 C:\\data 폴더의 이미지이다.

- 저 이미지에 대해서 응답할 컨트롤러의 메소드를 만들었기 때문에 가능한 것이다.

 

- 서버가 imageName을 경로변수로 설정해서

 경로변수를 활용해서 imageName을 읽어내서 실제 c드라이브 데이터 폴더의 데이터를 응답했다.

- 결국 컨트롤러가 있기 때문에 가능한 일!

 

 

GalleryController

//프로필 이미지 요청에 대한 응답을 할 메소드를 따로 만들어야 한다.
//이미지 데이터가 응답되어야 한다.
//웹브라우저에게 이미지 데이터를 응답한다고 알려야 한다.
//응답할 이미지의 이름은 그때 그때 다르다.

/*
 * 이 컨트롤러의 메소드에서 응답한 byte[] 배열을 클라이언트에게 응답하는 방법
 * 1. @ResponseBody
 * 2. byte[] 배열 리턴
 * 
 * 응답된 byte[] 배열에 있는 데이터를 이미지 데이터로 클라이언트 웹브라우저가 인식하게 하는 방법
 * produces = MediaType.IMAGE_JPEG_VALUE 
 */
@GetMapping(
        value="/gallery/images/{imageName}",
        produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE, MediaType.IMAGE_GIF_VALUE}
    )
@ResponseBody
public byte[] galleryImage(@PathVariable("imageName") String imageName) throws IOException {

    String absolutePath=fileLocation+File.separator+imageName;
    //파일에서 읽어들일 InputStream
    InputStream is=new FileInputStream(absolutePath);
    //이미지 데이터(byte)를 읽어서 배열에 담아서 클라이언트에게 응답한다.
    return IOUtils.toByteArray(is);
}

 

- 컨트롤러에서 동적인 이미지 파일을 읽어낼 수 있도록 정의하고,

 메소드 안에서 inputStream으로 읽어내서 바이트 배열을 만들어내서 응답

 

- 바이트 배열을 리턴하려면 꼭 @ResponseBody 도 있어야 한다!

 

** 이 컨트롤러의 메소드에서 응답한 byte[] 배열을 클라이언트에게 응답하는 방법
1. @ResponseBody
2. byte[ ] 배열 리턴
  
- 응답된 byte[] 배열에 있는 데이터를 이미지 데이터로 클라이언트 웹브라우저가 인식하게 하는 방법
  produces = MediaType.IMAGE_JPEG_VALUE 

 

- 이 바이트배열이 이미지가 아니라 그냥 일반 데이터로 인식될 수도 있다.

- 실제 이미지로 화면상에 보여주게 하려면 produces가 필요한 것이다!

 

- 클라이언트 브라우저에게 내가 지금 이미지 데이터를 응답할 건데, 이미지 응답하게 해줘~ 라고

 알려주면서 요청하는 것!

 


 

ajax 폼 수정하기(업로드폼2)

 

galleryService

//이미지 ajax upload
public Map<String, Object> uploadAjaxImage(GalleryDto dto, HttpServletRequest request){
    //업로드된 파일의 정보를 가지고 있는 MultipartFile 객체의 참조값을 얻어오기
    MultipartFile image = dto.getImage();
    //원본 파일명 -> 저장할 파일 이름 만들기위해서 사용됨
    String orgFileName = image.getOriginalFilename();
    //파일 크기
    long fileSize = image.getSize();

    // 파일을 저장할 서버에서의 절대 경로		
    String realPath = fileLocation;
    //db 에 저장할 저장할 파일의 상세 경로
    String filePath = realPath + File.separator;
    //디렉토리를 만들 파일 객체 생성
    File upload = new File(filePath);
    if(!upload.exists()) {
        //만약 디렉토리가 존재하지X
        upload.mkdir();//폴더 생성
    }
    //저장할 파일의 이름을 구성한다. -> 우리가 직접 구성해줘야한다.
    String saveFileName = System.currentTimeMillis() + orgFileName;

    try {
        //upload 폴더에 파일을 저장한다.
        image.transferTo(new File(filePath + saveFileName));
        System.out.println();	//임시 출력
    }catch(Exception e) {
        e.printStackTrace();
    }

    String imagePath = saveFileName;

    //ajax upload 를 위한 imagePath return
    Map<String, Object> map = new HashMap<String, Object>();
    map.put("imagePath", imagePath);

    return map;
}

 

- 위를 아래로 수정함. fileLocation 경로 사용!

 

- 위를 아래로 수정. 저장된 파일명만 경로에 담기

- 이렇게 해주면 map을 @ResponseBody로 받아서 저장된 실제 이름만 json으로 응답할 수 있게 된다.

 

- gallery/ajax_form.jsp 수정

- /gallery/images/만 추가로 넣어주면 된다.

 

- 폼을 확인해보면 hidden으로 이미지 이름(파일명)을 들고 간다.

- 이미지 업로드시 이렇게 이름만 저장되는 것을 볼 수 있다.

 

- 폼에는 저장된 경로가 저장된다.

- 업로드한 후에 보면 gallery/images/ 경로에 잘 들어가 있다.

 


 

자료실 기능 수정

 

FileService

package com.sy.boot07.file.service;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.servlet.ModelAndView;

import com.sy.boot07.file.dto.FileDto;

public interface FileService {
	//파일 목록 얻어오기 
	public void getList(HttpServletRequest request);
	//업로드된 파일 저장하기 
	public void saveFile(FileDto dto, ModelAndView mView,
	      HttpServletRequest request);
	//파일하나의 정보 얻어오기 
	public FileDto getFileData(int num);
	//파일 삭제하기
	public void deleteFile(int num, HttpServletRequest request);

}

 

FileServiceImpl

package com.sy.boot07.file.service;

import java.io.File;
import java.net.URLEncoder;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;

import com.sy.boot07.exception.NotDeleteException;
import com.sy.boot07.file.dao.FileDao;
import com.sy.boot07.file.dto.FileDto;


@Service
public class FileServiceImpl implements FileService{

	@Autowired
	private FileDao dao;
	
	// custom.properties 파일에 작성한 내용 읽어오기
	@Value("${file.location}")
	private String fileLocation;

   @Override
   public void getList(HttpServletRequest request) {
      //한 페이지에 몇개씩 표시할 것인지
      final int PAGE_ROW_COUNT=5;
      //하단 페이지를 몇개씩 표시할 것인지
      final int PAGE_DISPLAY_COUNT=5;

      //보여줄 페이지의 번호를 일단 1이라고 초기값 지정
      int pageNum=1;

      //페이지 번호가 파라미터로 전달되는지 읽어와 본다.
      String strPageNum=request.getParameter("pageNum");
      //만일 페이지 번호가 파라미터로 넘어 온다면
      if(strPageNum != null){
         //숫자로 바꿔서 보여줄 페이지 번호로 지정한다.
         pageNum=Integer.parseInt(strPageNum);
      }   

      //보여줄 페이지의 시작 ROWNUM
      int startRowNum=1+(pageNum-1)*PAGE_ROW_COUNT;
      //보여줄 페이지의 끝 ROWNUM
      int endRowNum=pageNum*PAGE_ROW_COUNT;

      /*
         [ 검색 키워드에 관련된 처리 ]
         -검색 키워드가 파라미터로 넘어올수도 있고 안넘어 올수도 있다.      
      */
      String keyword=request.getParameter("keyword");
      String condition=request.getParameter("condition");
      //만일 키워드가 넘어오지 않는다면 
      if(keyword==null){
         //키워드와 검색 조건에 빈 문자열을 넣어준다. 
         //클라이언트 웹브라우저에 출력할때 "null" 을 출력되지 않게 하기 위해서  
         keyword="";
         condition=""; 
      }

      //특수기호를 인코딩한 키워드를 미리 준비한다. 
      String encodedK=URLEncoder.encode(keyword);

      //FileDto 객체에 startRowNum 과 endRowNum 을 담는다.
      FileDto dto=new FileDto();
      dto.setStartRowNum(startRowNum);
      dto.setEndRowNum(endRowNum);

      //만일 검색 키워드가 넘어온다면 
      if(!keyword.equals("")){
         //검색 조건이 무엇이냐에 따라 분기 하기
         if(condition.equals("title_filename")){//제목 + 파일명 검색인 경우
            dto.setTitle(keyword);
            dto.setOrgFileName(keyword);
         }else if(condition.equals("title")){ //제목 검색인 경우
            dto.setTitle(keyword);
         }else if(condition.equals("writer")){ //작성자 검색인 경우
            dto.setWriter(keyword);
         } // 다른 검색 조건을 추가 하고 싶다면 아래에 else if() 를 계속 추가 하면 된다.
      }


      //파일 목록을 select 해 온다.(검색 키워드가 있는경우 키워드에 부합하는 전체 글) 
      List<FileDto> list=dao.getList(dto);

      //전체 글의 갯수(검색 키워드가 있는경우 키워드에 부합하는 전체 글의 갯수)
      int totalRow=dao.getCount(dto);

      //하단 시작 페이지 번호 
      int startPageNum = 1 + ((pageNum-1)/PAGE_DISPLAY_COUNT)*PAGE_DISPLAY_COUNT;
      //하단 끝 페이지 번호
      int endPageNum=startPageNum+PAGE_DISPLAY_COUNT-1;

      //전체 페이지의 갯수 구하기
      int totalPageCount=(int)Math.ceil(totalRow/(double)PAGE_ROW_COUNT);
      //끝 페이지 번호가 이미 전체 페이지 갯수보다 크게 계산되었다면 잘못된 값이다.
      if(endPageNum > totalPageCount){
         endPageNum=totalPageCount; //보정해 준다. 
      }

      //응답에 필요한 데이터를 view page 에 전달하기 위해  request scope 에 담는다
      request.setAttribute("list", list);
      request.setAttribute("pageNum", pageNum);
      request.setAttribute("startPageNum", startPageNum);
      request.setAttribute("endPageNum", endPageNum);
      request.setAttribute("totalPageCount", totalPageCount);
      request.setAttribute("keyword", keyword);
      request.setAttribute("encodedK", encodedK);
      request.setAttribute("totalRow", totalRow); 
      request.setAttribute("condition", condition);
   }

	@Override
	public void saveFile(FileDto dto, ModelAndView mView, HttpServletRequest request) {
	      //업로드된 파일의 정보를 가지고 있는 MultipartFile 객체의 참조값 얻어오기 
	      MultipartFile myFile=dto.getMyFile();
	      //원본 파일명
	      String orgFileName=myFile.getOriginalFilename();
	      //파일의 크기
	      long fileSize=myFile.getSize();
	      
	      //파일을 저장할 폴더의 실제 경로 C:\data	      
	      String realPath=fileLocation;
	      //저장할 파일의 상세 경로
	      String filePath=realPath+File.separator;
	      //디렉토리를 만들 파일 객체 생성
	      File upload=new File(filePath);
	      if(!upload.exists()) {//만일 디렉토리가 존재하지 않으면 
	         upload.mkdir(); //만들어 준다.
	      }
	      //저장할 파일 명을 구성한다.
	      String saveFileName=
	            System.currentTimeMillis()+orgFileName;
	      try {
	         //upload 폴더에 파일을 저장한다.
	         myFile.transferTo(new File(filePath+saveFileName));
	         System.out.println(filePath+saveFileName);
	      }catch(Exception e) {
	         e.printStackTrace();
	      }
	      //dto 에 업로드된 파일의 정보를 담는다.
	      String id=(String)request.getSession().getAttribute("id");
	      dto.setWriter(id); //세션에서 읽어낸 파일 업로더의 아이디 
	      dto.setOrgFileName(orgFileName);
	      dto.setSaveFileName(saveFileName);
	      dto.setFileSize(fileSize);
	      //fileDao 를 이용해서 DB 에 저장하기
	      dao.insert(dto);
	      //view 페이지에서 사용할 모델 담기 
	      mView.addObject("dto", dto);		
	}

	@Override
	public FileDto getFileData(int num) {
		//다운로드할 파일의 정보를 얻어와서 리턴
		return dao.getData(num);				
	}

	@Override
	public void deleteFile(int num, HttpServletRequest request) {

		//삭제할 파일의 정보를 얻어오기
		FileDto dto=dao.getData(num);

		//글 작성자와 로그인된 아이디가 일치하는지 확인해서 일치하면 삭제하고, 일치하지 않으면 예외를 발생시키기
		String id=(String)request.getSession().getAttribute("id");
		if(!dto.getWriter().equals(id)) {
			//예외를 발생시키면 해당 예외를 처리하는 곳으로 실행의 흐름이 넘어간다.
			throw new NotDeleteException("남의 파일 지우기 없기!");
		}

		//파일 시스템에서 삭제
		String saveFileName=dto.getSaveFileName();
		String path=fileLocation+File.separator+saveFileName;
		//경로를 사용해서 파일객체를 생성해서
		new File(path).delete();
		//DB에서 파일 정보 삭제
		dao.delete(num);		
	}
}

 

FileController

package com.sy.boot07.file.controller;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.sy.boot07.file.dto.FileDto;
import com.sy.boot07.file.service.FileService;

@Controller
public class FileController {

	@Autowired
	private FileService service;
	
	@Value("${file.location}")
	private String fileLocation;
	
	@RequestMapping("/file/list")
	public String list(HttpServletRequest request) {
		service.getList(request);
		
		return "file/list";
	}
	
	@RequestMapping("/file/upload_form")
	public String uploadForm() {
		return "file/upload_form";
	}
	
	@RequestMapping("/file/upload")
	public ModelAndView upload(FileDto dto, ModelAndView mView, HttpServletRequest request) {
		service.saveFile(dto, mView, request);
		mView.setViewName("file/upload");
		return mView;
	}
	/*
	 * 컨트롤러에서 파일을 직접 다운로드 시켜주기
	 * 
	 * 1. ResponseEntity 객체에 다운로드해줄 파일의 정보를 담고
	 * 2. ResponseEntity<InputStreamResource> 객체를 리턴해준다.
	 */
	@GetMapping("/file/download")
	public ResponseEntity<InputStreamResource> download(int num) throws UnsupportedEncodingException, FileNotFoundException {
		FileDto dto=service.getFileData(num);
		//다운로드 시켜줄 원본파일명(인코딩 필요)
		String encodedName=URLEncoder.encode(dto.getOrgFileName(), "utf-8");
		//파일명에 공백이 있는 경우 파일명이 이상해지는 것을 방지
		encodedName=encodedName.replaceAll("\\+", " ");
		//응답 헤더정보 구성하기(웹브라우저의 알림정보)
		HttpHeaders headers=new HttpHeaders();
		//파일을 다운로드 시켜주겠다는 정보
		headers.add(HttpHeaders.CONTENT_TYPE, "application/octet-stream");		
		//파일의 이름 정보(웹브라우저가 해당정보를 이용해서 파일을 만들어준다)
		headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename="+encodedName);
		//파일의 크기 정보
		headers.setContentLength(dto.getFileSize());
		//읽어들일 파일의 경로 구성
		String path=fileLocation + File.separator + dto.getSaveFileName();
		//파일에서 읽어들일 스트림 객체
		InputStream is=new FileInputStream(new File(path));
		/*
		 * 미리 준비된 헤더정보와 추가 헤더정보, 파일에서 읽어들일 스트림 객체를 이용해서
		 * ResponseEntity 객체의 참조값을 얻어낸다.
		 */
		ResponseEntity<InputStreamResource> re=ResponseEntity.ok()
		.headers(headers)
		.header("Content-Transfer-Encodeing", "binary")
		.body(new InputStreamResource(is));
		
		//ResponseEntity 객체를 리턴해주면 알아서 파일에 자운로드가 된다.
		return re;
		
	}
	
	@RequestMapping("/file/delete")
	public ModelAndView delete(int num, ModelAndView mView, HttpServletRequest request) {
		service.deleteFile(num, request);
		mView.setViewName("redirect:/file/list");
		return mView;
	}
}

 

- bean이 되어있기만 하면 @Value는 어디에든 들어갈 수 있다.

 (@Autowired도 그렇다!)

 

- 저장할 때는 이렇게 작성!(위를 아래로 수정)

 

- 삭제할 때의 메소드에서도 경로 넣어주기

 

- 다운로드할때는? 원래는 위와 같은 fileDownView 를 사용해서 파일을 다운로드해주었는데,

 Boot를 쓰니까 컨트롤러를 거치는 것으로 바꿔보려 한다.

 

- 다운로드를 위해 ResponseEntity 객체를 사용한다.

 

 

- 다운로드할 파일의 정보를 읽어오는 메소드!

- 이전에는 mView를 받아서 사용했는데, 이제는 파일 정보를 리턴해주는 것으로 바꾸려고 함!

 

fileService 인터페이스수정

public FileDto getFileData(int num);

 

- ServiceImpl 수정

- ServiceImpl 에서 해당 메소드 수정! file 데이터를 dto에 담아서 리턴해준다.

 

- int 를 받아오는 것으로 FileController 수정

 

- ok() 메소드를 사용하면 응답의 body를 구성할 수 있는 builder인, BodyBuilder타입이 리턴된다.

 

 

- ok()메소드에서 헤더를 구성할수있다. 그리고 bodybuilder 타입이 계속 리턴된다.

- header.header. ... 으로 메소드를 계속 호출할 수 있다.

 

- 인코딩하고, 발생하는 exception은 throw 해준다.

 

- HttpHeaders를 import해서 헤더 정보 구성하기

- "application/octet-stream" : 파일 다운로드시에는 이 타입을 사용!

 

- 컨트롤러에도 @Value 추가

 

- 예외 발생하면 throw 해주기

 

- 해당 메소드를 호출한 곳에 계속 어떤 객체가 리턴되기 때문에, 이렇게 사용할수 있는 구조가 된다.

 

- 이렇게 만들면 view가 필요없다.

- 이렇게 따로담은 이유는 content encoding이 없어서 따로 넣어주었다.

 

ResponseEntity<InputStreamResource> 

- 이 객체를 사용하면 produce, Responsebody 등이 필요없다. 여기에 다 포함되어 있다.

- 응답에 필요한 정보를 다 담고 있는 객체라고 보면 된다.

- 다 담아놓으면 알아서 파일 다운로드를 시켜준다.

 

- 한글, 공백 인코딩에 유의하기.

- .replaceAll() 메소드 사용!

 

 

 

- 사전작업을 다 마친 후 인코딩된 이름을 브라우저에 전달하는 것.

 

 

.setContentLength() 

- .add() 대신 파일의 크기업로드를 담아주는 메소드로 바꿔주었다.

 

- 이미지 데이터를 응답하는 것과 파일을 다운로드시켜주는 것은 별개이다!
- 이미지를 응답할 때는 produce, contentType만 명시하면 된다.

- 다운로드는 ResponseEntity를 사용해야 한다.