토론 주제 수정 API를 구현한다.
토론 주제 수정 시 토론 수정 dto와 이미지를 MultipartFile
을 통해 받아올 때 발생한 오류를 정리한다.
@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;
}
@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;
}
@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 파일을 각각 받도록 하며 필수여부에 따라 이미지 파일을 선택적으로 보낼 수 있도록 했다.
@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
를 생성한다.
@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로 변경해주도록 했다.
@RequestPart + @RequestPart를 사용하고 MultipartJackson2HttpMessageConverter를 생성한다.
@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;
}
}
@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("성공적으로 토론주제를 수정하였습니다.");
}
@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 요청이 가능하도록 해결