Spring Boot와 파일서버로 사용하는 AWS S3를 연동할 것이다.

S3 버킷 생성

image image → Spring을 통해 S3에 파일을 업로드하기 위해서는 ACL을 활성화해야 한다. 비활성화 상태라면 AmazonS3Exception: The bucket does not allow ACL이 발생한다

  • 버킷 정책설정
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AddPerm",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::myapp-file/*"
        }
    ]
}
  • IAM 사용자 생성 image image

설정

build.gradle

implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

application.yml

cloud:
  aws:
    credentials:
      access-key: AKIARAVCBSDFBM5U3M4K6
      secret-key: lkRQsFSDFDb8f0DePAJdY4gQuXxKca
    s3:
      bucket: myapp-file
    region:
      static: ap-northeast-2
    stack:
      auto: false

→ IAM에서 생성한 사용자의 access-keysecret-key를 입력한다
→ 해당 S3 버킷의 이름s3.bucket과 지역region.static을 입력한다

Sptring

AwsS3Config

@Configuration
public class AwsS3Config {
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public AmazonS3Client amazonS3Client() {
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}

AwsS3Uploader

@Log4j2
@RequiredArgsConstructor
@Component
public class AwsS3Uploader {

    private final AmazonS3Client amazonS3Client;

    @Value("${cloud.aws.s3.bucket}")
    public String bucket;

    public String upload(MultipartFile multipartFile, String dirName) throws IOException {
        File uploadFile = convert(multipartFile)        // 파일 생성
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File convert fail"));

        return upload(uploadFile, dirName);
    }

    private String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName();
        String uploadImageUrl = putS3(uploadFile, fileName);    // s3로 업로드
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }

    // 1. 로컬에 파일생성
    private Optional<File> convert(MultipartFile file) throws IOException {
        File convertFile = new File(file.getOriginalFilename());
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();
    }

    // 2. S3에 파일업로드
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        log.info("File Upload : " + fileName);
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    // 3. 로컬에 생성된 파일삭제
    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("File delete success");
            return;
        }
        log.info("File delete fail");
    }


    public void delete(String fileName) {
        log.info("File Delete : " + fileName);
        amazonS3Client.deleteObject(bucket, fileName);
    }
}
  • 순서
    1. 클라이언트로 부터 입력받은 MultipartFile를 S3에 업로드 하기 위해 File로 변환한다 (로컬에 파일생성)
    2. AWS 라이브러리를 사용해 로컬에 생성된 파일을 S3에 업로드 한다
    3. S3 업로드를 위해 로컬에 생성했던 파일을 삭제한다

FileController

@RequiredArgsConstructor
@RestController
public class FileController {
    private final AwsS3Uploader awsS3Uploader;

    @PostMapping("/upload")
    public String upload(@RequestParam("file") MultipartFile multipartFile) throws IOException {
        String fileName = awsS3Uploader.upload(multipartFile, "test");
        return fileName;
    }
}

테스트

  • S3 Mock을 사용해서 로컬 환경에서 S3 버킷으로의 업로드 테스트가 가능하다.
  • findify의 S3Mock을 사용 할 것이다.
testImplementation 'io.findify:s3mock_2.13:0.2.6'

AwsS3MockConfig

  • 테스트를 위한 설정파일
@TestConfiguration
public class AwsS3MockConfig {
    @Value("${cloud.aws.s3.bucket}")
    public String bucket;

    @Value("${cloud.aws.region.static}")
    private String region;

    @Bean
    public S3Mock s3Mock() {
        return new S3Mock.Builder().withPort(8001).withInMemoryBackend().build();
    }

    @Bean
    public AmazonS3 amazonS3(S3Mock s3Mock){
        s3Mock.start();
        AwsClientBuilder.EndpointConfiguration endpoint = new AwsClientBuilder.EndpointConfiguration("http://localhost:8001", region);
        AmazonS3 client = AmazonS3ClientBuilder
                .standard()
                .withPathStyleAccessEnabled(true)
                .withEndpointConfiguration(endpoint)
                .withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials()))
                .build();
        client.createBucket(bucket);

        return client;
    }
}

AwsS3UploaderTest

  • 앞서 구현한 S3 버킷에 업로드하는 컴포넌트의 테스트 파일
  • MockMultipartFile을 사용하여 업로드
@Import(AWSS3MockConfig.class)
@SpringBootTest
class AwsS3UploaderTest {

    @Autowired
    private S3Mock s3Mock;
    @Autowired
    private AwsS3Uploader awsS3Uploader;

    @AfterEach
    public void tearDown() {
        s3Mock.stop();
    }

    @Test
    void upload() throws IOException {
        // given
        String path = "test.png";
        String contentType = "image/png";
        String dirName = "test";

        MockMultipartFile file = new MockMultipartFile("test", path, contentType, "test".getBytes());

        // when
        String urlPath = awsS3Uploader.upload(file, dirName);

        // then
        assertThat(urlPath).contains(path);
        assertThat(urlPath).contains(dirName);
    }
}