티스토리 뷰

업무 중 파일 업로드를 실제 서버로 파일을 보내는 대신 클라이언트에서 처리하는 방향으로 변경하면서 PreSignedUrl을 사용하게 되어 관련 내용을 정리하려고 합니다. 전 회사에서 해당 방식을 사용한다는 것은 알고 있었지만 어떻게 동작하는지 구체적으로 관심가지지 않아서 몰랐던 내용을 직접 해보면서 정리하였습니다. 😅

 

1. AWS IAM Role 에 S3 업로드 권한 부여

S3에 파일을 업로드 및 다운로드하기 위해서는 S3에 대한 권한을 가지고 있어야 합니다.

저는 개인적으로 사용하던 기존 User에 S3FullAccess 권한을 부여했습니다. (local AWS CLI를 통해 접근하는 User 또는 EC2에게 주어지는 Role에 권한을 추가해주면 됩니다.)

 

 

* 사용자를 새로 생성한 경우 CLI 에서 접속할 수 있는 액세스 키를 생성하고 생성된 키를 .aws/config 파일에 추가해줍니다.

더보기

 

.aws/config 파일에 다음과 같이 작성

[PROFILE_NAME]
aws_access_key_id = 생성된_액세스_키
aws_secret_access_key = 생성된_시크릿_키

 

 

2. AWS S3 버킷 생성 및 정책 등록

이후 파일을 업로드할 버킷을 생성합니다. S3 버킷은 기본적으로 퍼블릭 접근을 허용하지 않도록 만들어지지만 지금의 경우 클라이언트에서 접근이 가능해야 하기 때문에 퍼블릭 설정 후 정책을 추가하도록 하겠습니다. (퍼블릭 액세스를 차단하고 접근이 가능한지는 조금 더 살펴봐야할 것 같습니다...ㅠㅠ)

 

 

위와 같이 버킷을 생성한 다음 버킷 정책을 설정하였습니다.

{
    "Version": "2012-10-17",
    "Id": "Policy169930*******",
    "Statement": [
        {
            "Sid": "Stmt169930*******",
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "S3_Full_Access_권한을_가진_사용자_ARN"
                ]
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::BUCKET_NAME/*"
        }
    ]
}

 

3. Backend 소스 작성

(1) aws credential 설정

위에서 생성한 사용자의 access-key, secret-key 를 가지고 .aws/credential 설정을 합니다. (config 파일에 등록해서 사용해도 됩니다. 1번 하단 더보기 참고)

 

(2) build.gradel 에 dependency 추가

저는 SpringBoot 3.1.3 + Java 17 을 사용하는 환경이었고 다음과 같이 aws에 대한 dependency를 추가하였습니다.

dependencies {
    ...
    
    // aws cloud
    implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
    
    ....
}

 

(2) application.yml 설정

cloud:
  aws:
    s3:
      bucket: BUCKET_NAME
    region:
      static: ap-northeast-2

 

(3) AmazonS3Config 작성

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.WebIdentityTokenCredentialsProvider;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import java.util.Arrays;
import java.util.List;

@Slf4j
@Configuration
public class AmazonS3Config {
    private static final List<String> SUPPORTED_PROFILES = Arrays.asList("default", "local");
    private static final String LOCAL_AWS_ROLE = "HB-DEV";

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

    @Value("${spring.profiles.default}")
    private String activeProfile;

    @Bean
    @Primary
    public AWSCredentialsProvider awsCredentialsProvider() {
        if (SUPPORTED_PROFILES.contains(activeProfile)) {
            return new ProfileCredentialsProvider(LOCAL_AWS_ROLE);
        }

        return new WebIdentityTokenCredentialsProvider();
    }

    @Bean
    public AmazonS3 amazonS3Client() {
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(awsCredentialsProvider())
                .build();
    }
}

 

(4) PreSignedUrl 을 생성하는 역할을 하는 서비스 개발

import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.UUID;

@Slf4j
@Component
@RequiredArgsConstructor
public class PreSignedUrlUtils {
    private final AmazonS3 amazonS3;

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

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

    public String getUploadPreSignedUrl(String prefix, String fileName) {
        String onlyOneFileName = onlyOneFileName(fileName);

        if (!prefix.equals("")) {
            onlyOneFileName = prefix + "/" + onlyOneFileName;
        }

        return getPreSignedUrl(bucket, onlyOneFileName, HttpMethod.PUT);
    }

    public String getDownloadPreSignedUrl(String key) {
        return getPreSignedUrl(bucket, key, HttpMethod.GET);
    }

    private String onlyOneFileName(String filename){
        return UUID.randomUUID() + "-" + filename;
    }

    private String getPreSignedUrl(String bucket, String key, HttpMethod httpMethod) {
        GeneratePresignedUrlRequest generatePresignedUrlRequest =
                new GeneratePresignedUrlRequest(bucket, key)
                        .withMethod(httpMethod)
                        .withExpiration(getPreSignedUrlExpiration());

        return amazonS3.generatePresignedUrl(generatePresignedUrlRequest).toString();
    }

    private Date getPreSignedUrlExpiration() {
        Date expiration = new Date();
        long expTimeMillis = expiration.getTime();
        expTimeMillis += 1000 * 60 * 2;  // 유효 시간 2분
        expiration.setTime(expTimeMillis);

        log.info(expiration.toString());

        return expiration;
    }

    public String findByName(String onlyOneFileName) {
        if (!amazonS3.doesObjectExist(bucket, onlyOneFileName))
            return "File does not exist";

        log.info("Generating signed URL for file name {}", onlyOneFileName);
        return amazonS3.getUrl(bucket, onlyOneFileName).toString();
    }
}

 

(5) Controller 개발

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class FileController {
    private final PreSignedUrlUtils presignedUrlUtils;

    @PostMapping("/file/upload/presigned")
    public ResponseEntity<String> createUploadPreSignedUrl(@RequestBody FileAccessDto dto) {
        String path ="contact/" + dto.bucketType();  //원하는 경로 지정
        return ResponseEntity.ok(presignedUrlUtils.getUploadPreSignedUrl(path, dto.fileName()));
    }

    @PostMapping("/file/download/presigned")
    public ResponseEntity<String> createDownloadPreSignedUrl(@RequestBody FileAccessDto dto) {
        return ResponseEntity.ok((presignedUrlUtils.getDownloadPreSignedUrl(dto.filePath()));
    }
}

 

4. 확인

이제 포스트맨을 통해 preSignedUrl을 얻고 파일을 업로드해보겠습니다.

 

서버에 요청을 보내 PreSignedUrl 얻기
위에서 얻은 PreSignedUrl로 파일 업로드하기

 

❗️만약 업로드 시 다음과 같은 에러가 발생한다면...

더보기

 

위와 같은 에러가 발생한다면 해당 버킷의 권한 > 객체 소유권을 편집하여 ACL을 허용해주어야 합니다.

 

 

 


이렇게 PreSignedUrl에 대해 알아보았습니다. 클라이언트에서 PreSignedUrl을 통해 업로드한 뒤 업로드한 정보를 서버로 보내 DB에 저장하는 과정이 추가로 필요합니다. 그래야 나중에 파일에 대한 정보를 조회하고 해당 정보를 바탕으로 다운로드가 가능한 PreSignedUrl을 생성할 수 있기 때문입니다. 이 부분은 해당 글에서 생략하였습니다.

 

감사합니다.

 

참고

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함