티스토리 뷰
[Spring] 이미지를 AWS S3로 업로드하는 2가지 방법 (MultipartFile, PresignedUrl)
heemang.dev 2024. 5. 2. 07:36
클라이언트가 이미지를 업로드할 때, 서버에서 처리할 수 있는 방법은 2가지가 존재합니다.
- 서버에서 MultipartFile 형태로 데이터를 받아서 AWS S3로 업로드한다.
- 서버에서 presignedUrl을 발급하여 AWS S3로 업로드한다.
2번에서 presignedUrl이 다소 생소할지라도 이번 글을 통해 이해할 수 있습니다. (AWS S3 생성 및 스프링과 연결 방법에 대해서는 생략합니다)
1. MultipartFile 형태로 서버에서 처리
스프링에서는 MultipartFile 인터페이스를 제공합니다. 업로드한 파일의 이름, 크기 등을 제공하는 메서드가 존재합니다.
Controller
form-data로 넘어오는 이미지를 MultipartFile로 받습니다.
@RestController
@RequestMapping("/upload")
@RequiredArgsConstructor
@Slf4j
public class UploadController {
private final FileUploadService fileUploadService;
/**
* 클라이언트 -> 서버 -> S3 업로드
*/
@PostMapping
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile multipartFile) {
String fileUrl = fileUploadService.uploadFile(multipartFile);
return ResponseEntity.ok(fileUrl);
}
}
Service
AWS S3에 이미지를 업로드합니다. 여러 사용자가 동일한 파일명을 업로드할 수 있기 때문에 UUID를 사용하여 중복되지 않도록 처리하였습니다.
@Service
@RequiredArgsConstructor
@Slf4j
public class FileUploadService {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
/**
* MultipartFile을 AWS S3에 업로드하고 업로드된 파일의 URL을 반환한다.
*
*/
public String uploadFile(MultipartFile multipartFile) {
try {
String originalFilename = multipartFile.getOriginalFilename(); // 실제 파일명
String uploadFileName = UUID.randomUUID().toString() + "_" + originalFilename; // S3에 저장될 파일명
ObjectMetadata metadata = new ObjectMetadata(); // 파일 메타데이터 생성
metadata.setContentLength(multipartFile.getSize()); // 파일 크기
metadata.setContentType(multipartFile.getContentType()); // 파일 타입
// 파일 업로드
amazonS3Client.putObject(bucket, uploadFileName, multipartFile.getInputStream(),
metadata);
return amazonS3Client.getResourceUrl(bucket, uploadFileName).toString(); // 업로드된 파일 URL
} catch (IOException ex) {
throw new RuntimeException("Error uploading file");
}
}
}
API 호출
클라이언트 -> 서버 -> AWS S3 순서대로 이미지가 업로드됩니다.
2. PresignedUrl 발급하여 이미지 업로드
1번의 경우 반드시 서버를 거쳐서 이미지를 업로드해야 합니다. 1번이 갖는 단점은 아래와 같습니다.
- 이미지가 서버를 거치기 때문에 네트워크 부하와 트래픽이 증가한다.
- PresignedUrl 방법보다 업로드 속도가 증가한다.
- 사용자 수가 급증하였을 경우 서비스의 안정성을 보장할 수 없다.
따라서 2번 방법으로 이미지를 업로드하는 것이 좋습니다.
설명에 앞서 presignedUrl에 대해서 알아보겠습니다. PresignedUrl이란 S3 버킷에 파일을 업로드하거나 다운로드할 수 있도록 하는 URL입니다. 물론 임시 URL이기 때문에 시간이 유효합니다. 과정은 다음과 같습니다.
- AWS SDK를 사용하여 S3 객체에 대한 presignedUrl을 발급한다.
- presignedUrl에는 AWS 액세스 키, 객체의 이름, 버킷, 만료시간 등이 포함됩니다.
- 발급받은 presignedUrl은 만료 시간이 존재하며, 시간 내에 S3 버킷으로부터 이미지 다운로드 및 업로드가 가능합니다.
위 설명을 토대로 스프링에 적용해보겠습니다.
Controller
DTO 정보
@Getter
@Setter
@Builder
public class GetPresignedUrlRequestDto {
private PresignedUrlStatus httpMethod; // GET, PUT
private String fileName;
}
@RestController
@RequestMapping("/upload")
@RequiredArgsConstructor
@Slf4j
public class UploadController {
private final S3Service s3Service;
/**
* GET : 화면에 사용자의 프로필 이미지를 보여주기 위한 PresignedUrl을 발급한다.
* PUT : 클라이언트가 요청한 이미지의 명으로 된 PresignedUrl을 받아 이미지를 업로드한다.
*/
@GetMapping("/presigned-url")
public ResponseEntity<String> generatePresignedUrl(@ModelAttribute GetPresignedUrlRequestDto request) {
log.info("filename : {}", request.getFileName());
String presignedUrl = s3Service.getPresignedUrl(request);
return ResponseEntity.ok(presignedUrl);
}
}
S3Service
HttpMethod가 GET, PUT에 따라 처리를 다르게 해야합니다.
- GET : AWS S3에 업로드되어 있는 이미지를 다운로드하기 위한 URL
- PUT : AWS S3로 이미지를 업로드하기 위한 URL
@Service
@RequiredArgsConstructor
public class S3Service {
private final FileUploadService fileUploadService;
/**
* GET : 화면에 사용자의 프로필 이미지를 보여주기 위한 PresignedUrl을 발급한다.
* PUT : S3에 이미지를 업로드하기 위한 PresignedUrl을 발급한다.
*/
public String getPresignedUrl(GetPresignedUrlRequestDto request) {
PresignedUrlStatus httpMethod = request.getHttpMethod();
if (httpMethod == GET) {
return fileUploadService.generatePresignedUrlForShowProfileImage(request.getFileName());
} else {
return fileUploadService.generatePresignedUrlForUpload(request.getFileName());
}
}
}
FileUploadService
마찬가지로 S3 버킷에 파일명이 중복될 수 있으므로 UUID를 사용하여 중복을 방지합니다. GeneratePresignedUrlRequest 생성할 때 GET 또는 PUT을 사용할 수 있습니다. 생성된 presignedUrl을 반환합니다.
@Service
@RequiredArgsConstructor
@Slf4j
public class FileUploadService {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
/**
* AWS S3에 파일을 업로드하기 위해 유효시간이 30분인 PresignedUrl 생성
*/
public String generatePresignedUrlForUpload(String fileName) {
// 파일명을 UUID로 변경하여 중복을 방지한다.
String uniqueFileName = UUID.randomUUID().toString() + "_" + fileName;
Date expiration = new Date();
long expireTimeMillis = expiration.getTime() + (1000 * 60 * 30); // 유효시간 30분
expiration.setTime(expireTimeMillis);
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, uniqueFileName)
.withMethod(com.amazonaws.HttpMethod.PUT)
.withExpiration(expiration);
log.info("PUT 요청");
return amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest).toString();
}
/**
* AWS S3에 저장된 사진을 가져오기 위해 유효시간이 30분인 PresignedUrl 생성
*/
public String generatePresignedUrlForShowProfileImage(String fileName) {
Date expiration = new Date();
long expireTimeMillis = expiration.getTime() + (1000 * 60 * 30); // 유효시간 30분
expiration.setTime(expireTimeMillis);
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(com.amazonaws.HttpMethod.GET)
.withExpiration(expiration);
log.info("GET 요청");
return amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest).toString();
}
}
API 호출
API를 호출하여 이미지 업로드를 위한 presignedUrl를 발급받습니다. 발급된 presignedUrl로 PUT 요청을 하여 이미지를 업로드합니다. 이때 PUT 요청은 서버에서 제공하는 API가 아닌 S3 버킷으로 이미지를 바로 업로드를 할 수 있도록 AWS에서 제공하는 API입니다.