현재 상황
현재 진행 중인 프로젝트에서, 이미지 저장을 위한 객체 저장소로 AWS S3를 이용하고 있습니다.
그리고 클라이언트가 서버로 MultipartFile을 이용해서 이미지를 전송하고, 서버에서는 받은 파일을 S3로 업로드하고 리턴된 URL을 저장 후 클라이언트에게 전달합니다.
이미지를 포함한 요청을 서버로 보내게 되면 네트워크적으로 일반 json을 주고받는 요청보다 부하가 크다고 생각합니다. 텍스트보다 파일 용량이 훨씬 크기 때문입니다.
그렇다면 왜 서버를 거쳐야 할까?
클라이언트에서 바로 S3에 접근한다면 보안 작업 없이 서비스의 파일에 접근해서 삭제 혹은 업로드를 할 것입니다. 또한, 클라이언트에서 접근을 위한 key 등을 갖고 있는다면 탈취될 위험도 있습니다.
따라서 미리 서명된 URL이라는 뜻의 presignedURL을 이용하게 되면 서버에서 관리되고 있는 key를 이용해서 AWS와 통신 후 presignedURL만 발급해서 리턴해줍니다. 이후 클라이언트는 해당 URL을 받아서 바로 S3로 업로드할 수 있게 됩니다. 또한, 이 URL은 정해둔 시간이 지나면 만료가 됩니다.
저는 이번에 스프링에서 presignedURL을 이용해서 업로드하는 케이스를 정리해보려고 합니다.
1. AWS S3 세팅 (다루지 않음) 후 받은 bucket명, region, key를 application.yml에 세팅합니다.
2. Amazon S3 Client 객체를 생성하기 위한 S3 Config.java 파일을 작성합니다.
3. S3FileUploader라는 클래스와 presignedURL을 받기 위한 메서드를 작성합니다.
4. 해당 메서드를 이용해서 presignedURL 테스트를 위한 Controller를 작성합니다.
5. Postman 요청을 통해 리턴된 presignedURL을 확인합니다.
6. Postman을 통해 해당 URL에 이미지를 업로드해봅니다. (그래서 클라이언트는 해당 URL을 어떻게 이용해야 하나?)
1. AWS와 S3 세팅
우선 AWS에 S3 버킷이 있어야 하고, 해당 버킷을 접근하기 위한 권한 및 aws credential access-key와 secret-key 설정이 필요합니다. 콘솔에서 버킷 생성 및 퍼블릭 접근 권한 설정은 생략하겠습니다.
이후 발급받은 정보들을 application.yml에 입력해줍니다.
cloud:
aws:
region:
static: 리전
credentials:
access-key: access-key
secret-key: secret-key
instance-profile: true
s3:
bucket: S3 생성 시 만든 버킷 이름
2. PresignedURL을 생성하기 위한 코드
2-1. S3Config.java
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
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;
@Configuration
@Slf4j
public class S3Config {
@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 awsCredentials = new BasicAWSCredentials(accessKey,secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
S3를 사용하기 위한 설정 클래스입니다.
2-2. S3FileUploader.java
import com.amazonaws.HttpMethod;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.Headers;
import com.amazonaws.services.s3.model.CannedAccessControlList;
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 org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
@Component
@RequiredArgsConstructor
@Slf4j
public class S3FileUploader {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
public String getPreSignedUrl(String dirName, String fileName) {
if (!dirName.equals("")) {
fileName = dirName + "/" + fileName;
}
GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(fileName);
URL url = amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString();
}
private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String fileName) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(HttpMethod.PUT)
.withExpiration(getPreSignedUrlExpiration());
generatePresignedUrlRequest.addRequestParameter(
Headers.S3_CANNED_ACL,
CannedAccessControlList.PublicRead.toString());
return generatePresignedUrlRequest;
}
private Date getPreSignedUrlExpiration() {
Date expiration = new Date();
long expTimeMillis = expiration.getTime();
expTimeMillis += 1000 * 60 * 2;
expiration.setTime(expTimeMillis);
log.info(expiration.toString());
return expiration;
}
}
저는 S3FileUploader라는 클래스를 생성해서, 해당 메서드를 작성했습니다.
public 메서드인 getPreSignedUrl을 이용하면 클라이언트가 S3에 접근해서 이미지를 올릴 수 있는 서명된 URL이 반환됩니다.
3. 실습을 위한 Controller 작성
스프링에서 URL요청을 받아 presignedURL을 반환하고, 클라이언트에서 해당 URL을 이용해서 이미지를 업로드해보겠습니다. 스프링의 기본 세팅은 생략하고, Controller 코드에 위에서 작성한 메서드를 호출해보겠습니다.
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class TestController {
private final S3FileUploader s3FileUploader;
@GetMapping("/signedURL")
public ResponseEntity<String> getPresignedUrl() {
return ResponseEntity.ok(s3FileUploader.getPreSignedUrl("test", "dog.jpeg"));
}
}
위에서 언급한 S3Config와 S3FileUploader를 잘 작성하셨으면 오류가 나지 않을 것입니다.
파라미터는 S3 버킷 내의 폴더 경로와 올리려는 이미지 이름입니다.
저의 경우 버킷명/test 폴더가 있으므로 파라미터로 "test", 파일명은 "dog.jpeg"이므로 위와 같이 전달해줬습니다.
저장하려는 폴더 명과, 테스트하려는 파일 명을 변경해주시면 됩니다.
단순 동작 테스트를 위한 코드이므로 서버 로직에 맞게 잘 사용하면 될 것 같습니다.
4. 요청 보내기
서버를 run 시키고 postman을 통해 요청을 보내봅니다.
GET 요청으로, URL Path는 http://localhost:8080/api/v1/signedURL로 요청을 보냈습니다.
위와 같이 요청으로 presignedURL이 리턴됩니다.
5. 해당 presignedURL을 이용해서 사진 업로드해보기
AWS 공식 깃헙에서 Upload 부분 코드를 살펴봤습니다.
public void uploadContentsWithPreSignedUrl(URL url, MultipartFile multipartFile) throws IOException {
// Create the connection and use it to upload the new object by using the presigned URL.
byte[] pic = multipartFile.getBytes();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type","image/png");
connection.setRequestMethod("PUT");
connection.getOutputStream().write(pic);
connection.getResponseCode();
System.out.println("HTTP response code is " + connection.getResponseCode());
}
파라미터로 원래 byte []이 입력되어 있었는데, 제가 MutipartFile로 변경했습니다.
위의 메서드를 보면, PUT 메서드를 이용해서 Content-Type은 image/png로 보내면 될 것 같습니다.
즉, POSTMAN을 이용한다면 PUT 메서드로 presignedURL에 MultipartFile 형식으로 파일을 전송하면 됩니다.
위의 URL에 발급받은 presignedURL을 입력 후, PUT Method를 이용합니다.
Body에서 form-data -> KEY에서 File로 변경해주시고 아까 입력한 파일명과 동일한 파일을 업로드하면 됩니다.
위와 같이 요청을 보냈다면, 200 OK와 함께 S3에 파일이 업로드된 것을 확인할 수 있습니다.
6. 업로드된 파일 접근 URL
정상적으로 업로드됐다면, 파일에 접근하는 URL은 "https://버킷이름.s3.region.amazonaws.com/폴더명/파일명"입니다.
presignedURL은 위의 형식 + 서명된 URL임을 확인할 수 있는 여러 가지 queryParams로 이루어져 있습니다.
따라서 발급받은 presignedURL에서 쿼리 파라미터를 나타내는 "?" 이후부터 다 지우시면 객체가 저장된 URL을 확인할 수 있습니다.
https://버킷명.s3.ap-northeast-2.amazonaws.com/폴더명/파일명?x-amz-acl=&X-Amz-Algorithm=&X-Amz-Date=&X-Amz-SignedHeaders=&X-Amz-Expires=&X-Amz-Credential=
위의 형식에서 파일명 ? 이후 모든 쿼리 파라미터를 제거하면 저장된 위치!
Reference
https://www.inflearn.com/questions/286989
https://gksdudrb922.tistory.com/223
'개발 공부 > 스프링' 카테고리의 다른 글
[SpringBoot x JPA] Soft Delete #2 Insert (중복, Unique 제약조건, 인덱스의 관점에서) (0) | 2022.10.27 |
---|---|
[Junit5, Mockito] Mockito를 이용해서 Void 메서드 Mocking하기 (0) | 2022.10.05 |
[Github Actions] CI 스크립트에 Redis 환경 추가하기 (0) | 2022.10.02 |
[SpringBoot x JPA] Soft Delete #1 Select (해당 Status 조건 추가 조회 방법) - @Where (1) | 2022.09.11 |
[SpringBoot x JPA] List 초기화에서 Builder 패턴 사용 시 NullPointerException (0) | 2022.08.08 |