ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Amazon S3로 파일 업로드 및 삭제
    Spring Boot/기타 2023. 1. 27. 17:34
    반응형

    Amazon Simple Storage Service(Amazon S3) S3란?

    확장성, 데이터 가용성, 보안 및 성능을 제공하는 객체 스토리지 서비스

     

    참조 - https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/userguide/Welcome.html


    Amazon S3 설정

    1. 의존성 추가

    Amazon S3를 사용하기 위해 spring-cloud-starter-aws 의존성을 추가하세요.

    build.gradle

    plugins {
        id 'java'
        id 'org.springframework.boot' version '2.7.7'
        id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    }
    
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
    
        // Spring Boot
        implementation 'org.springframework.boot:spring-boot-starter-web'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
        // Spring Cloud Starter AWS
        implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
    
        // Lombok
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        testCompileOnly 'org.projectlombok:lombok'
        testAnnotationProcessor 'org.projectlombok:lombok'
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    }


    2. 환경변수 설정

    Amazon S3에 접근하기 위해 필요한 bucket, accesskey, secretKey 정보를 설정하세요.

    application.yml

    # SPRING 설정
    spring:
      profiles:
        active: local
        group:
          local: profiles-local
          dev: profiles-dev
    
    ---
    # AWS 설정
    cloud:
      aws:
        region:
          static: ap-northeast-2  # AWS 기본 리전을 설정
        stack:
          auto: false # Spring Cloud를 실행하면, 서버 구성을 자동화하는 CloudFormation이 실행되는데 이를 사용하지 않으므로 false로 설정
    
    logging:
      level:
        com:
          amazonaws:
            util:
              EC2MetadataUtils: error # 해당 클래스에서 예외가 발생하면 로그를 출력하지 않음
    
    ---
    # Multipart 설정
    spring:
      servlet:
        multipart:
          max-file-size: 100MB # 요청한 파일 한 개의 크기
          max-request-size: 100MB # 요청한 파일 전체의 크기
    
    ---
    # LOCAL 설정
    spring.config.activate.on-profile: "profiles-local"
    spring:
      s3:
        bucket:
        access-key:
        secret-key:
    
    ---
    # DEV 설정
    spring.config.activate.on-profile: "profiles-dev"
    spring:
      s3:
        bucket:
        access-key:
        secret-key:


    3. Configuration 설정

    Amazon S3로 접근하기 위해 필요한 AmazonS3Client 클래스를 사용하기 위해 AmazonS3ClientBuilder 클래스로 객체를 생성한 후 빈으로 등록하세요.

    AmazonS3Config.java

    package com.example.amazon.s3.config;
    
    import com.amazonaws.auth.AWSStaticCredentialsProvider;
    import com.amazonaws.auth.BasicAWSCredentials;
    import com.amazonaws.services.s3.AmazonS3Client;
    import com.amazonaws.services.s3.AmazonS3ClientBuilder;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class AmazonS3Config {
    
      @Value("${spring.s3.access-key}")
      private String accessKey;
    
      @Value("${spring.s3.secret-key}")
      private String secretKey;
    
      @Value("${cloud.aws.region.static}")
      private String region;
    
      @Bean
      public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder
            .standard()
            .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
            .withRegion(region)
            .build();
      }
    }


    4. 파일 업로드 및 삭제 구현

    4_1. 원본 파일명, 업로드된 파일명, 업로드된 파일 경로, 업로드된 파일의 URL를 응답하기 위해 Dto를 추가하세요.

    S3FileDto.java

    package com.example.amazon.s3.dto;
    
    import lombok.Builder;
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    @Getter
    @Setter
    @ToString
    @Builder
    public class S3FileDto {
    
      private String originalFileName;
      private String uploadFileName;
      private String uploadFilePath;
      private String uploadFileUrl;
    }

     

    4_2. 파일 업로드는 AmazonS3Client의 putObject 메서드, 파일 삭제는 AmazonS3Client의 deleteObject 메서드를 사용하세요.  

    Amazon3SService.java

    package com.example.amazon.s3.service;
    
    import com.amazonaws.services.s3.AmazonS3Client;
    import com.amazonaws.services.s3.model.ObjectMetadata;
    import com.amazonaws.services.s3.model.PutObjectRequest;
    import com.example.amazon.s3.dto.S3FileDto;
    import java.io.IOException;
    import java.io.InputStream;
    import java.text.SimpleDateFormat;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.Locale;
    import java.util.UUID;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    import org.springframework.web.multipart.MultipartFile;
    
    @Slf4j
    @RequiredArgsConstructor
    @Service
    public class Amazon3SService {
    
      @Value("${spring.s3.bucket}")
      private String bucketName;
    
      private final AmazonS3Client amazonS3Client;
    
      /**
       * S3로 파일 업로드
       */
      public List<S3FileDto> uploadFiles(String fileType, List<MultipartFile> multipartFiles) {
    
        List<S3FileDto> s3files = new ArrayList<>();
    
        String uploadFilePath = fileType + "/" + getFolderName();
    
        for (MultipartFile multipartFile : multipartFiles) {
    
          String originalFileName = multipartFile.getOriginalFilename();
          String uploadFileName = getUuidFileName(originalFileName);
          String uploadFileUrl = "";
    
          ObjectMetadata objectMetadata = new ObjectMetadata();
          objectMetadata.setContentLength(multipartFile.getSize());
          objectMetadata.setContentType(multipartFile.getContentType());
    
          try (InputStream inputStream = multipartFile.getInputStream()) {
    
            String keyName = uploadFilePath + "/" + uploadFileName; // ex) 구분/년/월/일/파일.확장자
    
            // S3에 폴더 및 파일 업로드
            amazonS3Client.putObject(
                new PutObjectRequest(bucketName, keyName, inputStream, objectMetadata));
    
            // TODO : 외부에 공개하는 파일인 경우 Public Read 권한을 추가, ACL 확인
            /*amazonS3Client.putObject(
                new PutObjectRequest(bucket, s3Key, inputStream, objectMetadata)
                    .withCannedAcl(CannedAccessControlList.PublicRead));*/
    
            // S3에 업로드한 폴더 및 파일 URL
            uploadFileUrl = amazonS3Client.getUrl(bucketName, keyName).toString();
    
          } catch (IOException e) {
            e.printStackTrace();
            log.error("Filed upload failed", e);
          }
    
          s3files.add(
              S3FileDto.builder()
                  .originalFileName(originalFileName)
                  .uploadFileName(uploadFileName)
                  .uploadFilePath(uploadFilePath)
                  .uploadFileUrl(uploadFileUrl)
                  .build());
        }
    
        return s3files;
      }
    
      /**
       * S3에 업로드된 파일 삭제
       */
      public String deleteFile(String uploadFilePath, String uuidFileName) {
    
        String result = "success";
    
        try {
          String keyName = uploadFilePath + "/" + uuidFileName; // ex) 구분/년/월/일/파일.확장자
          boolean isObjectExist = amazonS3Client.doesObjectExist(bucketName, keyName);
          if (isObjectExist) {
            amazonS3Client.deleteObject(bucketName, keyName);
          } else {
            result = "file not found";
          }
        } catch (Exception e) {
          log.debug("Delete File failed", e);
        }
    
        return result;
      }
    
      /**
       * UUID 파일명 반환
       */
      public String getUuidFileName(String fileName) {
        String ext = fileName.substring(fileName.indexOf(".") + 1);
        return UUID.randomUUID().toString() + "." + ext;
      }
    
      /**
       * 년/월/일 폴더명 반환
       */
      private String getFolderName() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
        Date date = new Date();
        String str = sdf.format(date);
        return str.replace("-", "/");
      }
    }

     

    4_3. multipart/form-data의 Content-Type을 요청을 처리하기 위해 @RequestPart 어노테이션을 사용하세요. 

    AmazonS3Controller.java

    package com.example.amazon.s3.controller;
    
    import com.example.amazon.s3.service.Amazon3SService;
    import java.util.List;
    import lombok.RequiredArgsConstructor;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RequestPart;
    import org.springframework.web.multipart.MultipartFile;
    
    @RequiredArgsConstructor
    @Controller
    public class AmazonS3Controller {
    
      private final Amazon3SService amazon3SService;
    
      @PostMapping("/uploads")
      public ResponseEntity<Object> uploadFiles(
          @RequestParam(value = "fileType") String fileType,
          @RequestPart(value = "files") List<MultipartFile> multipartFiles) {
        return ResponseEntity
            .status(HttpStatus.OK)
            .body(amazon3SService.uploadFiles(fileType, multipartFiles));
      }
    
      @DeleteMapping("/delete")
      public ResponseEntity<Object> deleteFile(
          @RequestParam(value = "uploadFilePath") String uploadFilePath,
          @RequestParam(value = "uuidFileName") String uuidFileName) {
        return ResponseEntity
            .status(HttpStatus.OK)
            .body(amazon3SService.deleteFile(uploadFilePath, uuidFileName));
      }
    }

     

    5. 파일 업로드 및 삭제 확인

    Mock 파일 객체를 생성하기 위해 MockMultipartFile 클래스를 사용

    public MockMultipartFile(
    	String name, @Nullable String originalFilename, @Nullable String contentType, InputStream contentStream)
    	throws IOException {
    
    this(name, originalFilename, contentType, FileCopyUtils.copyToByteArray(contentStream));
    }
    • name : form-data의 key 값
    • originalFilename : form-data의 value 값
    • contentType : 파일의 형식
    • contentStream : 바이트 단위의 입력 스트림, 파일을 바이트 단위로 입력할 수 있도록 FileInputStream 클래스를 사용

    multipart 타입으로 요청하기 위해 MockMvcRequestBuilders 클래스의 multipart() 메서드를 사용

    public static MockMultipartHttpServletRequestBuilder multipart(String urlTemplate, Object... uriVariables) {
    	return new MockMultipartHttpServletRequestBuilder(urlTemplate, uriVariables);
    }


    AmazonS3ControllerTest.java

    package com.example.amazon.s3.controller;
    
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    import java.io.FileInputStream;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.mock.web.MockMultipartFile;
    import org.springframework.test.context.ActiveProfiles;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.ResultActions;
    
    @Slf4j
    @AutoConfigureMockMvc
    @SpringBootTest
    @ActiveProfiles("local")
    class AmazonS3ControllerTest {
    
      @Autowired
      MockMvc mockMvc;
    
      @DisplayName("uploadFiles_S3로 파일 업로드")
      @Test
      public void testUploadFiles() throws Exception {
    
        // Given
        String name = "files";
        String contentType = "text/plain";
        String path = "src/main/resources/static/temp";
        String fileType = "temp";
        String originalFileName01 = "temp01.txt";
        String originalFileName02 = "temp02.txt";
        String originalFileName03 = "temp03.txt";
    
        MockMultipartFile multipartFile01 = new MockMultipartFile(
            name,
            originalFileName01,
            contentType,
            new FileInputStream(path + "/" + originalFileName01));
    
        MockMultipartFile multipartFile02 = new MockMultipartFile(
            name,
            originalFileName02,
            contentType,
            new FileInputStream(path + "/" + originalFileName02));
    
        MockMultipartFile multipartFile03 = new MockMultipartFile(
            name,
            originalFileName03,
            contentType,
            new FileInputStream(path + "/" + originalFileName03));
    
        // When
        ResultActions resultActions = mockMvc.perform(
            multipart("/uploads")
                .file(multipartFile01)
                .file(multipartFile02)
                .file(multipartFile03)
                .param("fileType", fileType)
        );
    
        // Then
        resultActions
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
            .andExpect(jsonPath("$[0].originalFileName").value(originalFileName01))
            .andExpect(jsonPath("$[0].uploadFileName").isNotEmpty())
            .andExpect(jsonPath("$[0].uploadFilePath").isNotEmpty())
            .andExpect(jsonPath("$[0].uploadFileUrl").isNotEmpty())
            .andExpect(jsonPath("$[1].originalFileName").value(originalFileName02))
            .andExpect(jsonPath("$[1].uploadFileName").isNotEmpty())
            .andExpect(jsonPath("$[1].uploadFilePath").isNotEmpty())
            .andExpect(jsonPath("$[1].uploadFileUrl").isNotEmpty())
            .andExpect(jsonPath("$[2].originalFileName").value(originalFileName03))
            .andExpect(jsonPath("$[2].uploadFileName").isNotEmpty())
            .andExpect(jsonPath("$[2].uploadFilePath").isNotEmpty())
            .andExpect(jsonPath("$[2].uploadFileUrl").isNotEmpty())
            .andDo(print());
      }
    }

     

    Postman에서 multipart/form-data 요청 시 설정

     

    소스 코드는 Github Repository - https://github.com/tychejin1218/blog.git 에서 amazon-s3 프로젝트를 참조하세요.

    반응형

    댓글

Designed by Tistory.