[스프링] form-data 형태로 이미지 + json API 구현 과정

sonnng·2024년 1월 18일
0

Spring

목록 보기
37/41
post-thumbnail

토론 주제 수정 API를 구현한다.

토론 주제 수정 시 토론 수정 dto와 이미지를 MultipartFile을 통해 받아올 때 발생한 오류를 정리한다.

  • CommunitySubjectImageEditDTO
    @Getter
    @AllArgsConstructor
    @ApiModel(value = "CommunitySubjectImageEditDTO(토론주제 수정 정보)", description = "토론주제 id, 토론제목, 토론내용을 가진 Domain Class")
    public class CommunitySubjectImageEditDTO {
    
       @NotNull
       @ApiModelProperty(value = "토론 주제 id")
       private Long subjectId;
       
    	 @NotBlank(message = "토론 주제 제목이 비워져 있어서는 안됩니다.")
       @ApiModelProperty(value = "토론 주제")
       private String subjectName;
       
    	 @NotBlank(message = "토론 주제 내용이 비워져 있어서는 안됩니다.")
       @ApiModelProperty(value = "토론 내용")
       private String content;
       
    	 @ApiModelPropert(value = "이미지", required = false)
       private MultipartFile image;
    }
  • CommunitySubjectImageEditDTO
    @Getter
    @AllArgsConstructor
    @ApiModel(value = "CommunitySubjectEditDTO(토론주제 수정 정보)", description = "토론주제 id, 토론제목, 토론내용을 가진 Domain Class")
    public class CommunitySubjectEditDTO {
    
       @NotNull
       @ApiModelProperty(value = "토론 주제 id")
       private Long subjectId;
       
    	 @NotBlank(message = "토론 주제 제목이 비워져 있어서는 안됩니다.")
       @ApiModelProperty(value = "토론 주제")
       private String subjectName;
       
    	 @NotBlank(message = "토론 주제 내용이 비워져 있어서는 안됩니다.")
       @ApiModelProperty(value = "토론 내용")
       private String content;
    
    }

시도 1: @ModelAttribute

@PutMapping(value = "/subject", consumes = {"multipart/form-data"})
public BaseResponse<String> modifySubject(
   @Valid @ModelAttribute CommunitySubjectImageEditDTO c) {
   communityService.modifySubject(c);
   return BaseResponse.success("성공적으로 토론주제를 수정하였습니다.");
}

이 시도는 image값을 dto에 필수적으로 넣어주어야하기 때문에 올리고자 하는 이미지가 없더라도 프론트엔드에서는 매번 빈값으로 보내주어야 한다는 문제점이 있었다.

그리고 postman에서는 동작했지만 swagger에서는 image 파일 조차 String으로 입력받도록 설정되는 문제점이 있었다. 당시에 consumes = MediaType.MULTIPART_FORM_DATA_VALUE 가 아니라 consumes = {”multipart/form-data”} 와 같이 직접 문자열로 입력해주는 경우에도 swagger에서 file 입력이 받아지는 참고사이트가 있었기 때문에 시도해 보았으나 변화는 없었다.

이 두 가지 문제점을 해결하기 위해서 @RequestPart를 이용해 DTO와 image 파일을 각각 받도록 하며 필수여부에 따라 이미지 파일을 선택적으로 보낼 수 있도록 했다.

시도 2: @RequestPart + @RequestPart

@PutMapping(value = "/subject", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public BaseResponse<String> modifySubject(
    @Valid @RequestPart(value = "dto") CommunitySubjectEditDTO c,
    @RequestPart(value = "file", required = false) MultipartFile image) {
    communityService.modifySubject(c, image);
    return BaseResponse.success("성공적으로 토론주제를 수정하였습니다.");
}

@RequestPart 어노테이션은 MultipartFile의 경우 MultiPartResolver를 통해 역직렬화가 가능하고, 없는 경우는 @RequestBody와 마찬가지로 HttpMessageConverter를 통해 JSON 타입도 역직렬화가 가능하다고 한다.

하지만 postman으로 테스트할 때는 제대로 동작하지만, swagger에서는 아래의 오류와 함께 테스트가 되지 않는다.

WARN 14252 --- [nio-8080-exec-6] .w.s.m.s.DefaultHandlerExceptionResolver :
 Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/octet-stream' not supported]

이는 swagger에서는 파라미터 별로 content-type을 지정할 수 없어서 발생한 문제이다.

이를 해결하기 위해 MultipartJackson2HttpMessageConverter를 생성한다.

  • MultipartJackson2HttpMessageConverter
    @Component
    public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    
        /**
         * "Content-Type: multipart/form-data" 헤더를 지원하는 HTTP 요청 변환기
         */
        public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
            super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
        }
    
        @Override
        public boolean canWrite(Class<?> clazz, MediaType mediaType) {
            return false;
        }
    
        @Override
        public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
            return false;
        }
    
        @Override
        protected boolean canWrite(MediaType mediaType) {
            return false;
        }
    }

하지만, 프론트엔드 입장을 고려해보지 못했다.

내가 구현한 방식은

토론 글을 수정할 때, 기존 이미지 파일 그대로 게시하고 싶은 경우 큰 문제가 된다는 점이다. Swagger 테스트에서는 파일을 첨부하는 방식이라, 심각성을 못느꼈지만 프론트엔드 입장에서는 토론글 조회에 나타나는 s3 url만을 가지고 있기 때문에, 서버에 JSON 객체를 보내줄 때, S3 URL을 파일 객체로 바꿔야한다는 문제가 있었다.

하지만 url을 파일 객체로 바꾸는데는 한계가 있었고 CORS에러까지 발생했다.

표에 제시된 대로 기존 이미지 파일을 포함해 수정할 때, 이미지 없이 수정할 때, 새 이미지 파일로 수정할 때로 크게 3가지 경우로 나누어서 구현했다.

기존 이미지 파일 그대로 수정한다면 url만 넘겨주면 되도록 했다.

  • 변경된 코드
    public boolean isExist(String fileRoute) {
            boolean isObjectExist = false;
            int index = fileRoute.indexOf(url);
            String fileName = fileRoute.substring(index + url.length() + 1);
            try {
                isObjectExist = amazonS3Client.doesObjectExist(bucket, fileName);
            } catch (Exception e) {
                throw new InternalServerErrorException(ErrorCode.FILE_NOT_FOUND);
            }
            return isObjectExist;
     }
    
    String communityImgUrl = null;
    String imgPath = c.getImageUrl();
    
    if (!"".equals(imgPath) && imgPath != null) {
        if (awss3Service.isExist(imgPath)) {
            communityImgUrl = imgPath;
        }
    } else {
        if (image != null && !image.isEmpty()) {
            deleteCommunityS3Image(community);
            communityImgUrl = awss3Service.upload(image, AWS_S3_DISCUSSION_DIR_NAME);
        } else {
            deleteCommunityS3Image(community);
        }
    }
    
    public void deleteCommunityS3Image(Community community) {
          if (community.getImageUrl() != null) {
              awss3Service.delete(community.getImageUrl());
          }
      }

AWSS3Service 클래스에 포함된 isExist로 s3에 이미지 url이 존재하는지 여부를 확인하는 메서드다. 이 메서드로 실제로 존재한다면 제거해주고 아니라면 제거하지 않도록 분기처리했고, 이미지 파일객체가 포함되었다면 새 이미지 url로 변경해주도록 했다.

🔍 @RequestParam vs @RequestPart

@RequestParam

  • name-value 쌍의 form 필드와 함께 사용된다.

@RequestPart

  • JSON, XML등을 포함하는 복잡한 내용의 Part와 함께 사용된다.

📌 정리

1. DTO와 MultipartFile을 같이 사용하는 경우

@RequestPart + @RequestPart를 사용하고 MultipartJackson2HttpMessageConverter를 생성한다.

  • CommunitySubjectEditDTO
    
    @Getter
    @NoArgsConstructor
    public class CommunitySubjectEditDTO {
    
        @NotNull
        private Long subjectId;
        @NotBlank(message = "토론 주제 제목이 비워져 있어서는 안됩니다.")
        private String subjectName;
        @NotBlank(message = "토론 주제 내용이 비워져 있어서는 안됩니다.")
        private String content;
    
        @Builder
        public CommunitySubjectEditDTO(Long subjectId, String subjectName, String content) {
            this.content = content;
            this.subjectName = subjectName;
            this.subjectId = subjectId;
        }
    
    }
  • controller
    @PutMapping(value = "/subject", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public BaseResponse<String> modifySubject(
        @Valid @RequestPart(value = "dto") CommunitySubjectEditDTO c,
        @RequestPart(value = "file", required = false) MultipartFile image) {
        communityService.modifySubject(c, image);
        return BaseResponse.success("성공적으로 토론주제를 수정하였습니다.");
    }
  • MultipartJackson2HttpMessageConverter
    
    @Component
    public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    
        /**
         * "Content-Type: multipart/form-data" 헤더를 지원하는 HTTP 요청 변환기
         */
        public MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
            super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
        }
    
        @Override
        public boolean canWrite(Class<?> clazz, MediaType mediaType) {
            return false;
        }
    
        @Override
        public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
            return false;
        }
    
        @Override
        protected boolean canWrite(MediaType mediaType) {
            return false;
        }
    }


파일 + json을 함께 스프링에서 함께 구현하는 방법 개요

파일 + json을 이용해 form-data로 post 요청을 했음에도 성공하지 않는 경우
-> default value must not be null 과 같은 DTO @Valid가 작동했던 문제 해결

@RequestPart(required=false)를 사용
-> file에 값이 없어도 api 요청이 가능하도록 해결

@RequestPart 로 API 구현하는 경우 consumes 작성방법

@RequestPart를 이용해 api 구현하는 경우 form-data에서 json 세팅

0개의 댓글