[Spring] REST API방식으로 파일 받아오다 생긴 사건

조시현·2024년 5월 3일
0

Restapi

목록 보기
1/1
post-thumbnail

REST API방식으로 개발을 하는 도중 MultipartFile 객체로 요청을 받아와야하는 상황이 생겼습니다.
우선 첫번째 문제는 REST API 개발에 대해서 한번도 경험해본 적이 없다는 것이고
두번째 문제는 익숙하지 않는 파일을 다룬 다는 것이였다.

파일 입력

| 환경은 PostMan보다 가벼운 Talend API Tester를 사용하였습니다. 우리의 입력은 car 객체에 대해서 vin, modelName, color, mileage, img(이미지 이름)을 보내고 file에 이미지를 받아와야 한다.

위와 같은 입력을 주기 위해서는 파일을 포함해야 하므로 크게 2가지 방식이 있었습니다.

  1. multipart/form-data type을 통해서 body에 form형식으로 넣어 주는 방식
  2. application/json을 통해서 body에 text형식으로 넣어주는 방식이 있었습니다.

2번 방식을 하기 위해서는 json에서 파일을 넣어주기 위해서 multipart/form-data 형식으로 파일을 넣어야하는 추가적인 기법이 필요하며,
json방식으로 multipart/form-data형식을 사용하기 위해서는 front측 에서도 가공하기 위해서 추가적인 기법이 필요하다고 하여서

REST API 개발이 익숙하지 않은 상태에서 비교적 간단한 방식인 multipart/form-data 형식을 통해서 데이터를 주고 받기로 하였습니다.

고민 과정

문제를 해결 할 수 있는 메서드의 파라미터 방식은 2가지가 생각이 났는데,
1. Car에 대한 정보를 지닌 Car 객체와 MultipartFile 타입의 file 객체 두가지를 받아오는 방식
2. Car 객체에 MutilpartFile 타입의 변수를 포함시켜서 하나의 객체로 모두 받아오는 방식이 있다.

그리고 이 객체들을 받아올 방식들에 대해서 고민해야하는
크게 2가지를 고민해보아야 했습니다.

환경

아래는 우리가 보낼 데이터 이다.

Car에 대한 정보를 지닌 Car 객체와 MultipartFile 타입의 file 객체 두가지를 받아오는 방식

환경

아래 코드는 우리가 해결해야 할 메서드 이다.

@PostMapping("/car")
    public ResponseEntity<?> insert(________________________________) {
        try {
            int result = cs.insert(car, file);
            System.out.println(result);
            return new ResponseEntity<Integer>(result, HttpStatus.CREATED);
        } catch (Exception e) {
            return exceptionHandling(e);
        }
    }

1. @RequestBody를 이용하는 방식

    public ResponseEntity<?> insert(@RequestBody Car car, MultipartFile file)

웹에서는
HTTP 상태 415 – 지원되지 않는 Media Type
에러가 발생하였고,

서버에서는
10:10:09.893 WARN handler.AbstractHandlerExceptionResolver - Resolved org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type 'multipart/form-data;boundary=----WebKitFormBoundaryJbyVqELhytAu9DUJ;charset=UTF-8' is not supported
와 같은 에러가 발생하였다.
에러를 해석해보면,

요청을 보니
분명 multipart/form-data 형식으로 보냈지만,
application-json형식으로 값을 받고 있었다.

왜 그런가 검색하여 보니

@RequestBody는 DTO와 함께 JSON 또는 XML 데이터를 사용하는 것을 의미합니다. MultipartFile의 경우 JSON 데이터를 사용할 수 없으므로 @RequestBody를 사용할 수 없습니다. @ModelAttribute 어노테이션을 사용해 보세요.
라는 답변이 있었습니다.

@RequestBody는 JSON 또는 XML 데이터를 사용하는 경우만 요청을 받아올 수 있는 방식이여서
내가 multipart/form-data 형식으로 보냈지만,
application-json형식으로 값을 받아 와서 타입을 지원하지 않는다는 에러를 spring 측에서 보내 준것 같았다.

@RequestBody에 대해서 찾아보니
이 어노테이션은 HTTP 요청으로 넘어오는 body의 내용을 HttpMessageConverter를 통해 Java Object로 역직렬화를 해주는 역할을 한다.
그리고 multipart 요청이 아닌, 즉 어떤 바이너리 파일을 포함하고 있지 않은 데이터를 받는 역할을 한다.

    public ResponseEntity<?> insert(Car car, @RequestBody MultipartFile file)

혹시 @RequestBody를 MultipartFile에서 사용하면 어떻게 될까 궁금하여서 사용해보았는데
Request processing failed: java.lang.IllegalArgumentException: Name for argument of type [org.springframework.web.multipart.MultipartFile] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters' flag. 가 발생하였다.

파라미터 이름에 대한 정보를 찾지 못하는 문제라고 하는데,

내가 보낸 데이터 중에서 file이라는 이름의 MultipartFile의 내부 파라미터들에 대해서 이름을 찾지 못해서 나타나는 에러라고 생각이 된다.

즉, @ReqeustBody는 파일 데이터를 받아오기에는 적합하지 않은 어노테이션이다.

2. @RequestParam을 이용하는 방식. (성공)

    public ResponseEntity<?> insert(Car car, @RequestParam(name = "file") MultipartFile file)

@RequestParam을 사용하니 정상적으로 실행이 되었다.

왜 정상적으로 실행되었을까?
@RequestParam은 서블릿 요청 매개변수(즉, 쿼리 매개변수 또는 양식 데이터)를 컨트롤러의 메소드 인수에 바인딩 할 수 있습니다.
@ReqeustParam은 Type 변환은 Target Method 파라미터 Type이 String이 아닌 경우 자동으로 적용됩니다.
그러므로, 혹시나 파라미터가 들어오지 않을 가능성이 있다면 null객체가 들어오기 때문에 BadRequest가 발생하므로
파라미터가 들어올 수도, 들어오지 않을 수도 있다면 required = false를 주어야 한다.

또한 @RequestParam 어노테이션이 어노테이션에 지정된 매개 변수 이름 없이 Map<String, String> 또는 MultiValueMap<String, String>으로 선언되면 맵에 지정된 매개 변수 이름별 요청 매개 변수 값이 채워집니다.

생각나는 어노테이션을 사용하여서 막무가네로 실행이 성공하였지만, 찝찝함이 남아 Spring 사이트의 Multipart 부분에는 어떻게 파일을 받아오는 것을 추천할 지 궁금하여서 찾아보았다.

Spring사이트에서는 @RequestParam을 통해 받아오는 방식을 우선 소개한다.

3.@RequestPart를 이용하는 방식 (성공)

    public ResponseEntity<?> insert(Car car, @RequestPart("file") MultipartFile file)

Spring 사이트에서는 @RequestPart를 사용한 방식을 이렇게 설명한다.
| "meta-data" 부분에 액세스할 수 있지만 JSON(@RequestBody와 유사)에서 역직렬화하기를 원할 수 있습니다. HttpMessageConverter로 변환한 후 @RequestPart 어노테이션을 사용하여 다중 부분에 액세스합니다

만약 JSON로 값을 받아와서 @RequestParam을 사용할 수 있겟지만, @RequestBody와 유사하게 역직렬화를 하여서 값을 받아오기를 원한다면, HttpMessageConverter 로 변환한 후 @RequestPart를 사용할 수 있다.

여기서 HttpMessageConverter는 FormHttpMessageConverter를 말하는데 HttpMessageConverter를 구현하여
'정상적인' HTML 양식을 읽고 쓰는 것과 여러 부분의 데이터(예: 파일 업로드)를 쓰는 것.
즉, 이 변환기는 "응용 프로그램/x-www-form-urlencoded" 미디어 유형을 MultiValueMap<String, String>으로 읽고 쓸 수 있으며, (우리가 일반적으로 쓰는 from데이터들의 유형을 객체로 key - value(String)으로 들어오는 것을 의미함.)
MutliPartResolver를 통해 역직렬화를 하여서 MultiValueMap<String, Object>로 "multipart/form-data"와 "multipart/mixed" 미디어 유형을 쓸 수도 있습니다.
(multipartFile에 대해서 읽어올 수 있다느 것(upload)를 의미)

이러한 방식을 사용하면 @Valid 어노테이션도 쓸 수 있다고 하지만 주제에 벗어나는 것 같아 언급만 하고 가겠습니다.

4. 혹시 @ModelAttribute는 안될까??

// 단순 값 유형이 아니고 argument resolver에 의해 처리 되지 않을 경우,
// @ModelAttribute가 default로 들어간다.
public ResponseEntity<?> insert(Car car, MultipartFile file)

jakarta.servlet.ServletException: Request processing failed: java.lang.IllegalArgumentException: Name for argument of type [org.springframework.web.multipart.MultipartFile] not specified, and parameter name information not available via reflection. Ensure that the compiler uses the '-parameters'
가 발생한다

@ModelAttribute는 필드 내부와 1:1로 값이 Setter를 통해서 매핑되기 때문에, MultipartFile내부에는 Setter 메서드가 하나도 없을 뿐만 아니라,
요청이 MultipartFile내부의 변수와 같은 key의 이름으로 오는지도 모르기 때문에 에러가 발생하는 것 같다.

Car 객체에 MutilpartFile 타입의 변수를 포함시켜서 하나의 객체로 모두 받아오는 방식

환경

아래 코드는 우리가 해결해야 할 메서드 이다.

	@PostMapping("/car")
    public ResponseEntity<?> insert(________________________________) {
        try {
            int result = cs.insert(car, file);
            System.out.println(result);
            return new ResponseEntity<Integer>(result, HttpStatus.CREATED);
        } catch (Exception e) {
            return exceptionHandling(e);
        }
    }
    +
    public class Car {
      private String vin;
      private String modelName;
      private String color;
      private int mileage;
      private String img;
      private MultipartFile file;
    
    // 아래에 getter setter이 구현되어 있다.
    }
    

1. @RequestBody를 이용하는 방식

    public ResponseEntity<?> insert(@RequestBody Car car) {

위에서 설명하였듯, @RequestBody는 JSON 또는 XML 데이터를 사용하는 경우만 요청을 받아올 수 있는 방식이여서 MultipartFile의 경우 JSON 데이터를 사용할 수 없으므로 @RequestBody를 사용할 수 없으므로 불가능(415에러)하다.

2. @ReqeustParam을 이용하는 방식

    public ResponseEntity<?> insert(@RequestParam(name = "car") Car car) {

웹에서 400에러가 발생하며
서버에서는
WARN handler.AbstractHandlerExceptionResolver - Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'car' for method parameter type Car is not present]
에러가 발생한다.
메서드 매개 변수 유형에 필요한 요청 매개 변수 'car'가 없다는 뜻으로
@ReqeustParam은 하나의 HTTP 파라미터를 받을 때 사용해야 하므로
json으로 client에서 car객체를 생성해서 넣어주면 가능한데, 내가 원하는 요청값으로는 불가능하다.

3. @ReqeustPart를 이용하는 방식

    public ResponseEntity<?> insert(@RequestPart(name = "car") Car car) {

웹에서 400에러가 발생하며
서버에서는
WARN handler.AbstractHandlerExceptionResolver - Resolved [org.springframework.web.multipart.support.MissingServletRequestPartException: Required part 'car' is not present.]
내부에 MultipartFile에 대해서 확인을 하여서 multipart측에서 에러가 발생하였지만
메서드 매개 변수 유형에 필요한 요청 매개 변수 'car'가 없다는 뜻으로
json으로 client에서 car객체를 생성해서 넣어주면
@RequestBody + multipart/form-data인 경우이므로 사용 가능하며
MultipartFile이 포함되는 경우에 MutliPartResolver가 동작하여 (여기서도 전략 패턴이 사용된다) 역직렬화를 하게 됨.
MultipartFile이 포함되지 않는 경우는 @RequestBody와 같이 HttpMessageConverter가 동작하게 된다.
하지만, 내가 원하는 요청값으로는 불가능하다.

4. @ModelAttribute를 이용하는 방식 (성공)

public ResponseEntity<?> insert(Car car) {

드디어 성공하였다!

@ModelAttribute는 필드 내부와 1:1로 값이 Setter를 통해서 매핑되기 때문에,
내가 원하는 요청 값은 필드 각각의 값을 1대1로 매핑해서 보내주므로 값이 전부 매핑될 수 있으므로 가능하다.!


파일 입력 방식에 대해서 이해가 잘 되지 않고, 정리되어 있는 사이트도 찾기 어려워
정리를 하면서 내가 만났던 수 많은 에러들이 더 있지만, 그 수 많은 타입과 에러들을 정리하기엔 현재 분량의 5배가 될 것이므로... 생략해보겠습니다.

이상 RESTAPI로 파일 받아오기 사건 파일을 마치겠습니다.

참고

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods
https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.html#page-title

https://green-bin.tistory.com/44
https://dangdangee.tistory.com/entry/Spring-RequestParam-%EC%82%AC%EC%9A%A9%EB%B2%95
https://middleearth.tistory.com/35
https://stackoverflow.com/questions/48051177/content-type-multipart-form-databoundary-charset-utf-8-not-supported
https://cheershennah.tistory.com/179

profile
노력하는 개발자

0개의 댓글