몇개월에 걸쳐 진행된 TIFY 프로젝트도 끝을 향해 나아가는데,
그간 이런 핑계 저런 핑계를 대다가
정말 오랜만에 블로그를 작성한다.
어느새 개발이 거의 다 진행되어
QA 마무리 단계까지 온 탓에
블로그를 쓸 것들이 산더미,,
S3 Properties 리팩토링 하는 김에
나태해지지 않기로 했으니 열심히 써보자...o_O!
기획, 디자인, 프론트 팀원들과 QA를 진행하다 보니
꽤나 대용량의 이미지 파일을 관리해야하는 상황이 있다는 것을 인지했다.
열정 넘치고 실력있는 디자이너 팀원들의 노력의 산물이
3D까지 구현된 것이다 보니 용량이 크더라..
이전 프로젝트에서 NCP Object Storage를 이용해 보았기 때문에
다시 한번 그런 방식으로 AWS의 서비스를 찾아 본 결과
S3라는 서비스를 찾을 수 있었다.
NCP Object Storage 포스팅 ⬇️
[NCP] Multipart 파일 업로드 with Object Storage
피카소 프로젝트 자체가 미술 경매 사이트였기 때문에 파일 업로드는 필수 불가결한 기능이다. 필자의 팀은 서비스에 올라오는 모든 사진을 NCP의 Object Storage에 올려놓고 관리하기로 하였다. 만
dev-sh-bong.tistory.com
S3 Presigned URL 적용 초반 커밋 ⬇️
feat : s3 설정 3트 · Team-TIFY/TIFY-SERVER@fdf0062
bongsh0112 committed Dec 29, 2023
github.com
S3 Presigned URL 리팩토링 pr ⬇️
[REFACTOR] s3 properties 수정 by bongsh0112 · Pull Request #151 · Team-TIFY/TIFY-SERVER
📝 PR Summary S3 Properties 수정 🌲 Working Branch refactor/144-s3-prop 🌲 TODOs Related Issues close #144
github.com
0) Presigned URL
NCP Object Storage를 사용할 때는
서버에서 직접 우리가 만든 NCP 버킷에 multipart 파일을 전송했다.
용량이 큰 파일을 이런 방식으로 올리는 것에 대해 찾아보니
용량이 작은 파일을 올릴 때는 문제가 없을지 모르나
스프링의 MultifileResolver는 10MB가 넘어가는 파일을 처리할 때
성능 저하를 일으킨다는 것을 알았다.
이를 위해 AWS에서 제공하는 S3 Presigned URL을 이용하기로 했다.
0-1) Presigned URL이란?
말그대로 미리 서명된 URL이다.
클라이언트가 파일의 정보(파일 이름, 파일 확장자, 용량)를 서버에 넘겨주면
서버에서는 S3 서버와 통신(이 과정에서 S3 서버가 허가 비슷하게 서명을 하는 것)하여
파일에 맞게 파일이 저장될 url을 받아온다.
클라이언트는 이렇게 미리 허가된(서명된) url에
요청을 보내어 파일을 업로드 할 수 있다.
즉, 서버에서 파일을 직접 업로드하는 방식과는 다르게
서버에서는 파일을 업로드 할 url만 클라이언트에 제공하고
클라이언트 단에서 한번에 S3 버킷에 파일을 업로드할 수 있게 되는 것이다.
1) S3 버킷 생성
구글링을 하여 S3 버킷을 생성하는 포스팅들을 보면
보통 IAM 유저를 만들어서 하는데,
우리는 어차피 백엔드 두명만 신경쓰면 되기 때문에
루트 권한의 사용자만 사용할 수 있도록 설정하기로 했다.
1-1) 액세스 키, 시크릿 키 발급
루트 사용자의 액세스 키와 시크릿 키를 발급받기 위해 보안 자격 증명 메뉴에서
액세스 키 만들기를 실행한다.
IAM 사용자라면 모를까 루트 사용자의 키들이기 때문에
반드시 잘 보존해야한다. 나중에 .env 파일에도 들어가야 한다.
1-2) 버킷 생성
버킷을 만들 때 나오는 설정 화면이다.
첫 번째 사진에서는 버킷의 이름을 설정하고
버킷에 대한 접근 제한 목록 활성화/비활성화를 결정한다.
두 번째 사진에서는 버킷의 public/private 상태를 결정한다.
한번 만들어보니 처음에는 이것을 public으로 설정하고 만드는 것이 편하다.
나중에 private으로 바꿀 수 있으니 public으로 먼저 열어두고
설정할 것들 모두 설정한 다음 private으로 바꿔보도록 하자.
밑에 나오는 것들은 건드리지 않았다.
1-3) 버킷 설정
위 단계를 거쳐 버킷이 만들어지면 이렇게 확인이 가능하다.
버킷 설정을 위해 들어온 화면이다.
위에서 말한 버킷 public/private 설정을 퍼블릭 액세스 차단(버킷 설정)에서 할 수 있다.
우리가 봐야할 부분은 버킷 정책과 CORS 설정이다.
편집 버튼을 눌러서 조금 해본 결과,
버킷 정책에서는 버킷의 버전을 설정하고
Statement에 정책 문장들을 집어넣어
허가할 액션과 불허할 액션을 정하며
AWS 측에서 정해놓은 규율(Principal)을 넣을 수 있어 보였다.
Resource는 당연히 우리 버킷 안의 파일들이겠거니..
CORS 설정도 중요해보였는데,
어차피 우리 버킷을 다시 private으로 돌릴 것이고
버킷을 쓰는 것도 우리 프로젝트밖에 없을 것 같아
와일드카드로 거의 모든 것을 허용해주었다.
이후 퍼블릭 액세스 차단에서 항목을 모두 체크하면 끝!
2) S3 Properties 설정
2-1) ConfigurationPropertiesConfig
@EnableConfigurationProperties({JwtProperties.class, OauthProperties.class, S3Properties.class})
@Configuration
public class ConfigurationPropertiesConfig {}
S3Properties를 사용하기 위해 클래스정보를 제공한다.
2-2) application-core.yml & S3 Properties
cloud:
aws:
credentials:
accessKey: ${S3_ACCESS_KEY}
secretKey: ${S3_SECRET_KEY}
s3:
bucket: ${S3_BUCKET_NAME}
region:
name: ${S3_REGION}
static: ap-northeast-2
stack:
auto: false
properties를 Core 모듈에다 모아놓았으니,
application-core.yml에 사용해야 할 값들을 넣어놓았다.
S3에 관련된 정보들은 .env 파일에 고이 모셔두고..
@Getter
@AllArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "cloud.aws")
public class S3Properties {
private Credentials credentials;
private S3 s3;
private Region region;
@Getter
@Setter
public static class Credentials {
private String accessKey;
private String secretKey;
}
@Getter
@Setter
public static class S3 {
private String bucket;
}
@Getter
@Setter
public static class Region {
private String name;
}
public String getAccessKey() {
return credentials.getAccessKey();
}
public String getSecretKey() {
return credentials.getSecretKey();
}
public String getBucketName() {
return s3.getBucket();
}
public String getRegion() {
return region.getName();
}
}
application-core.yml의 형식에 맞게 S3Properties를 구현해주었다.
맨 처음 작업 할 때는
application.core.yml의 cloud 관련 설정(그 중에서도 stack: auto: false)을 잡아주지 않아
S3Properties에서 yml 파일의 값을 끌어오는게 되지 않았다.
이에 관해서는 더 많은 공부가 필요할 것 같은데,
우선 구현이 급하니 잠깐 넣어두기로...
2-3) S3Config & S3Service
S3 서버와의 통신을 위한
AmazonS3Client에 필요한 설정이다.
우리 서버가 아닌 다른 서버와의 통신이 필요한 부분이기 때문에
Infrastructure 모듈에 구현한다.
@Configuration
@RequiredArgsConstructor
public class S3Config {
private final S3Properties s3Properties;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCredentials =
new BasicAWSCredentials(s3Properties.getAccessKey(), s3Properties.getSecretKey());
return (AmazonS3Client)
AmazonS3ClientBuilder.standard()
.withRegion(s3Properties.getRegion())
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
AmazonS3Client를
우리 S3 버킷의 설정에 맞추어 Bean으로 등록해놓고
실제 S3 서비스를 사용하는 Service 객체에서 사용할 것이다.
@Service
@RequiredArgsConstructor
public class S3Service {
private final AmazonS3 amazonS3Client;
private final S3Properties s3Properties;
public PreSignedDTO getPreSignedUrl(Long userId, FileExtension fileExtension) {
String uuidString = UUID.randomUUID().toString();
String fileName = userId + "-" + uuidString + fileExtension.getValue();
return generatePreSignedUrl(fileName);
}
private PreSignedDTO generatePreSignedUrl(String fileName) {
Date date = new Date();
long time = date.getTime();
time += 1000 * 60 * 30;
date.setTime(time);
String bucket = s3Properties.getBucketName();
try {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(bucket, fileName)
.withMethod(PUT)
.withExpiration(date);
generatePresignedUrlRequest.addRequestParameter(
Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString());
URL url = amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest); // S3와 통신하는 부분
return new PreSignedDTO(url.toString(), fileName);
} catch (NullPointerException e) {
throw FeignException.EXCEPTION;
}
}
}
위 S3Config에서 Bean으로 등록해놓은 AmazonS3의
AmazonS3Client를 사용하여 만든
S3Service이다.
@Getter
@AllArgsConstructor
public class PreSignedDTO {
private String imageUrl;
private String name;
}
이 Service 객체에서는 유저의 pk값(id)과 파일의 확장자를 받아
그 파일이 저장될 url를 만들고,
그 url을 S3 서버에 보내어 서명을 받을 수 있도록 하며
PresignedDTO에 url과 파일의 이름을 넣어 리턴해준다.
2-4) ImageController
@Getter
@NoArgsConstructor
public class PreSignedUrlRequest {
@Schema(description = "파일의 확장자입니다.", implementation = FileExtension.class)
@NotNull(message = "파일의 확장자를 입력하세요.")
private FileExtension fileExtension;
}
@RestController
@RequiredArgsConstructor
@Tag(name = "8. [이미지]")
@RequestMapping(value = "/images")
@SecurityRequirement(name = "access-token")
public class ImageController {
private final S3Service s3Service;
@Operation(summary = "presigned url을 확인합니다.")
@PostMapping
public PreSignedDTO getPreSignedUrl(@RequestBody @Valid PreSignedUrlRequest request) {
return s3Service.getPreSignedUrl(
SecurityUtils.getCurrentUserId(), request.getFileExtension());
}
}
Presigned Url을 받아볼 수 있는 컨트롤러이다.
FileExtension은 필자가 임의로 만든 enum 객체로,
파일의 확장자명이 들어있다.
이 api를 사용하는 사용자가 등록하는 파일이므로
현재 로그인된 유저의 pk값을 전달하고,
사용자는 업로드하고자 하는 파일의 확장자를 request에 넣어
POST 요청을 한다.
POST 요청으로 설정한 이유는
사용자가 결국 PreSignedDTO를 리턴받지만,
S3 서버에 url 승인 요청이 들어가기 때문에
조금 애매하지만 POST로 설정했다.
3) 결과
id=5인 내가 PNG 파일 업로드를 위해
Presigned Url를 받아오는 모습이다.
S3Service에서 설정한 대로
파일의 name에 유저의 id값과
파일의 확장자명이 들어간다.
또 url에는 설정한 bucket의 이름이 잘 들어가는 것을 볼 수 있다.
클라이언트가 저 url로 파일과 함께 PUT 요청을 넣으면
버킷에 파일이 업로드 될 것이다.
[참고 사이트]
'🎉 프로젝트 > 🎁 TIFY' 카테고리의 다른 글
[TIFY] 검색 기능 성능 개선 with DB index (2) | 2024.01.28 |
---|---|
[TIFY] Apple Login 구현 시 OIDC 사용하기 (1) | 2024.01.16 |
[TIFY] Selenium을 이용한 올리브영 크롤링 in SpringBoot (0) | 2023.08.23 |
[TIFY] Slack WebHook으로 Spring 500에러 알림 받기 (0) | 2023.07.04 |