Waggle 프로젝트에서 이미지 업로드가 필요한 상황으로는 회원 프로필 이미지
가 있습니다.
때문에 AWS S3를 구성하고 처음으로 Rest API 환경에서 이미지를 요청받고 처리하는 과정을 구현할 수 있었습니다.
이미지 업로드 기능을 구현한 과정을 정리해봤습니다.
1) S3 버킷 생성
이미지가 들어갈 수 있는 공간을 생성하기 위해 S3 서비스에서 버킷을 생성했습니다.
해당 버킷의 파일을 Get하는 권한을 부여하기 위해 권한->버킷정책
에서 아래와 같이 권한을 설정했습니다.
{
"Version": "2012-10-17",
"Id": "Policy1738159925394",
"Statement": [
{
"Sid": "Stmt1738159922905",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::waggle-bucket/*"
}
]
}
2) 이미지 파일 받아오기
2-1. Multipart 적용하기
사용자 정보를 수정할 때 HTTP Request 정보에 수정할 사용자 정보들이 들어있을 것이고, 그 중 사용자 프로필 이미지도 포함되어 있습니다. 해당 이미지 파일을 HTTP Request로 받아오기 위해 Multipart를 사용했습니다.
@RequestPart를 활용하여 이미지 파일은 이미지 파일대로, 나머지 텍스트 값은 DTO를 통해 받아오도록 구성했습니다.
// 사용자 정보 수정 컨트롤러
public ResponseEntity<BaseResponse<Object>> updateUser(
@RequestPart(value = "profileImage", required = false) MultipartFile profileImage,
@RequestPart(value = "updateUserDto") UpdateUserDto updateUserDto
) {
User updatedUser = userService.updateUser(profileImage, updateUserDto);
return SuccessResponse.of(ApiStatus._OK, updatedUser);
}
결과적으로 아래와 같이 Swagger에서 파일을 첨부하여 요청할 수 있게 되었습니다.
2-2. Swagger 호출 오류 해결
그러나 오류가 발생했고 디버깅 결과 profileImage에 null이 들어오는 것이 확인되었습니다.
Swagger 요청 중 HttpMediaTypeNotSupportedException: Content-Type 'application/octet-stream' is not supported 오류가 발생한 이유
Swagger UI에서 multipart/form-data를 사용하여 JSON 데이터를 전송할 때 Content-Type이 application/octet-stream으로 설정된다는 이슈를 확인하였습니다.
해결 방안
이 문제점을 해결하기 위해 WebMvcConfigurer에서 application/octet-stream도 처리를 할 수 있도록 설정했습니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
private OctetStreamReadMsgConverter octetStreamReadMsgConverter;
@Autowired
public WebConfig(OctetStreamReadMsgConverter octetStreamReadMsgConverter) {
this.octetStreamReadMsgConverter = octetStreamReadMsgConverter;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(octetStreamReadMsgConverter);
}
}
결과적으로 Swagger에서도 Multipart를 인자로 받는 API를 사용할 수 있게 되었습니다.
3) S3 기능 구현하기
이미지 파일을 받아오는 데에 성공했으면 S3 버킷에 추가하거나, 삭제하는 기능 등이 필요합니다. 때문에 S3Service라는 클래스를 따로 만들어 필요할 때에 호출할 수 있도록 했습니다.
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3 amazonS3;
@Value("${AWS_BUCKET}")
private String bucket;
public String uploadFile(MultipartFile file, String fileName) {
validateFile(file);
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
amazonS3.putObject(bucket, fileName, file.getInputStream(), metadata);
return amazonS3.getUrl(bucket, fileName).toString();
} catch (IOException | AmazonS3Exception e) {
throw new S3Exception(ApiStatus._S3_UPLOAD_FAILED, e);
}
}
public void deleteFile(String fileUrl) {
try {
String fileName = extractfilenamefromurl(fileUrl);
amazonS3.deleteObject(bucket, fileName);
} catch (Exception e) {
throw new S3Exception(ApiStatus._S3_DELETE_FAILED, e);
}
}
...
}