쌩로그

Spring Controller 계층에서 파일 입력받기 본문

Spring/Spring & Spring Boot

Spring Controller 계층에서 파일 입력받기

.쌩수. 2023. 11. 19. 16:51
반응형

목록

  1. 포스팅 개요
  2. 본론
  3. 요약

1. 포스팅 개요

실무에서 Vue와 SpringBoot를 이용해서 파일을 업로드해야하는 상황이 있었습니다.

처음 시도해봤었는데, 적용했고, 잊지않고자 기록합니다.

정말 간단히 세팅했고, Controller로만 확인해보도록 하겠습니다.


참고로 프로젝트 시작시에 일어나는 에러가 있긴했습니다.
yml에 아무것도 추가하지 않고, 최소한으로 시도하려 했습니다만,
log를 사용하기로 했는데, 로깅 레벨을 추가하지 않거나,
database를 사용한다고 gradle에 추가해놓고, database 정보를 주지 않거나, 혹은 Spring Security를 추가했는데, 프론트에서 백으로 403 에러가 나는 부분이 있었지만,
yml에 파일 설정을 더해줬고, Spring Security는 gradle에서 비활성화 시켰다고 구두로 알립니다.


2. 본론

프론트 셋팅

muliple

muliple은 HTML의 input 태그의 속성 중 하나로,
파일을 하나만 받거나, 여러 개 받는 여부에 대한 키워드입니다.
input 태그에 type은 file이면, 파일을 받을 수 있는데 하나만 받을 수 있습니다.
multiple을 추가하면, 여러 개를 입력받을 수 있습니다.

  <div>
    <ul>
<!--      <li><input type="file" @change="files"></li>  파일 하나만 받기 -->
      <li><input type="file" multiple @change="handleFileChange"></li>
      <li><input type="text" v-model="title" placeholder="title"></li>
      <li><input type="text" v-model="content" placeholder="content"></li>
      <li><button v-on:click="post">저장</button></li>
    </ul>
  </div>

위의 소스는 Vue 템플릿에 대한 내용입니다.

제일 위의 input 태그에 muliple 속성의 입력여부에 따른 차이를 보겠습니다.

현재 muliple을 추가한 상태로 파일을 2개 입력받은 상태이고,
다음 사진도 보겠습니다.

제 컴퓨터에 저장된 짤들(?)입니다...;;
자주 쓰는 짤입니다...;;ㅎㅎㅎㅋㅋㅋ

여하튼 이렇게 복수로 선택되는 것을 확인할 수 있습니다.

반면에, multiple을 제외해보겠습니다.

쉬프트 혹은 컨트롤을 누르고, 다른 파일들을 눌러봐도 only one, 즉 하나만 선택됩니다.

이처럼 multiple은 파일 입력 개수를 1개 와 여러 개로 조절가능합니다.

아직 JS의 이해가 많이 부족하여 GPT의 도움을 좀 받아서 수정했습니다.

먼저 Vue에 대한 코드입니다.

<template>
  <div>
    <ul>
<!--      파일 하나만 받기-->
<!--      <li><input type="file" @change="files"></li>-->
      <li><input type="file" multiple @change="handleFileChange"></li>
      <li><input type="text" v-model="title" placeholder="title"></li>
      <li><input type="text" v-model="content" placeholder="content"></li>
      <li><button v-on:click="post">저장</button></li>
    </ul>
  </div>
</template>

<script>
import {reactive, ref} from "vue";
import axios from "axios";

export default {
  setup() {

    const title = ref('');
    const content = ref('');
    const files = reactive([]);

    const handleFileChange = (event) => {
      files.length = 0; // Clear existing files
      for (let i = 0; i < event.target.files.length; i++) {
        files.push(event.target.files[i]);
      }
    };

    const post = async () => {
      const formData = new FormData();
      formData.append('title', title.value);
      formData.append('content', content.value);

      for (let i = 0; i < files.length; i++) {
        formData.append('files', files[i]);
      }

      console.log(formData);

      try {
        const response = await submitData(formData);
        console.log(response.data);
      } catch (error) {
        console.error(error);
      }
    };

    const submitData = async (data) => {
      try {
        const responseData = await axios.post('/api/toy/write', data);
        console.log(responseData);
        return responseData;
      } catch (error) {
        throw new Error(error);
      }
    };

    return {
      title, content, files, post, handleFileChange
    };
  },
};
</script>

<style lang="css" scoped>


</style>

UI는 음.. 병맛 같은데, 다음과 같습니다...

프론트 세팅은 끝났습니다.

백엔드 세팅

참고로 폴더 구조는 이처럼 간단하게 했습니다.

컨트롤러입니다.


@Slf4j
@RestController
@RequestMapping("toy")
public class ToyController {

    @PostMapping("write")
    ResponseEntity toyWrite(Dto dto) {
        log.info("title = {}",dto.getTitle());
        log.info("contnet = {}",dto.getContent());

        log.info("======");
        log.info("======");
        log.info("======");


        if (!Objects.isNull(dto.getFiles())) {

            for (MultipartFile file : dto.getFiles()) {
                log.info("file 컨텐츠 타입 = {}", file.getContentType());
                log.info("file 실제 이름 = {}", file.getOriginalFilename());
                log.info("file 해당 파일의 key = {}", file.getName());
            }
        }
        return new ResponseEntity(HttpStatus.CREATED);
    }
}

DTO입니다.

@Getter
@NoArgsConstructor
public class Dto {
    String title;
    String content;
    List<MultipartFile> files;


    @Builder
    public Dto(String title, String content, List<MultipartFile> files) {
        this.title = title;
        this.content = content;
        this.files = files;
    }

}

파일 같은 경우는 JSON형식으로 받지 못 합니다.

FormData로만 받아야 하는데, 그럴려면 @ModelAttribute를 사용할 수 있습니다.
그런데 치명적이게도 @ModelAttribute는 @Setter를 사용해야합니다.

저는 @Setter 사용을 지향하고 싶어서 받은 데이터를 직접 접근하여 바인딩할 수 있도록 설정했습니다.

그 설정을 Config 클래스에 했습니다.

@ControllerAdvice
public class Config {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.initDirectFieldAccess();
    }

}

이 내용은 제가 블로그로 포스팅해놨는데, 참고하실분들은 여기를 참고해주세요.
참고로 향로님께서 쓰신 블로그를 참고했고, 거기에 참고블로그로 링크도 있다는 점 말씀드립니다.

이제 해보겠습니다.

다음과 같이 하여 저장해보고 IntelliJ를 확인해보도록 하겠습니다.

결과는 다음과 같습니다.

이처럼 잘 받았습니다.

결론적으로 FormData 형식으로 파일을 받으려면, MultipartFile 타입으로 받으면 됩니다.
그럼 보시는 바와 같이 정보들이 출력됩니다.

만약 여러 개의 파일을 받고 싶으면, List 타입으로 받으면 여러 파일을 받을 수 있습니다.

만약 무조건 파일을 하나만 받아야한다! 는 것이 아니라면,
List으로 받아서 유연하게 파일 입력을 받으면 어떨까 생각됩니다.

3. 요약

간단히 스프링의 컨트롤러로부터 파일을 요청받는 방법에 대해 알아보도록 하겠습니다.
사실 Vue에서 백엔드로 파일을 저렇게 입력하면, CORS 에러가 나오는데,,,,

흠... CORS 와 관련된 내용은 곧 다뤄보도록 하겠습니다.

결론을 다시 말씀드리면, 다음과 같습니다.

MultipartFile 타입을 이용하여 파일입력을 받을 수 있다.

-끝-

728x90
Comments