우리 프로젝트는 유저의 취향을 공유하는 애플리케이션을 구현하는 것으로, 음식에 대한 취향을 공유할 때 사진을 업로드할 수 있어야 한다.
백엔드 팀원들끼리 회의를 하면서 이 기능에 대한 구현을 내가 맡기로 했다.
왜 S3 버킷을 사용했나요?
기능 구현을 맡고 어떻게 구현해야 하는지에 대해 감이 잘 잡히지 않았다.
그래서 멘토님에게 질문을 하여 다음과 같은 답변을 받았다.
1. 이미지를 base64로 인코딩하여 DB에 저장
2. 이미지 자체를 EC2 디렉토리에 저장 후 경로를 DB에 저장
3. 이미지 자체를 S3에 저장 후 경로를 DB에 저장
여기서 중요한 점은 업로드 방법보다는 이미지를 리사이징하여 용량을 줄이는 것이라 하셨다.
개인적으로 이미지를 인코딩하는 방법은 비효율적(용량측면에서)이라 생각해서 세 번째 방법을 선택했다.
사실 2, 3번 방법은 큰 차이가 없다고 생각되어졌는데 서치를 했을 때 s3 버킷을 사용하는 것에 대한 레퍼런스가 더 많아 빠르게 구현할 수 있을 것 같았다. (프로젝트의 종료가 얼마 남지 않았다...!)
구현기
S3 config
@Configuration
public class FileConfig {
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder
.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
}
Controller 작성
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
@PostMapping(value = "/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity saveImage(@RequestParam(value = "image") MultipartFile image) throws IOException {
String imageUrl = fileService.upload(image, "images");
FileDto.Response response = new FileDto.Response(imageUrl);
return new ResponseEntity(response, HttpStatus.CREATED);
}
@PatchMapping(value = "/images/upd", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity updateImage(@RequestParam(value = "fileKey") String fileKey,
@RequestParam(value = "image") MultipartFile image) throws IOException {
// 삭제할 파일이름을 전달받아 삭제
fileService.remove(fileKey);
// 새 이미지 업로드
String imageUrl = fileService.upload(image, "images");
FileDto.Response response = new FileDto.Response(imageUrl);
return new ResponseEntity(response, HttpStatus.OK);
}
@PostMapping(value = "/images/del", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity deleteImage(@RequestParam(value = "fileKey") String fileKey) {
fileService.remove(fileKey);
return ResponseEntity.noContent().build();
}
}
이미지 DTO
public class FileDto {
public static class Response {
private String fileKey;
public Response(String fileKey) {
this.fileKey = fileKey;
}
public String getFileKey() {
return fileKey;
}
}
}
Service 작성
public class FileService {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
String fileName = dirName + "/" + multipartFile.getOriginalFilename();
String uploadImageUrl = putS3(multipartFile, fileName);
return fileName;
}
private String putS3(MultipartFile multipartFile, String fileName) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(multipartFile.getContentType());
metadata.setContentLength(multipartFile.getSize());
try {
amazonS3Client.putObject(
new PutObjectRequest(bucket, fileName, multipartFile.getInputStream(), metadata)
.withCannedAcl(CannedAccessControlList.PublicRead)
);
} catch (IOException e) {
e.printStackTrace();
}
return amazonS3Client.getUrl(bucket, fileName).toString();
}
private void removeNewFile(File targetFile) {
if (targetFile.delete())
log.info("Success Remove File");
else
log.info("Fail Remove File");
}
private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(file.getOriginalFilename());
if (convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
public void remove(String fileName) {
if (!amazonS3Client.doesObjectExist(bucket, fileName))
throw new AmazonS3Exception(fileName + " does not exist");
amazonS3Client.deleteObject(bucket, fileName);
}
}
S3 버킷에 이미지 파일을 업로드하는 기능 구현이 끝났다.
게시물 CRUD 코드에 파일 경로에 대한 필드를 추가해주고 프론트에 응답으로 내려주면 이제 유저가 이미지를 업로드하고 업로드된 이미지를 요청할 수 있다.
트러블슈팅
application.yml
cloud:
aws:
credentials:
accessKey: ${AWS_ACCESS_KEY}
secretKey: ${AWS_SECRET_KEY}
s3:
bucket: ${BUCKET_NAME}
region:
static: ap-northeast-2
stack:
auto: false
S3 버킷을 사용하기 위해 AWS IAM 액세스 키를 application.yml에 추가했는데, 액세스 키는 공개되면 안되기 때문에 Github에 절대 public으로 업로드 되면 안된다.
편의를 위해서 개발/배포 yml을 따로 작성하여 팀원들에게 공유했는데 한 팀원이 실수로 개발 yml을 Github에 올려서 AWS로부터 메일 폭탄을 받고 정상적인 사용을 위해 소명하는데 꽤 고생했다. 하하😂
각설하고 액세스 키 처리를 위해 ubuntu 서버에 환경변수를 설정하여 문제를 해결하려고 했는데 생각보다 그 과정이 매우 험난하여 따로 블로깅을 했다.
[code deploy] 환경변수를 불러오지 못하는 문제 해결
🟥 Error AWS IAM 액세스 키를 서버에서 불러오기 위해 환경변수를 설정했는데 Code Deploy에서 변수를 불러오지 못하는 문제가 발생했다. 금방 해결될거라 생각했는데 예상과는 다르게 이번 프로젝
eunaya.tistory.com
이미지 업로드에 대한 기능은 구현했으나 멘토님께서 조언해주셨던 리사이징에 대한 문제는 해결하지 못했다.
이 부분에 대한 고민을 더 해볼 예정이다.
참고
image 전송과 함께 데이터는 json으로 보내고 싶은 경우 - 인프런 | 질문 & 답변
restAPI에서 image 와 함께 데이터를 같이 보낼때, 보내려는 데이터가 많이 복잡할 경우에는 json으로 보내는 것이 좋을 것 같아 json으로 보내는 방법을 시도해봤는데요 @RequestPart DTO dto 이런식으로
www.inflearn.com
[SpringBoot] AWS S3로 이미지 업로드하기
프로젝트 시작하기 전에는 스프링부트 1도 몰랐고 어떻게 돌아가는지도 몰랐는데 이번에 이미지 s3로 업로드하는 api만들면서 조금 감이 온것 같다. 진짜 에러의 에러 연속을 만나면서 나 스프링
velog.io
https://kim-jong-hyun.tistory.com/78
[Spring] - Spring Boot + AWS S3를 이용하여 이미지 조회/등록/삭제 및 accessKey, secretKey, butket 값을 외부에
이번장에는 Spring Boot와 AWS SDK를 이용하여 AWS S3에 이미지/파일 정적리소스들을 등록, 조회, 삭제하는 내용에 대해 정리해보고자한다. 그전에 AWS S3에 버킷생성 / IAM 사용자를 생성후 S3 액세스 권
kim-jong-hyun.tistory.com