0. Intro


이전 게시물에서 파일 업로드를 위한 사전 작업을 마무리하였다.

이제 본격적으로 AWS S3 생성부터 파일 업로드까지 진행해보자.


1. AWS S3 버킷 생성


버킷을 생성하기에 앞서, S3 버킷의 개념을 간단히 알아보자.

S3에는 Bucket(버킷)과 Object(객체)라는 개념이 등장한다.

  • 여기서 객체는 데이터와 메타데이터를 구성하고 있는 저장 단위이며,
  • 버킷은 이러한 객체를 저장하고 관리하는 역할을 한다.

그래서 S3를 구성할 때, 버킷을 놓을 Region을 선택하고, 해당 버킷 안에 객체형태로 데이터를 저장하는 방식으로 구성한다.

그렇기 때문에, S3에 저장되는 데이터는 모두 객체, Object이다.

조금 더 살펴보면, S3는 모든 객체를 관리하는 최상위 디렉토리이며 S3에서 버킷은 전세계에서 유일해야 하기 때문에 중복된 이름이 존재할 수 없다.

그럼 지금부터 버킷을 생성해보자.

1-1. S3 버킷 생성

생성할 버킷 이름을 입력할 때 전세계에서 유일해야 하기 때문에 누군가 생성한 버킷 이름으로는 생성할 수 없다.

버킷 리전은 아마 아시아 태평양(서울) ap-northeast-2로 설정되어 있을 것이다.

image

객체 소유권과 퍼블릭 액세스 차단 설정은 Default 설정 그대로 두고 생성한다.

image image

생성하고, 해당 버킷 창에 들어가면, 중간에 폴더 만들기를 통해 폴더를 생성해야 한다.

나는, 이미 test라는 폴더를 만들었지만, 처음 생성한 사람은 아무것도 뜨지 않을 것이다.

✅ 여기서 폴더는 Spring Boot와 연동할 때 파일을 저장할 디렉토리 경로에 사용할 것이다.

image

image


2. Springboot 설정


이제 개발용 IAM 계정했고, S3 버킷도 생성했으니, Springboot랑 연동해보자.

2-1. 스프링 클라우드 의존성 추가

Spring Cloud AWS를 사용하면 S3를 통한 파일 업로드 기능을 구현할 수 있다.

인텔리제이 하단에 Dependencies를 통해 해당 Springboot 프로젝트에 가장 적합한 의존성 버전을 추가한다.

image

2-2. application.yml 설정

다음으로, application.yml파일에 이전에 개발용 IAM 사용자와 S3 버킷을 생성하면서 발급받은 엑세스 키, 비밀 엑세스 키, 버킷 이름, 버킷 리전을 입력해준다.

🚨 해당 정보들은 외부에 공개되면 안되므로 yml 파일엔 가짜 정보를 올리고, Edit Configuration을 통해 환경변수로 입력해준다.


cloud:
  aws:
    credentials:
      accessKey: ${AWS_ACCESS_KEY_ID}       # AWS IAM AccessKey
      secretKey: ${AWS_SECRET_ACCESS_KEY}   # AWS IAM SecretKey
    s3:
      bucket: bucket    # 버킷 이름
    region:
      static: ap-northeast-2
    stack:
      auto: false

image

2-3. AWS 설정 정보 클래스

AwsConfig.java


@Configuration
public class AwsConfig {

    @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() {
        AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }

}

yml에 등록한 엑세스 키, 비밀 엑세스 키, 리전을 Bean으로 등록해준다.

2-4. Entity

S3 버킷에 같은 이름의 파일이 업로드되면 나중에 등록된 파일이 덮어쓰는 문제가 생긴다.

그래서, S3 버킷에 저장되는 파일 이름이 중복되지 않도록 저장된 파일의 URL을 나타내는 필드인 storeFileUrl를 추가했다.

그리고 사용자가 업로드한 파일 이름을 나타내는 uploadFileName필드도 추가했다.

최초 파일 업로드 시간, 마지막 수정 시간, 삭제 시간을 위해 BaseEntity 클래스를 상속했다.

File.java


@Entity
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@Getter
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@SQLDelete(sql = "UPDATE upload_file SET deleted_at = CURRENT_TIMESTAMP where id = ?")
public class File extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String uploadFileName;
    private String storeFileUrl;

    public File(String uploadFileName, String storeFileUrl) {
        this.uploadFileName = uploadFileName;
        this.storeFileUrl = storeFileUrl;
    }
}

2-5. Controller

MultipartFile 객체를 파라미터로 받아와서 Service Layer에 요청을 전달한다.

현재는, 파일 첨부가 하나만 가능하지만, 추후에 여러 개의 파일 첨부도 가능하게 변경할 생각이다.

FileController.java

@RequiredArgsConstructor
@RestController
@Slf4j
public class FileController {

    private final S3UploadService s3UploadService;

    @PostMapping("/upload")
    public ResponseEntity<Response<UploadFileResponse>> uploadFile(@RequestParam MultipartFile multipartFile) throws IOException {
        log.info("파일 등록 완료");
        return ResponseEntity.ok().body(Response.success(s3UploadService.saveUploadFile(multipartFile)));
    }
}

image

2-6. Service

S3UploadService.java

@Service
@RequiredArgsConstructor
public class S3UploadService {

    private final AmazonS3Client amazonS3Client;
    private final FileRepository fileRepository;

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

    @Transactional
    public UploadFileResponse saveUploadFile(MultipartFile multipartFile) throws IOException {

        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(multipartFile.getContentType());
        // 파일 크기 설정
        objectMetadata.setContentLength(multipartFile.getSize());

        // 파일 이름
        String originalFilename = multipartFile.getOriginalFilename();
        int index = originalFilename.lastIndexOf(".");
        String ext = originalFilename.substring(index + 1);

        // 저장될 파일 이름
        String storeFileName = UUID.randomUUID() + "." + ext;
        // 저장할 디렉토리 경로 + 파일 이름
        String key = "test/" + storeFileName;

        try (InputStream inputStream = multipartFile.getInputStream()) {
            amazonS3Client.putObject(new PutObjectRequest(bucket, key, inputStream, objectMetadata)
                    .withCannedAcl(CannedAccessControlList.PublicRead));
        }

        String storeFileUrl = amazonS3Client.getUrl(bucket, key).toString();
        File file = new File(originalFilename, storeFileUrl);
        File savedFile = fileRepository.save(file);

        return UploadFileResponse.of(savedFile);
    }
}

앞서, Spring Cloud AWS를 사용하면, AmazonS3Client Bean은 자동 생성되기 때문에, 따로 AWS 설정 정보 클래스를 만들어주지 않아도 된다.

나는 나중에 알아버려서… 나중에 빼는 방향으로 바꿔보도록 하겠다.

코드 순서대로 설명해보면,

  • ObjectMetadata 객체에 content-length와 size를 지정한다. 지정하지 않으면 warning 로그 발생.
  • 파일 이름과 UUID로 생성한 랜덤 값을 합쳐서 S3에 저장될 파일 이름을 만든다.
  • 그리고, 해당 파일을 이전에 버킷 생성 시 폴더를 생성했는데, 해당 폴더에 저장되도록 한다.
  • 버킷, 저장할 디렉토리 + 파일이름 등의 데이터와 메타데이터를 AmazonS3Client Bean에 주입하고, withCannedAcl 메서드를 통해 업로드한 파일을 모두가 읽을 수 있게 설정한다.
  • 마지막으로, getUrl 메서드를 통해 S3에 업로드된 사진 URL을 가져와 File Entity에 저장한다.

2-7. Repository, DTO

Repository는 JpaRepsitory를 상속받는 인터페이스로 생성한다.

반환 DTO에서 성공 메세지와 함께 사용자가 업로드한 파일 이름을 반환하도록 한다.

FileRepository.java

@Repository
public interface FileRepository extends JpaRepository<File, Long> {
}

UploadFileResponse.java

@Getter
@Builder
public class UploadFileResponse {

    private String uploadFileName;
    private String message;

    public static UploadFileResponse of(File file) {
        return UploadFileResponse.builder()
                .uploadFileName(file.getUploadFileName())
                .message("파일 첨부 완료")
                .build();
    }
}


3. 에러 해결


여기서 요청을 날렸을 때 되면 좋겠지만, 초기에 S3 버킷을 생성할 때, Default 설정으로 진행해 작동하지 않는다.

3-1. 업로드 파일 용량 초과 에러

파일 업로드 요청을 보내면, 다음과 같이 Maximum upload size exceeded 문구가 뜬다.

업로드할 수 있는 최대 용량을 넘긴 파일을 업로드 하려고 할 때 발생하는 에러인데, Default 값이 약 1MB라고 한다. 내가 업로드하려고 했던 파일이 1.1MB라 에러가 났다.

image

image

나는 SpringBoot 2.x버전이기 때문에 다음과 같이 application.yml 파일에서 설정을 추가하면 된다.

servlet:
  multipart:
    max-file-size: 50MB
    max-request-size: 50MB

3-2. SdkClientException 에러

용량 조절을 해주었는데도 이 에러는 여전히 뜬다.

image

사실, 애플리케이션 실행 자체에는 문제가 없어 넘어갈 수 있는 에러지만, 프로젝트를 Run 할 때마다 시간이 딜레이 되면서 버벅거린다.

해당 에러가 발생하는 이유는, build.gradle에 spring-cloud-starter-aws 의존성 주입시 로컬환경은 AWS 환경이 아니기 때문이라고 한다.

방법은 2가지가 있다.

  1. 로그만 안보이게 하기 - 여전히 딜레이는 됨. 로그만 안 보일 뿐
  2. ✅ AWS가 아닌 환경의 시스템 환경변수를 만들어 주기 - 채택

Edit Configuration에서 vm options-Dcom.amazonaws.sdk.disableEc2Metadata=true를 입력한다.

image

다시 Run하면… EC2 메타데이터를 제외하고 실행한다는 의미의 에러로 변경되었다. 즉, 애플리케이션 실행 시 딜레이가 더이상 없다는 의미가 된다.

image

3-3. The bucket does not allow ACLs (Service: Amazon S3; Status Code: 400; Error Code: AccessControlListNotSupported 에러

해당 에러는 간단하다.

초기에 S3 버킷 생성 시에 버킷의 객체 소유권(ACL)을 비활성화시킨 상태로 생성했기 때문에 발생한 것이다.

버킷 > 객체 소유권 편집 에서 ACL 활성화 시키면 된다.

image

3-4. AWS S3 403 Forbidden 에러

이 에러도 마찬가지로 초기 버킷 생성 시에 버킷 정책을 설정해주지 않아서 발생했다.

  1. 먼저, 퍼블릭 엑세스 차단을 수정한다.

권한 -> 퍼블릭 액세스 차단 -> 편집 -> 체크 모두 해제 -> 변경사항 저장

아마 Default 설정 그대로 버킷을 생성했다면 모든 퍼블릭 엑세스 차단에 클릭이 되어 있을 것이다. 모두 풀어주자.

image

  1. 다음으로, 버킷 정책을 편집해야 한다.

권한 -> 버킷정책 -> 편집 -> 버킷정책

버킷 정책 편집에서 버킷 ARN을 복사하고, 오른쪽 상단에 있는 정책 생성기 버튼을 클릭한다.

image

  1. 버킷 정책 생성

정책 생성기 버튼을 클릭했다면 정책 생성 페이지가 나온다.

  • Select Type of Policy - S3 Bucket Policy
  • Principal - *
  • Actions - GetObject
  • ARN - 복사한 버킷 ARN 뒤에 /*추가한뒤, Add Statement를 클릭한다.

image

Generate Policy를 클릭하면

image

JSON 타입의 정책이 생성되고, 해당 내용을 복사하여 처음의 버킷 정책 편집 창에 붙여넣고 저장한다.

image

최종 파일 업로드 요청

이제, 파일 업로드 요청을 보내면, 파일 첨부되었다는 성공 메세지와 함께 S3 버킷의 test 폴더에 파일이 저장되고, DB에도 저장된 파일의 URL과 사용자가 저장할 때의 파일 이름이 저장된다.

image

image

image

References

Leave a comment