Spring/Spring 문법

AWS S3 버킷 사용하기

열심히 해 2024. 11. 25. 13:51

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 기반 파일 업로드가 필요한 경우