행운복권 프로젝트를 진행하면서 유저의 프로필 이미지와 앱 로고를 저장해서 api 통신 시 클라이언트로 이미지를 제공하고자 한다.
AWS S3 버켓 생성 및 환경설정(참고)
AWS S3 버킷 생성 및 환경설정이 됐다는 가정하에 이야기를 진행하고자 한다.

yml 파일에 S3 관련 설정들을 환경 변수로 세팅하여 외부 노출을 막았다. 노출되는 순간 바로 이메일이 날아온다... 조심해야 한다
implementation 'com.amazonaws:aws-java-sdk-s3control:1.12.364'
먼저 dependency를 추가 해야한다.
S3Config
@Configuration
public class S3Config {
@Value("${aws.access-key}")
private String accessKey;
@Value("${aws.secret-key}")
private String secretKey;
@Bean
public AmazonS3 getS3ClientBean() {
AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
return AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(Regions.AP_NORTHEAST_2)
.build();
}
}
S3Config 설정을 해준다. S3Client 사용하기 위해 Bean으로 등록해주었다.
ErrorCode
@Getter
@AllArgsConstructor
public enum ErrorCode {
BAD_FILE_EXTENSION(404, "FILE extension error"),
FILE_EMPTY(404, "FILE empty"),
FILE_UPLOAD_FAIL(404, "FILE upload fail"),
FILE_OVER_SIZE(404, "FILE 크기가 10mb를 초과 하였습니다"),
// .........
private int status;
private String reason;
}
예외 처리를 위해 메시지를 따로 보관하였다.
ImageController
@Tag(name = "업로드", description = "업로드 관련 API")
@RequiredArgsConstructor
@RequestMapping("/api/v1/images")
@RestController
@Slf4j
public class ImageController {
private final ImageService imageService;
@Operation(summary = "사진 업로드")
@PostMapping("/upload")
public UploadImageResponse uploadImage(
@Parameter(name = "file",
description = "multipart/form-data 형식의 이미지를 input으로 받습니다.",
required = true)
@RequestPart MultipartFile file) {
log.info("file = {}",file);
return imageService.uploadImage(file);
}
}
@RequestPart를 사용하여 사진 파일을 multipart/form-data 형식으로 받도록 구현했다.
ImageService
@RequiredArgsConstructor
@Service
@Slf4j
public class ImageService implements ImageUtils{
@Value("${aws.s3.bucket}")
private String bucket;
@Value("${aws.s3.base-url}")
private String baseUrl;
private final AmazonS3 amazonS3;
public UploadImageResponse uploadImage(MultipartFile file) {
String url = upload(file);
return new UploadImageResponse(url);
}
public String upload(MultipartFile file) {
if (file.isEmpty() && file.getOriginalFilename() != null){
throw FileEmptyException.EXCEPTION;
}
if (file.getSize() / (1024 * 1024) > 10) {
throw FileOversizeException.EXCEPTION;
}
String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
if (!(ext.equals("jpg")
|| ext.equals("HEIC")
|| ext.equals("jpeg")
|| ext.equals("png")
|| ext.equals("heic"))) {
throw BadFileExtensionException.EXCEPTION;
}
String randomName = UUID.randomUUID().toString();
String fileName = SecurityUtils.getCurrentUserId() + "|" + randomName + "." + ext;
try {
ObjectMetadata objMeta = new ObjectMetadata();
byte[] bytes = IOUtils.toByteArray(file.getInputStream());
objMeta.setContentType(file.getContentType());
objMeta.setContentLength(bytes.length);
amazonS3.putObject(
new PutObjectRequest(bucket, fileName, file.getInputStream(), objMeta)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw FileUploadFailException.EXCEPTION;
}
return baseUrl + "/" + fileName;
}
@Override
public void delete(String profilePath) {
String objectName = getBucketKey(profilePath);
amazonS3.deleteObject(bucket, objectName);
}
public String getBucketKey(String profilePath){
return profilePath.substring(profilePath.lastIndexOf('/') + 1);
}
}
이미지를 업로드하는데 핵심 로직이다. 조금 더 자세히 살펴보도록 하겠다.
if (file.isEmpty() && file.getOriginalFilename() != null) {
throw FileEmptyException.EXCEPTION;
}
if (file.getSize() / (1024 * 1024) > 10) {
throw FileOversizeException.EXCEPTION;
}
파일이 비어있거나, 파일 최대 크기에 대해서 예외 처리를 진행했다.
String originalFilename = file.getOriginalFilename();
String ext = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
if (!(ext.equals("jpg")
|| ext.equals("HEIC")
|| ext.equals("jpeg")
|| ext.equals("png")
|| ext.equals("heic"))) {
throw BadFileExtensionException.EXCEPTION;
}
String randomName = UUID.randomUUID().toString();
String fileName = SecurityUtils.getCurrentUserId() + "|" + randomName + "." + ext;
받아온 파일에 확장자를 확인하여 예외 처리를 진행했다. 또한 S3에 사진 파일을 올릴 때 어떤 유저가 이미지를 올렸는지는 파악하고자 uuid 랜덤 값과 SecurityContext에서 유저의 id 값을 가져와 파일을 업로드할 수 있도록 구현했다.
try {
ObjectMetadata objMeta = new ObjectMetadata();
byte[] bytes = IOUtils.toByteArray(file.getInputStream());
objMeta.setContentType(file.getContentType());
objMeta.setContentLength(bytes.length);
amazonS3.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), objMeta)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw FileUploadFailException.EXCEPTION;
}
return baseUrl + "/" + fileName;
ObjectMetadata 객체를 생성하여 파일의 콘텐츠 타입 및 길이를 설정한다.
amazonS3.putObject 메서드를 통해서 Amazon S3에 파일을 업로드한다.

이제 포스트 맨으로 api를 테스트 해보자

성공적으로 통신을 완료했고 이미지 url을 응답 스펙에 맞추어 잘 보내주는 것을 확인했다.

AWS S3에 이미지가 잘 업로드됐다. 우리가 원하는 대로 uuid와 유저의 id 값을 조합하여 url을 구성한 것을 확인했다.

이미지가 잘 저장된 것을 확인할 수 있었다.
프로젝트 링크를 통해서 참고하시면 좋을 것 같습니다! 감사합니다. 도움이 되셨으면 좋겠습니다.👋🏼
행운 복권 깃허브 링크
https://github.com/Uttug-Seuja/luck-lottery-server
개발할때 가장 멋있는 남자..