[Spring] 파일 업로드

dooboocookie·2023년 1월 17일
0
post-thumbnail

목표

  • 병원 등록 시, 병원에 대한 이미지가 들어간다.
  • 병원 이미지에 대한 이미지를 사용자가 등록할 수 있어야한다.

HTTP Form

HTTP (Hypertext Transfer Protocol)

클라이언트 - 서버간 요청과 응답을 통하여 정보를 주고받을 때 사용하는 통신 규약이다.

요청 메세지 구조

  • 요청 메세지의 구조는 다음과 같이 나타낼 수 있고
  1. Request Line에는 요청 상태를 나타낸다.
    • ex) GET / HTTP/1.1
  2. HTTP Header에는 요청을 지정하거나 바디를 정의하는 내용을 포함한다.
  3. Empty Line은 빈줄이다.
  4. Message Body
    • 본문 내용이 들어간다.

< form > 태그 - 폼 데이터

  • 입력 내용(폼 데이터)을 감싸고 있는 태그로
  • 입력 내용을 어디로, 어떻게 보낼지를 정의하는 태그이다.
    • action : 요청을 보낼 위치를 지정한다.
    • method : 요청을 어떤 방식을 보낼지 지정한다. (ex. GET, POST)
    • enctype : 폼 데이터를 서버로 보낼 때 인코딩 되는 방식을 결정한다.

인코딩 방식

  • application/x-www-form-urlencoded (디폴트)
    • 기본 인코딩 방식
    • enctype속성이 입력되지 않았거나 application/x-www-form-urlencoded로 입력하면 Content-Type: application/x-www-form-urlencoded이 HTTP Header에 추가된다.
    • 폼 데이터를 &로 나눠서 나열해서 보낸다
    • ex) name=dooboo&age=5name:dooboo, age:5
  • multipart/form-data
    • 모든 문자를 인코딩하지 않음
    • 파일을 전송할 때 보통 사용한다.
    • 아래와 같이 Content-TypeL multipart.form data; boundary= ---...이 HTTP Header에 추가된다.

      POST / HTTP/1.1
      ...
      Content-type: multipart/form-data; boundary=---------------------------asd123456
      ...
      --------------------------asd123456
      content-disposition : form-data; name="file"; filename="테스트.png"
      content-type : image/png
      ...
      9��7�xCL�vp�c��38�c8Z��K��id))WR%��� N...(바이너리 데이터)
      --------------------------asd123456
      ...
      --------------------------asd123456
      ...
      --------------------------asd123456--

    • boundary 뒤의 --- ... 부분은 Part들을 나누기 위한 구분자이다.
    • 마지막에는 구분자에 --를 추가적으로 붙인다.

서블릿에서 multipart/form-data 받기

httpServletRequest.getPart()

  • multipart로 전송된 데이터를 반환하는 메소드
  • 반환
    • Collection< Part >
    • HTTP 요청에 multipart 데이터 즉, 각각의 Part를 담고있는 Collection으로 반환

Part 객체

  • multipart로 넘어온 폼 데이터의 각각이 Part 이다.

part.getHeaderNames()

  • Part의 해더 이름들을 반환한다.
  • Collection< String > 반환

part.getHeader(String headerName)

  • Part의 headerName이름을 가진 헤더 정보를 가져온다.
  • String 반환

part.getInpuStream();

  • Part의 데이터(내용)을 반환한다.
  • InputStream 반환

part.getSubmittedFileName()

  • Part 파일의 이름을 반환한다
  • String

part.write(String path)

  • 해당 위치에 Part의 데이터(내용)을 저장한다.
@Slf4j
@Controller
public class SaveTestController {

    @PostMapping("/save")
    public String save(HttpServletRequest request) throws ServletException, IOException {
        log.info("request={}", request);

		String fileDir = "파일저장할 경로";
        
        String itemName = request.getParameter("itemName");
        log.info("itemName={}", itemName);

        Collection<Part> parts = request.getParts();
        log.info("parts={}", parts);

        for (Part part : parts) {
            Collection<String> headerNames = part.getHeaderNames();
            
            // 파일 명
            log.info("submittedFileName={}", part.getSubmittedFileName());
            // 크기 (용량)
            log.info("size={}", part.getSize());

            //테이터 읽기
            String body = StreamUtils.copyToString(part.getInputStream(), StandardCharsets.UTF_8);
            log.info("body={}", body);

            //파일에 저장하기
            if (StringUtils.hasText(part.getSubmittedFileName())) {
                String fullPath = fileDir + part.getSubmittedFileName();
                log.info("파일 저장 fullPath={}", fullPath);
                part.write(fullPath);
            }
        }

        return "...";
    }
}

MultipartFile

  • 스프링에서 제공하는 multipart/data-form으로 넘어온 파일을 사용하기 위한 인터페이스

multipartFile.getOriginalFilename()

  • 넘어온 파일의 원래 이름을 반환한다.

multipartFile.transferTo(File file)

  • 바이너리 데이터로 넘어온 파일을 File file에 파일을 작성한다.

Spring MVC에서 활용

  • @RequestParam, @ModelAttribute를 통하여 해당 파일 파라미터를 변수나 필드에 바인딩해서 사용할 수 있다.
<input type="file" name="file">
  • 위와 같이 name이 file로 넘어간 파일 파라미터를 아래처럼 스프링에서 받을 수 있다.
@PostMapping("/fileUpload")
public String fileUpload(@RequestParam List<MultipartFile> file) throws IOException {

	String fileDir = "파일 저장 경로";
    
    if (!file.isEmpty()) {
    	// 이 위치에 파일 저장
        String fullPath = fileDir + file.getOriginalFilename();
        file.transferTo(new File(fullPath));
        
    	// 파일 저장 후 String fullPath와 파일 정보를 레파지토리의 save하는 과정이 필요함
        // 예를 들면, HosImg엔티티를 생성하고 영속성 컨텍스트에 저장하는 과정... 
        
    }  

    return "...";
}

실제 프로젝트 적용

  • 입력 폼
<form th:action method="post" th:object="${hospitalSaveForm}" enctype="multipart/form-data">
  	<div>
    	<label for="hosName">병원명</label>
    	<input type="text" th:field="*{hosName}" placeholder="입력하세요">
  	</div>
  	
  	<!-- 다른 파라미터 받는 인풋 태그-->
  	
  	<div>
    	<label for="hosImgs">사진 등록</label>
    	<input type="file" th:field="*{hosImgs}" multiple>
      	<!-- 이미지 목록을 나타낼 태그 -->
    	<div id="hosImgsList"></div>
  	</div>
  	<div>
    	<button type="submit"> 등록</button>
 	</div>
</form>
  • 입력 폼에서 넘어온 데이터를 바인딩하는 DTO
@Data
public class HospitalSaveForm {

    @NotBlank
    private String hosName;
    
	// ...
    
    private List<MultipartFile> hosImgs;
    
}
  • 병원을 저장하는 요청을 받는 컨트롤러
@PostMapping(value = "/add")
public String add (HospitalSaveForm hospitalSaveForm) {
    //예외처리 생략

    /*병원 저장*/
    Long hospitalId = hospitalService.registerHos(hospitalSaveForm);

    return "redirect:/hospital/detail/" + hospitalId;

}
  • 병원병원 이미지를 영속성 컨텍스트에 저장하는 과정
@Transactional
public Long registerHos(HospitalSaveForm hospitalSaveForm) {
    // 병원 엔티티 생성
    Hospital hospital = Hospital.createHospital(...);
    // 병원을 저장 (병원 영속성 시작)
    hospitalRepository.save(hospital);
    // 병원 이미지 저장
    for (MultipartFile file : files) {
        if(!file.isEmpty()) {
            HosImg hosImg = HosImg.createHosImg(hospital, file);
            hosImgRepository.save(hosImg);
        }
    }
    return hospital.getHosId();
}
  • 병원 이미지를 엔티티 생성 과정
@Slf4j
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class HosImg extends TimeStamped {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long himId;
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "hosId") //hosImg 테이블의 컬럼명
    private Hospital hospital; //hospital PK
    @Column(length = 1000)
    private String himPath;
    private boolean himMain;
    @Column(length = 1000)
    private String himOrigin;


    /*== 생성 메소드 ==*/
    public static HosImg createHosImg(Hospital hospital, MultipartFile file){
        String path = file.getOriginalFilename();
        String savedPath = "파일 저장 경로";
        File existChk = new File(savedPath);
        if(!existChk.exists()) existChk.mkdirs(); //해당 경로가 없다면 디렉토리 만들기
        String savedFile = UUID.randomUUID().toString().replaceAll("-", "") // 서버에 저장할 파일 명으로 쓸 UUID (중복방지)
                + path.substring(path.lastIndexOf(".")); // 확장자명
        try {
            file.transferTo(new File(savedPath, savedFile)); // 서버에 파일로서 저장
        } catch (IOException e) {
            e.printStackTrace();
        }
        return HosImg.builder()
                .himPath("/images/upload/" + savedFile)
                .hospital(hospital)
                .himOrigin(path)
                .build();
    }

}
profile
1일 1산책 1커밋

0개의 댓글