Presign URL

S3 버킷에 이미지를 저장할 때 이전 글에서 했던 방식대로 백엔드 버에서 SDK로 저장할 수 있었다.

하지만 이번 글에서는 조금 다른 방식으로 프론트엔드 버에서 이미지를 저장해보려 한다.

 

프론트 엔드 서버에서도 AWS SDK를 이용하면 이미지를 저장할 수 있다.

하지만 프론트 쪽은 브라우저의 개발자 도구로 민감한 정보를 보기 편하기 때문에 꺼려진다.

그래서 이번에 써볼건 Presign URL 방식인데, 간단히 설명하자면 S3 버킷에 이미지를 저장할 수 있는 권한을 가진 URL이다.

이 URL로 몇번이고 S3 버킷에 요청을 할 수 있기 때문에 유효시간을 지정하여 발급한다.

프리사인 URL의 흐름은 아래와 같다.

  1. 프론트에서 백엔드로 이미지 파일명을 보내서 프리사인 URL을 요청한다.
  2. 백엔드는 요청 받은 이미지 파일명으로 S3 버킷에 저장할 수 있는 프리사인 URL을 발급한다.
  3. 프론트에서 프리사인 URL에 PUT 요청으로 이미지를 저장한다.

사용 기술

  • Spring Boot 3.2.4 / gradle-kotlin
  • Java 17
  • Aws SDK

1. AwsConfig 설정

AwsConfig

S3 프리사이너를 빈 등록 한다.

Aws 관련 yml 설정은 이 글의 내용과 동일하다.

@RequiredArgsConstructor
@ConfigurationProperties(prefix = "aws")
public class AwsConfig {

    private final String accessKey;
    private final String secretKey;
    private final String s3AccessPoint;

    public String getS3AccessPoint() {
        return s3AccessPoint;
    }

    @Bean
    public S3Presigner s3Presigner() {
        return S3Presigner.builder()
            .credentialsProvider(getAwsBasicCredentials())
            .region(Region.AP_NORTHEAST_2)
            .build();
    }

    private StaticCredentialsProvider getAwsBasicCredentials() {
        return StaticCredentialsProvider.create(
            AwsBasicCredentials.create(accessKey, secretKey)
        );
    }
}

2. ImageService 구현

ImageService

사실 프리사인 URL이라고 별게 있는건 아니고 컨트롤러에서 받아온 filename 하나만 있으면 된다.

이 파일 이름을 UUID로 변환하든, 파일 이름 그대로 저장하든 편한대로 로직을 구성하면 된다.

나머지는 백엔드 환경 변수에 설정한 Aws 설정 값들로 프리사인 URL을 만든다.

아래처럼 프리사인 리퀘스트를 만들고 프리사인을 해서 프리사인 URL 을 만들고 응답 값으로 주면 된다.

여기서는 프리사인 URL을 PUT 메서드로 요청하게끔 설정하였다.

유효기간은 5분으로 주었다.

@Service
@RequiredArgsConstructor
public class ImageService {

    private final AwsConfig awsConfig;
    private final S3Presigner s3Presigner;

    public String getPresignedUrl(String filename) {
        if (filename == null || filename.isBlank()) {
            throw new IllegalArgumentException("파일명을 확인해주세요.");
        }
        return generatePresignedUrl(filename);
    }

    private String generatePresignedUrl(String filename) {
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
            .bucket(awsConfig.getS3AccessPoint())
            .key(filename)
            .build();

        PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
            .signatureDuration(Duration.ofMinutes(5))
            .putObjectRequest(putObjectRequest)
            .build();

        try (S3Presigner presigner = s3Presigner) {
            return presigner.presignPutObject(presignRequest)
                .url()
                .toString();
        }
    }
}

3. 결과 확인

3-1. 프리사인 URL 발급 요청

프리사인 URL을 생성하는 컨트롤러를 만들었다.

아래처럼 파일 이름을 요청 값으로 보내고 프리사인 URL을 발급 받았다.

요청 값에 입력한 파일명이 string 이므로 S3에 string이라는 이름으로 저장될 것이다.

3-2. 프리사인 URL로 PUT 요청

아래 사진 처럼 form-data 형식으로 PUT 메서드로 저장하고자 하는 이미지를 넣어 요청한다.

여기서 요청하는 이미지 파일의 이름이나 key 값은 의미가 없고 프리사인 URL 발급 시 지정한 이름으로 S3 버킷에 저장된다.

아래 사진 처럼 200번 응답이 오면 성공한것이다.

실패할 경우 에러 페이지가 출력된다.

3-3. S3 버킷 확인

아래처럼 string 이라는 이름으로 파일이 저장됨을 알 수 있다.

 


4. PostMan이 아닌 프론트에서 요청할 때

CORS는 웹 브라우저의 보안 기능이다.

웹 브라우저는 스크립트가 현재 페이지와 다른 도메인으로 요청을 보낼 때 이를 제한하기 위해 CORS 정책을 적용한다.

포스트맨과 같은 API 테스트 도구는 브라우저가 아니므로, CORS 정책을 적용하지 않는다.

따라서 위처럼 해도 CORS 에러가 발생하지 않는다.

 

하지만 프론트엔드와 연동하면 브라우저에서 요청하기 때문에 CORS 에러가 뜬다.

CORS 에러를 막기 위해서는 S3 버킷에서 권한 처리를 해줘야한다.

아래처럼 S3 버킷에 들어가서 권한 탭에서 CORS 설정을 해주도록 한다.

본인은 PUT 요청으로 프리사인 URL을 만들었으므로 아래처럼 설정해주었다.

S3에 이미지 저장

이전 글에서 S3 버킷을 만들어두었다.

이번 글에서는 S3에 이미지를 저장하는 방법을 정리해보려 한다.

최근 알게 된 Presigned URL 방식으로 프론트에서 이미지를 저장하는 방식도 있지만

이번 글에서는 프론트에서 MultipartFile 형태로 이미지를 받아서 저장하는 방식으로 해보았다.

Presigned URL 방식은 추후 따로 글 쓸듯.


사용 기술

  • Spring Boot 3.2.4 / gradle-kotlin
  • Java 17
  • Aws SDK

1. Aws SDK 관련 설정

Aws SDK 의존성 추가

// build.gradle.kts
implementation(platform("software.amazon.awssdk:bom:2.25.35"))
implementation("software.amazon.awssdk:s3")

 

Aws SDK를 사용하는 방법은 두가지가 있다.

  • BOM을 이용한 개별 의존성 추가 방법
  • SDK 전체 추가

본인은 어차피 S3만 쓸것 같기 때문에 S3만 추가하였다.

application.yml 설정

액세스 키, 시크릿 키를 확인하는 방법은 이 글을 확인하자.

이전 글의 마지막에서 만든 액세스 포인트를 활용한다.

aws:
  access-key: # AWS 액세스 키
  secret-key: # AWS 시크릿 키
  s3-access-point: # S3 액세스 포인트

AwsConfig 설정

이미지 업로드에 사용할 S3Client를 빈 등록해준다.

@RequiredArgsConstructor
@ConfigurationProperties(prefix = "aws")
public class AwsConfig {

    private final String accessKey;
    private final String secretKey;
    private final String s3AccessPoint;

    public String getS3AccessPoint() {
        return s3AccessPoint;
    }

    @Bean
    public S3Client s3Client() {
        return S3Client.builder()
            .credentialsProvider(getAwsBasicCredentials())
            .region(Region.AP_NORTHEAST_2)
            .build();
    }

    private StaticCredentialsProvider getAwsBasicCredentials() {
        return StaticCredentialsProvider.create(
            AwsBasicCredentials.create(accessKey, secretKey)
        );
    }
}

2. S3Client를 이용하여 이미지 업로드

ImageController

@RequestParam이나 @RequestPart 애노테이션을 이용해서 MultipartFile을 받는다.

@ModelAttribute로 객체로 받아도 무관하다.

@RestController
@RequiredArgsConstructor
public class ImageController {

    private final ImageService imageService;

    @PostMapping("/upload")
    public String uploadFile(@RequestPart MultipartFile file) throws IOException {
        imageService.upload(file);
        return "이미지 저장 성공";
    }
}

ImageService

컨트롤러에서 받은 MultipartFile을 서비스 레이어에서 처리한다.

S3Client.putObject() 메서드가 받는 인자는 PutObjectRequest, RequestBody 두 개이다.아래처럼 MultipartFile의 inputStream으로 RequestBody를 만들어주면 되고key에는 S3에 저장할 파일 이름을 넣어주면 된다.PutObject라는 이름처럼 같은 이름의 파일이라면 덮어 씌우므로 여러 사람이 이미지를 올린다면 UUID 형태로 저장하는게 좋다.

@Service
@RequiredArgsConstructor
public class ImageService {

    private final S3Client s3Client;
    private final AwsConfig awsConfig;

    public void upload(MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();
        RequestBody requestBody = RequestBody.fromInputStream(file.getInputStream(), file.getSize());

        s3Client.putObject(builder -> builder
                .bucket(awsConfig.getS3AccessPoint())
                .key(fileName)  // S3에 저장할 파일명
            , requestBody);
    }
}

3. 결과 확인

PostMan 으로 이미지를 저장해보자.

MultipartFile 형태로 데이터를 받으므로 form-data로 요청을 보내야한다.

S3 버켓을 확인하면 이미지도 제대로 저장됨을 알 수 있다.

이미지 저장소

팀 프로젝트 주제가 중고 마켓이기 때문에 유저 프로필 사진이나 상품 사진 등을 넣어야한다.

따라서 이 이미지를 저장할 이미지 저장소가 따로 필요하게 되었다.

추후 바뀔수도 있겠지만 우선은 S3 이미지 저장소를 만들어보려고 한다.


1. 사전작업 - IAM 사용자 생성

IAM 사용자는 이전 글에서 만들어뒀던 사용자를 쓰려고 한다.

이 사용자는 S3, CodeDeploy에 두 서비스에 대한 권한이 있지만 당장은 역할 분리를 할 필요는 없을것 같다.


2. 이미지 저장용 S3 버킷 생성

이미지 저장용 S3 버킷은 CI/CD용 S3 버킷 생성 때와는 과정이 조금 다르다.

CI/CD 때 썼던 버킷은 CodeDeploy에서 내부적으로 처리하므로 외부 접근을 생각할 필요가 없었다.

하지만 이미지는 이미지 URL로 외부에서 조회해야하므로 퍼블릭 엑세스를 열어둬야 한다.

2-1. S3 버킷 생성 페이지로 이동

2-2. 버킷 만들기

아래처럼 리전과 이름을 설정한다.

 

S3에 저장한 이미지를 외부에서 볼 수 있어야 하므로 퍼블릭 엑세스를 열어둔다.

2-3. 버킷 생성 확인


3. S3 버킷 권한 설정

3-1. S3 버킷 권한 탭으로 이동

생성한 이미지 저장용 S3 버킷으로 이동하고 권한 탭으로 이동한다.

외부에서 S3 버킷에 저장된 리소스를 조회하기 위해서는 액세스를 허용해줘야 한다.

여기서 모든 퍼블릭 액세스 차단이 비활성화 됐는지 확인한다.

3-2. 버킷 정책 수정

버킷 정책을 수정해야한다.

아래와 같은 json 형식을 입력하면 외부에서 리소스 조회를 허용할 수 있다.

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "PublicReadGetObject",
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:GetObject",
			"Resource": "{버킷 ARN}/*"    // ARN 뒤에 /* 을 붙여야함!
		}
	]
}

 

참고로 버킷 ARN은 편집하기를 누르면 편집창 위에서 확인할 수 있다.


4. 액세스 포인트

추가로 AWS SDK로 애플리케이션에서 S3 버킷에 접근할 때 여러 방법이 있겠지만

그 중 액세스 포인트로 접근하려 한다.

4-1. 버킷 선택 후 액세스 지점 탭 이동

4-2. 액세스 포인트 생성

아래처럼 액세스 포인트 이름과 네트워크 오리진만 변경하고 기본값으로 생성한다.

여기서는 모든 퍼블릭 엑세스를 차단한다고 설정 돼있는데, 버킷이 모든 퍼블릭 엑세스를 허용해서 그런지 기본 값으로 해도 상관 없는듯 하다.

디테일 한 설정이 필요하다면 추가로 설정해주면 될듯.

4-3. 액세스 포인트 복사

ARN 주소는 아래처럼 액세스 포인트를 선택하고 복사할 수 있다.

이 액세스 포인트는 추후 백엔드 코드에서 사용한다.

+ Recent posts