티스토리 뷰

 

클라이언트가 이미지를 업로드할 때, 서버에서 처리할 수 있는 방법은 2가지가 존재합니다.

  1. 서버에서 MultipartFile 형태로 데이터를 받아서 AWS S3로 업로드한다. 
  2. 서버에서 presignedUrl을 발급하여 AWS S3로 업로드한다.

2번에서 presignedUrl이 다소 생소할지라도 이번 글을 통해 이해할 수 있습니다. (AWS S3 생성 및 스프링과 연결 방법에 대해서는 생략합니다)

 

1. MultipartFile 형태로 서버에서 처리

스프링에서는 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 호출

API 호출 결과

클라이언트 -> 서버 -> AWS S3 순서대로 이미지가 업로드됩니다.

 

2. PresignedUrl 발급하여 이미지 업로드

1번의 경우 반드시 서버를 거쳐서 이미지를 업로드해야 합니다. 1번이 갖는 단점은 아래와 같습니다.

  • 이미지가 서버를 거치기 때문에 네트워크 부하와 트래픽이 증가한다.
  • PresignedUrl 방법보다 업로드 속도가 증가한다.
  • 사용자 수가 급증하였을 경우 서비스의 안정성을 보장할 수 없다.

따라서 2번 방법으로 이미지를 업로드하는 것이 좋습니다.

 

설명에 앞서 presignedUrl에 대해서 알아보겠습니다. PresignedUrl이란 S3 버킷에 파일을 업로드하거나 다운로드할 수 있도록 하는 URL입니다. 물론 임시 URL이기 때문에 시간이 유효합니다. 과정은 다음과 같습니다.

  1. AWS SDK를 사용하여 S3 객체에 대한 presignedUrl을 발급한다.
    1. presignedUrl에는 AWS 액세스 키, 객체의 이름, 버킷, 만료시간 등이 포함됩니다.
  2. 발급받은 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 호출 결과

API를 호출하여 이미지 업로드를 위한 presignedUrl를 발급받습니다. 발급된 presignedUrl로 PUT 요청을 하여 이미지를 업로드합니다. 이때 PUT 요청은 서버에서 제공하는 API가 아닌 S3 버킷으로 이미지를 바로 업로드를 할 수 있도록 AWS에서 제공하는 API입니다.

 

S3 버킷에 저장된 객체

 

Total
Today
Yesterday
최근에 올라온 글
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30