[SpringBoot] SpringBoot를 이용한 AWS S3에 여러 파일 업로드 및 삭제 구현하기
FRAMEWORK/Spring

[SpringBoot] SpringBoot를 이용한 AWS S3에 여러 파일 업로드 및 삭제 구현하기

반응형

들어가기 전에

원래는 AWS API Gateway + AWS lambda + AWS S3 방식으로 이미지 업로드 및 삭제를 구현하고자 했습니다. 이때, 일반적으로 javascript나 python을 사용하는 것으로 보았는데 해당 언어로 구현하게 되면 정확히 알지 못하는 언어라 유지보수 측면에서 좋지 않을 것 같아 우선 백엔드단에서 java로 구현하되 추후 여유가 되면 Lambda 방식으로 변경하기로 했습니다.

따라서, 이번 포스팅에서는 SpringBoot를 통해 S3에 파일 업로드 및 삭제하는 방법에 대해 알아보겠습니다.

SpringBoot를 통한 S3에 파일 업로드하기(Rest API)

build.gradle

AWS에 접근하기 위해 먼저 build.gradle에 하기와 같이 의존성을 추가해줍니다.

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

AwsS3Config

그 후, S3용 config 파일을 생성하여 하기와 같이 작성합니다.

  • 해당 파일의 경우 AWS S3에 접근할 때 어떤 IAM을 통해 접근하고, 어떤 region을 통해 접근하는지 등을 설정해주는 것입니다.
  • 따라서, S3에 접근할 권한이 있는 IAM을 생성하지 않았다면, 먼저 하단에 작성되어 있는 IAM 사용자 추가하기 작업을 선행한 후에 진행하는 것을 권장합니다.
    • cloud.aws.credentials.access-key: IAM 계정의 accessKey값
    • cloud.aws.credentials.secret-key: IAM 계정의 secretKey값
    • cloud.aws.region.static: 사용하는 region 명
  • 추가적으로, S3에 권한 설정하는 작업 등이 포함된 S3 생성 내용 역시 하기에 기입해두었으니, S3 생성이 필요할 경우 해당 작업을 선행하는 것을 권장합니다.
@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();
    }
}

application.yml

위에서 @Value로 주입한 값은 아래와 같이 application.yml에 넣으면 되며, 필자의 경우 beanstalk을 통한 배포를 진행하여 각 값들을 beanstalk 환경변수에 넣어두었기 때문에 한 번 더 ${beanstalk 환경변수명}으로 넣어주고 있습니다.

  • 참고로, bucket에 넣을 값은 버킷 바로 안에 넣을 것이라면 버킷명만 넣으면 되며, 만약 버킷 내 특정 디렉토리에 넣기 위해서는 버킷명/특정 디렉토리명의 형식으로 넣어주어야 합니다.
cloud:
  aws:
    credentials:
      access-key: ${s3.accesskey}
      secret-key: ${s3.secretkey}
    region:
      static: ${s3.resion}
    s3:
      bucket: ${s3.bucket}
    stack:
      auto: false

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB
  • 기본적으로 multipart로 보낼 수 있는 max-file-sizemax-request-size의 default 값은 1MB입니다.
  • 필자의 경우 사진을 업로드할 때에 최대 10MB의 원본을 가지고 있어 spring.servlet.multipart.max-file-size=10MBspring.servlet.multipart.max-request-size=10MB를 추가하였습니다.

AmazonS3Controller.java

이제, S3 파일 업로드 및 삭제 시 사용할 API를 설정하기 위해 하기와 같이 controller를 생성합니다.

  • 파일 업로드의 경우, GET을 사용하고 있으며 파일 삭제의 경우, DELETE를 사용하도록 구현하였습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/s3")
public class AmazonS3Controller {

    private final AwsS3Service awsS3Service;

    /**
     * Amazon S3에 파일 업로드
     * @return 성공 시 200 Success와 함께 업로드 된 파일의 파일명 리스트 반환
     */
    @ApiOperation(value = "Amazon S3에 파일 업로드", notes = "Amazon S3에 파일 업로드 ")
    @PostMapping("/file")
    public ResponseEntity<List<String>> uploadFile(@ApiParam(value="파일들(여러 파일 업로드 가능)", required = true) @RequestPart List<MultipartFile> multipartFile) {
        return ApiResponse.success(awsS3Service.uploadImage(multipartFile));
    }

    /**
     * Amazon S3에 업로드 된 파일을 삭제
     * @return 성공 시 200 Success
     */
    @ApiOperation(value = "Amazon S3에 업로드 된 파일을 삭제", notes = "Amazon S3에 업로드된 파일 삭제")
    @DeleteMapping("/file")
    public ResponseEntity<Void> deleteFile(@ApiParam(value="파일 하나 삭제", required = true) @RequestParam String fileName) {
        awsS3Service.deleteImage(fileName);
        return ApiResponse.success(null);
    }
}

AwsS3Service.java

아래는 실제로 S3에 파일을 업로드하고 삭제하는 서비스 부분입니다. 현재, 파일 업로드는 여러 파일을 for문을 통해 각각 업로드할 수 있게 구현하였으며 삭제는 한개의 파일씩 가능하도록 구현하였습니다.

@Service
@RequiredArgsConstructor
public class AwsS3Service {

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

    private final AmazonS3 amazonS3;

    public List<String> uploadFile(List<MultipartFile> multipartFile) {
        List<String> fileNameList = new ArrayList<>();

        // forEach 구문을 통해 multipartFile로 넘어온 파일들 하나씩 fileNameList에 추가
        multipartFile.forEach(file -> {
            String fileName = createFileName(file.getOriginalFilename());
            ObjectMetadata objectMetadata = new ObjectMetadata();
            objectMetadata.setContentLength(file.getSize());
            objectMetadata.setContentType(file.getContentType());

            try(InputStream inputStream = file.getInputStream()) {
                amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
                        .withCannedAcl(CannedAccessControlList.PublicRead));
            } catch(IOException e) {
                throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");
            }

            fileNameList.add(fileName);
        });

        return fileNameList;
    }

    public void deleteFile(String fileName) {
        amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
    }

    private String createFileName(String fileName) { // 먼저 파일 업로드 시, 파일명을 난수화하기 위해 random으로 돌립니다.
        return UUID.randomUUID().toString().concat(getFileExtension(fileName));
    }

    private String getFileExtension(String fileName) { // file 형식이 잘못된 경우를 확인하기 위해 만들어진 로직이며, 파일 타입과 상관없이 업로드할 수 있게 하기 위해 .의 존재 유무만 판단하였습니다.
        try {
            return fileName.substring(fileName.lastIndexOf("."));
        } catch (StringIndexOutOfBoundsException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일(" + fileName + ") 입니다.");
        }
    }
}

Postman 결과물

S3에 파일 업로드하기

  • 필자의 경우, API를 호출할 때 RequestPart로 받을 multipart 이름을 multipartFile로 정했기 때문에 위와 같이 key에 multipartFile을 넣어 호출하였습니다.
  • 파일 업로드 응답으로 S3에 넣은 파일명들이 오며, 해당 파일들이 모두 S3에 들어가있음을 확인할 수 있습니다.

S3에 업로드한 파일 삭제하기

  • 위처럼 delete를 날리면 기존에 업로드했던 파일이 삭제되는 것을 확인할 수 있습니다.

추가 내용

S3 버킷 접근 권한을 가진 IAM 사용자 생성하기

먼저, AWS의 IAM 페이지에 접근하여 액세스 관리 > 사용자 > 사용자 추가 버튼을 누릅니다.

사용자 추가 버튼 클릭

그 후, 하기와 같이 액세스 키 방식을 선택하고 기존 정책 직접 연결 > AmazonS3FullAccess를 체크하여 사용자를 추가합니다.

액세스 키 방식 선택 및 AmazonS3FullAccess 체크하여 사용자 추가

추가가 완료되면, 아래와 같은 .csv 다운로드 페이지가 뜨며 해당 페이지를 건너뛸경우 accessKeysecretKey를 다시 확인할 수 없기 때문에 반드시 .csv 다운로드를 하여 키를 확인하여야 합니다.

accessKey 및 secretKey 확인을 위해 .csv 다운로드 필수

S3 Bucket 생성하기

AWS S3 Bucket 생성하는 방법에 대해 알아보겠습니다. SpringBoot를 통해 객체를 넣고, 삭제하는 작업을 해야하기 때문에 해당 권한을 넣은 S3 버킷을 만들도록 하겠습니다.

먼저 Amazon S3에 접근하여 버킷 만들기를 클릭한 후, 퍼블릭 엑세스가 가능하도록 버킷을 생성합니다.

  • SpringBoot를 통해 파일을 업로드/삭제를 진행해야 하기 때문에 퍼블릭 액세스가 가능해야 합니다.

새 ACL을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단 허용

  • 그중에서도 새 ACL(액세스 제어 목록)을 통해 부여된 버킷 및 객체에 대한 퍼블릭 액세스 차단에 대해 허용이 필요하니 실습이 아니라면 해당 항목을 제외하고 퍼블릭 액세스 차단해주어도 됩니다.

버킷 생성하기

생성된 버킷에 대해 파일을 업로드, 다운로드, 삭제가 가능하도록 정책을 생성하도록 하겠습니다.

  • 먼저, 생성된 버킷에 접근하여 권한을 클릭한 후 버킷 정책의 편집 버튼을 클릭합니다.
  • 그 후 버킷 정책 편집 페이지의 정책 생성기를 눌러 정책을 생성합니다.

권한 > 버킷 정책 > 편집 > 정책 생성기 클릭

새로 뜬 창의 정책 생성기에서 하기와 같이 필요한 항목들을 기입해줍니다. 필자의 경우 파일 업로드, 다운로드, 삭제를 진행할 것이기 때문에 Actions에 s3:DeleteObject, s3:GetObject, s3:PutObject를 선택하여 정책을 만들었습니다.

  • 이때 만들어진 정책은 버킷 정책의 정책 부분에 기입하여 변경 내용 저장을 해줍니다.

정책 생성기를 통한 Get, Put, Delete Object action 권한 생성

추가적으로, SpringBoot를 통해 S3 파일을 업로드/삭제를 진행하기 위해서는 ACL을 허용해주어야 합니다.

만약, 하기와 같이 객체 소유권이 버킷 소유자 적용으로 되어 있다면, ACL이 비활성화되며, SpringBoot를 통해 파일 업로드 시 The Bucket doest not allow ACLs라는 에러를 뱉어줍니다.

ACL 비활성화 시, SpringBoot를 통해 파일 업로드 불가

따라서, 하기와 같이 객체 소유권을 변경한 후 진행해야 합니다.

ACL 활성화를 통해 파일 업로드 성공

여기까지 완료하였다면 S3에 대한 설정은 완료된 것입니다.

EC2가 아닌 로컬에서 실행할 때 com.amazonaws.SdkClientException: Failed to connect to service endpoint: 해결법

com.amazonaws.SdkClientException: Failed to connect to service endpoint

해당 구문은 EC2 메타데이터를 읽다가 이슈가 발생한 것으로 EC2 인스턴스가 아닌 환경에서 실행할 때에는 의미 없는 에러입니다. 사실 해당 이슈로 인해 어플리케이션이 동작하지 않는 등의 이슈는 없지만, 보기 좋지 않기 때문에 해결해보도록 하겠습니다.

해당 이슈를 해결하기 위해서는 아래와 같이 VM option에 -Dcom.amazonaws.sdk.disableEc2Metadata=true을 넣어주면 됩니다.

VM option 설정

추가적으로, 테스트 환경에서 위 VM option을 작성하기 위해서는 static으로 system property를 넣어주면 됩니다.

    static {
        System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true");
    }

설정을 완료한 뒤, Springboot을 실행하면 아래와 같이 EC2 Instance Metadata Service is disabled라는 구문이 뜨면서 EC2 메타데이터 서비스를 제외하고 실행할 수 있게 됩니다.

EC2 metadata 해제

참고 자료

반응형