MultipartFile 방식
1. S3Config
S3 클라이언트를 설정하기 위한 클래스입니다. Amazon S3에 연결하려면 인증 정보와 지역 설정이 필요합니다.
accessKey, secretKey, region 정보를 담고 있는 AmazonS3Client를 Bean으로 등록합니다.
@Configuration
public class S3Config {
@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 AmazonS3Client amazonS3Client(){
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
필드: application.yml 또는 application.properties 파일에서 AWS Access Key, AWS Secret Key, S3 버킷이 위치한 AWS region 가져옵니다.
amazonS3Client() 메서드: AWS SDK를 사용하여 S3 클라이언트를 생성합니다.
- BasicAWSCredentials: AWS 자격 증명을 생성.
- AmazonS3ClientBuilder.standard(): S3 클라이언트를 설정.
- withRegion(region): S3 버킷의 리전을 지정.
- withCredentials(new AWSStaticCredentialsProvider(awsCredentials)): AWS 인증 정보를 사용.
- 생성된 AmazonS3Client를 Bean으로 등록하여 의존성 주입을 사용할 수 있게 합니다.
2. S3Service
이 클래스는 S3에 파일을 업로드하고 관련 작업을 수행하는 서비스입니다.
@Slf4j
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("Convert_Fail"));
return upload(uploadFile, dirName);
}
private String upload(File uploadFile, String dirName){
String fileName = dirName + "/" + uploadFile.getName();
String uploadImageUrl = putS3(uploadFile, fileName);
removeNewFile(uploadFile);
return uploadImageUrl;
}
private String putS3(File uploadFile, String fileName){
amazonS3Client.putObject(
new PutObjectRequest(bucket, fileName, uploadFile)
);
return amazonS3Client.getUrl(bucket, fileName).toString();
}
private void removeNewFile(File targetFile){
String name = targetFile.getName();
if (targetFile.delete()){
log.info(name + "파일 삭제 완료");
} else {
log.info(name + "파일 삭제 실패");
}
}
public Optional<File> convert(MultipartFile multipartFile) throws IOException{
File convertFile = new File(multipartFile.getOriginalFilename());
if (convertFile.createNewFile()){
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(multipartFile.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
}
필드
- private final AmazonS3Client amazonS3Client: S3와 통신하기 위한 클라이언트입니다. S3Config에서 설정한 Bean이 주입됩니다.
- @Value("${cloud.aws.s3.bucket}") private String bucket;: S3 버킷 이름을 가져옵니다.
주요 메서드 설명
1. upload(MultipartFile multipartFile, String dirName)
- 설명: MultipartFile 형식의 파일을 S3에 업로드합니다. 아래 private 메서드로 과정-역할이 세분화됩니다.
- 동작:
- convert(): MultipartFile을 로컬 디스크에 저장하고 이를 File 객체로 변환.
- 변환된 File을 upload() 메서드로 전달.
2. convert(MultipartFile multipartFile)
- 설명: MultipartFile 데이터를 로컬 디렉토리에 File로 변환.
- 동작:
- File convertFile = new File(multipartFile.getOriginalFilename()): 기존 파일 이름으로 새로운 파일 객체를 생성.
- convertFile.createNewFile(): 파일이 없으면 새 파일을 생성.
- FileOutputStream을 통해 파일 내용을 convertFile에 작성.
- 성공적으로 생성된 경우 Optional<File> 객체를 반환하고, 실패 시 빈 Optional을 반환.
3. upload(File uploadFile, String dirName)
- 설명: 파일을 S3에 업로드하고 업로드된 파일의 URL을 반환.
- 동작:
- String fileName = dirName + "/" + uploadFile.getName(): 디렉터리 이름과 파일 이름을 합쳐 S3에 저장할 경로를 생성.
- putS3(uploadFile, fileName): S3에 파일 업로드.
- removeNewFile(uploadFile): 로컬에 생성된 임시 파일 삭제.
4. putS3(File uploadFile, String fileName)
- 설명: 파일을 S3에 업로드.
- 동작:
- PutObjectRequest: S3에 업로드 요청을 생성.
- amazonS3Client.putObject(...): S3 버킷에 파일 업로드.
- amazonS3Client.getUrl(...): 업로드된 파일의 URL을 반환.
5. removeNewFile(File targetFile)
- 설명: convert() 과정에서 생성된 로컬 파일을 삭제.
- 동작:
- if (targetFile.delete()): 파일 삭제 성공 여부 확인 후 로그 기록.
3. FileController
@RestController
@RequestMapping("/api/v1/files")
@RequiredArgsConstructor
public class FileController {
private final S3Service s3Service;
@PostMapping("/upload")
public ResponseEntity<String> uploadFile(
@RequestPart(value = "file") MultipartFile multipartFile) {
// 파일 유효성 검사 (예: 파일 크기, 확장자 등)
validateFile(multipartFile);
try {
// S3 업로드 및 URL 반환
String fileUrl = s3Service.upload(multipartFile, "profile-images");
return ResponseEntity.ok(fileUrl);
} catch (IOException e) {
// 예외 처리
log.error("File upload failed: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("File upload failed.");
}
}
private void validateFile(MultipartFile file) {
if (file.isEmpty()) {
throw new IllegalArgumentException("Empty file is not allowed.");
}
if (file.getSize() > 5 * 1024 * 1024) { // 5MB 제한
throw new IllegalArgumentException("File size exceeds limit.");
}
String contentType = file.getContentType();
if (!Arrays.asList("image/jpeg", "image/png").contains(contentType)) {
throw new IllegalArgumentException("Invalid file type.");
}
}
}
참고 :
https://github.com/ParkSungGyu1/spring-plus
GitHub - ParkSungGyu1/spring-plus: spring-plus
spring-plus. Contribute to ParkSungGyu1/spring-plus development by creating an account on GitHub.
github.com
HttpServletRequest 방식
- 서비스
@Slf4j
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
// HttpServletRequest를 사용하여 파일 업로드 처리
public String upload(HttpServletRequest request, String dirName) throws IOException {
// HttpServletRequest를 MultipartHttpServletRequest로 캐스팅
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
// 파일을 HttpServletRequest에서 추출
MultipartFile multipartFile = multipartRequest.getFile("file");
if (multipartFile == null) {
throw new IllegalArgumentException("No file found in the request.");
}
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("Convert_Fail"));
return upload(uploadFile, dirName);
}
private String upload(File uploadFile, String dirName) {
String fileName = dirName + "/" + uploadFile.getName();
String uploadImageUrl = putS3(uploadFile, fileName);
removeNewFile(uploadFile); // convert() 과정에서 로컬에 생성된 파일 삭제
return uploadImageUrl;
}
private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(
new PutObjectRequest(bucket, fileName, uploadFile) // PublicRead 권한으로 upload
);
return amazonS3Client.getUrl(bucket, fileName).toString(); // File의 URL return
}
private void removeNewFile(File targetFile) {
String name = targetFile.getName();
// convert() 과정에서 로컬에 생성된 파일을 삭제
if (targetFile.delete()) {
log.info(name + "파일 삭제 완료");
} else {
log.info(name + "파일 삭제 실패");
}
}
public Optional<File> convert(MultipartFile multipartFile) throws IOException {
// 기존 파일 이름으로 새로운 File 객체 생성
// 해당 객체는 프로그램이 실행되는 로컬 디렉토리(루트 디렉토리)에 위치하게 됨
File convertFile = new File(multipartFile.getOriginalFilename());
if (convertFile.createNewFile()) { // 해당 경로에 파일이 없을 경우, 새 파일 생성
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
// multipartFile의 내용을 byte로 가져와서 write
fos.write(multipartFile.getBytes());
}
return Optional.of(convertFile);
}
// 새파일이 성공적으로 생성되지 않았다면, 비어있는 Optional 객체를 반환
return Optional.empty();
}
}
- HttpServletRequest로 파일 추출:
- HttpServletRequest를 사용하여 MultipartHttpServletRequest로 캐스팅하여 파일을 처리합니다.
- MultipartHttpServletRequest에서 getFile("file")을 사용하여 업로드된 파일을 추출합니다.
- 파일 업로드 처리:
- 추출한 MultipartFile을 기존 로직과 동일하게 변환하여 로컬에 저장한 뒤, S3에 업로드합니다.
- 컨트롤러
@PostMapping("/upload/{userId}")
public ResponseEntity<String> uploadFile(
HttpServletRequest request, // HttpServletRequest로 파일을 받음
@PathVariable Long userId) {
try {
// 파일 업로드 로직 처리
String fileUrl = s3Service.upload(request, "profile-images");
// 파일 URL을 DB에 저장
fileStorageService.saveFileUrl(fileUrl, userId);
return ResponseEntity.ok(fileUrl);
} catch (IOException e) {
log.error("File upload failed: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("File upload failed.");
}
}
Pre-Signed URL 방식
- 서비스
@Slf4j
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
// MultipartFile을 전달받아 File로 전환한 후 S3에 업로드
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("Convert_Fail"));
return upload(uploadFile, dirName);
}
// Pre-Signed URL 생성
public String generatePresignedUrl(String fileName) {
// 만료 시간 설정 (예: 1시간)
Date expiration = new Date(System.currentTimeMillis() + 1000 * 60 * 60);
// S3에 대한 Pre-Signed URL 생성
GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT) // PUT 메서드: 파일 업로드
.withExpiration(expiration);
// Pre-Signed URL 생성
URL url = amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString();
}
// 기존 파일 업로드 로직
private String upload(File uploadFile, String dirName) {
String fileName = dirName + "/" + uploadFile.getName();
String uploadImageUrl = putS3(uploadFile, fileName);
removeNewFile(uploadFile); // convert() 과정에서 로컬에 생성된 파일 삭제
return uploadImageUrl;
}
private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(
new PutObjectRequest(bucket, fileName, uploadFile) // PublicRead 권한으로 upload
);
return amazonS3Client.getUrl(bucket, fileName).toString(); // File의 URL return
}
private void removeNewFile(File targetFile) {
String name = targetFile.getName();
// convert() 과정에서 로컬에 생성된 파일을 삭제
if (targetFile.delete()) {
log.info(name + "파일 삭제 완료");
} else {
log.info(name + "파일 삭제 실패");
}
}
public Optional<File> convert(MultipartFile multipartFile) throws IOException {
File convertFile = new File(multipartFile.getOriginalFilename());
if (convertFile.createNewFile()) { // 해당 경로에 파일이 없을 경우, 새 파일 생성
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
// multipartFile의 내용을 byte로 가져와서 write
fos.write(multipartFile.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
}
- 컨트롤러
@RestController
@RequestMapping("/api/v1/files")
public class FileController {
private final S3Service s3Service;
public FileController(S3Service s3Service) {
this.s3Service = s3Service;
}
// Pre-Signed URL을 생성하여 반환하는 API
@GetMapping("/generate-presigned-url/{fileName}")
public ResponseEntity<String> generatePresignedUrl(@PathVariable String fileName) {
// Pre-Signed URL 생성
String preSignedUrl = s3Service.generatePresignedUrl(fileName);
return ResponseEntity.ok(preSignedUrl); // 클라이언트에게 URL 반환
}
}
- 파일 업로드: 클라이언트는 반환받은 Pre-Signed URL을 사용하여 S3에 파일을 업로드할 수 있습니다. 이때 PUT 요청을 사용하여 파일을 전송합니다.
- 파일 다운로드: Pre-Signed URL을 통해 S3에서 파일을 다운로드할 수도 있습니다. 이 경우 GET 요청을 사용합니다.
비교표
Pre-Signed URL | 서버 부하 감소, 전송 속도 빠름, 보안성 있음 | 복잡한 클라이언트 구현, 파일 검증 어려움, URL 유출 가능 | 대규모 업로드 또는 서버 부하를 줄이고 싶은 경우 |
HttpServletRequest | 검증 및 처리 가능, 클라이언트 구현 단순, 유연한 처리 가능 | 서버 부하 증가, 속도 저하, 비용 증가 | 강력한 검증이 필요하거나 파일 처리(압축, 변환 등)가 필요한 경우 |
MultipartFile | Spring에서 간단한 구현, 검증 및 처리 가능, 클라이언트 구현 단순 | 서버 부하 증가, 전송 속도 저하, 큰 파일 비효율 | 간단한 Spring 기반 파일 업로드가 필요한 경우 |
'Spring > Spring 문법' 카테고리의 다른 글
Mock 을 이용한 테스트 코드 - argument matcher (0) | 2024.11.26 |
---|---|
N + 1 문제 해결, JPQL과 QueryDsl에서 JOIN FETCH (0) | 2024.11.26 |
Transaction Propagation (0) | 2024.11.25 |
QueryDsl - Projections 의 4가지 방식 (0) | 2024.11.21 |
Spring Security 적용 (0) | 2024.11.14 |