개요
이전에 진행했던 프로젝트에서 사진을 업로드하고, 이를 조회에서 사용자의 프로필을 보여줘야하는 테스크가 있었다.
단순히 로컬단에 사용자의 이미지를 조회하고 그 내용을 보여줘도 해당 기능을 수행하는 데는 아무런 문제가 존재하지 않는다.
하지만, 사용자가 본인의 프로필을 바꾼다면? 자신의 애완 동물의 사진을 추가한다면? 이런 저런 이유들로 로컬단의 용량을 많이 잡아먹게된다. 또한, 보안적으로 취약할 수도 있다는 생각이 들었다. 그래서 다른 사람들은 이러한 작업들을 어떻게 수행할까해서 찾아보니, AWS의 Simple Storage Service(S3)를 사용해서 영상 또는 사진 데이터들을 처리하는 것을 알 수 있었다.
본론
AWS S3에 대한 내용은 추후에 다루게 될 것이다. 해당 포스팅에서는 단순히 AWS S3와 SpringBoot3를 연동하는 내용을 다룰 것이다.
AWS S3 Bucket 생성하기
우선 사용하고자 하는 S3 버킷을 만들자
여기서는 "범용"을 선택하고 넘어가면 된다. 다른 것들은 거들지 않아도 된다.
기본적으로 ACL이 비활성화 되어있는데, 추후에 URL로 받은 링크로 이미지를 보려면, ACL을 비활성화 해야한다.
만약 비활성화 하더라도 나중에 바꿀 수 있지만, 귀찮으니까 활성화 하고 넘어간다.
초기에는 "모든 퍼블릭 액세스 차단"이 활성화 되어 있을텐데, 이 부분을 체크 해제해준다.
이렇게 설정해주어야, 나중에 이미지나 영상을 조회할 때, 정상적으로 조회가 가능하다.
다음으로 아래에 뜬 주의문 안에 있는 체크박스를 눌러 퍼블릭으로 설정해준다.
이 다음으로는 설정하지말고, 생성을 눌러 버킷을 만들면 버킷을 정상적으로 생성이 완료될 것이다.
AWS S3 Bucket 권한 - 버킷 정책 수정
이렇게 AWS S3 Bucket을 생성했는데, 추가적으로 다른 부분들도 설정을 해주어야한다.
바로 해당 버킷의 정책을 수정해주어야하는데, 기본값으로는 해당 부분이 비어있다.
편집을 눌러 편집 페이지로 넘어간 다음,
다음과 같이 입력해준다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Principal": "*",
"Effect": "Allow",
"Action": "s3:*",
"Resource": "arn:aws:s3:::<버킷 이름>/*"
}
]
}
이렇게 입력하고, 편집한 내용을 저장한다.
그리고 만약, 위에서 ACL을 활성화 하고 싶다면, 객체 소유권을 편집해주면 된다.
AWS IAM 계정 추가
S3를 AWS 서비스 내부가 아닌 곳에서 사용하기 위해서 IAM을 생성해야한다.
IAM은 Identity and Access Manager의 약자로, 증명된 사용자를 의미한다. 어드민 계정에서 사용자를 만들고 특정 권한을 부여하면
해당 IAM 계정을 통해서 S3에 업로드 및 다운로드를 수행할 수 있다.
그러면 위와 같은 페이지가 보일 것이다.
사용자 생성을 눌러 사용자를 만들어 주어야한다
사용자 이름만 작성하고 다음버튼을 눌러 생성해주자.
다른 내용들은 무시해도 된다.
그럼 이렇게 사용자가 생성된 것을 확인할 수 있다.
세부 페이지로 접근한다.
해당 유저의 세부 페이지이다.
아직까진 해당 유저에게 어떠한 권한도 부여되지 않은 것을 확인할 수 있다.
이제 해당 유저를 통해서 Springboot3에서 AWS S3에 접근할 것이므로, 관련된 권한들을 부여한다.
직접 정책 연결을 클릭한다.
여기서 AmazonS3FullAccess 권한을 체크하고 다음 페이지로 넘어간다.
검토 페이지로 넘어가서도 생성 페이지를 눌러 권한을 부여하면 된다.
AWS IAM secret key와 access key 발급
유저의 상세 페이지에서 액세스 키 만들기를 클릭한다.
우리는 Spring Boot3에서 S3에 접근하게 될 것이므로, AWS 외부에서 실행되는 애플리케이션을 체크해주고 다음으로 넘어간다.
설명 태그 값은 적어줘도 되고 안적어도 된다.
이렇게 액세스 키와 비밀 액세스 키가 생성이 된다. 반드시 이 2개의 Key를 잃어버리면 안된다.
잃어버리는 순간 비용 폭탄을 맞을 수도 있다.
또한, 시크릿 키를 까먹으면 2번다시는 볼 수 없음으로, 반드시 .csv 파일로 저장하자.
Springboot에서 Dependecy 추가하기
dependencies {
...
//AWS
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
...
}
우선, build.gradle에서 해당 코드를 추가하고 코끼리를 흔들어주자.
그러면, AWS 관련 의존성들이 적용될 것이다.
만약 버전때문에 문제가 발생한다면, 2.2.6.RELEASE를 지우고 다시 해보면 자동으로 버전이 맞게 될 것이다.
ENV 파일 추가하기
## application.yml
spring:
servlet:
multipart:
max-file-size: 50MB
max-request-size: 50MB
cloud:
aws:
s3:
bucket: {만든 버킷 이름}
region:
static: {생성한 region}
auto: false
stack:
auto: false
credentials:
access-key: {발급받은 access key}
secret-key: {발급받은 secret key}
위와 같이 application.yml 파일을 작성해 주면 된다.
만약, application.properties 인 경우, cloud.aws.s3.bucket = {만든 버킷 이름}
이런 식으로 작성하면 된다.
AWS S3 Config
Springboot3에서 S3를 사용하기 위해서 AWS config를 Bean으로 등록해야하는데, 다음과 같이 등록하면 된다.
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class S3Config {
@Value("${AWS.SECRET_KEY}")
private String secretKey;
@Value("${AWS.ACCESS_KEY}")
private String accessKey;
@Value("${AWS.REGION}")
private String region;
@Bean
public AmazonS3 amazonS3Client() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.enablePathStyleAccess()
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
이렇게 적게 되면, 해당 secretKey와 accessKey를 가지고, 해당 region에 접속하여, 위의 과정에서 생성한 IAM 유저로 접속하게된다.
AWS S3 Service
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Optional;
@Slf4j
@Service
public class S3Service{
@Autowired
private AmazonS3 amazonS3;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final String DIR_NAME = "pet_picture";
public String upload(String fileName, MultipartFile multipartFile, String extend) throws IOException { // dirName의 디렉토리가 S3 Bucket 내부에 생성됨
File uploadFile = convert(multipartFile)
.orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File 전환 실패"));
return upload(fileName, uploadFile, extend);
}
private String upload(String fileName,File uploadFile,String extend) {
String newFileName = DIR_NAME + "/" + fileName+extend;
String uploadImageUrl = putS3(uploadFile, newFileName);
removeNewFile(uploadFile); // convert()함수로 인해서 로컬에 생성된 File 삭제 (MultipartFile -> File 전환 하며 로컬에 파일 생성됨)
return uploadImageUrl; // 업로드된 파일의 S3 URL 주소 반환
}
private String putS3(File uploadFile, String fileName) {
amazonS3.putObject(
new PutObjectRequest(bucket, fileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead) // PublicRead 권한으로 업로드 됨
);
return amazonS3.getUrl(bucket, fileName).toString();
}
private void removeNewFile(File targetFile) {
if(targetFile.delete()) {
log.info("파일이 삭제되었습니다.");
}else {
log.info("파일이 삭제되지 못했습니다.");
}
}
private Optional<File> convert(MultipartFile file) throws IOException {
log.info(file.getOriginalFilename());
File convertFile = new File(file.getOriginalFilename()); // 업로드한 파일의 이름
if(convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
public ResponseEntity<byte[]> download(String fileName) throws IOException {
S3Object awsS3Object = amazonS3.getObject(new GetObjectRequest(bucket, DIR_NAME + "/" + fileName));
S3ObjectInputStream s3is = awsS3Object.getObjectContent();
byte[] bytes = s3is.readAllBytes();
String downloadedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.IMAGE_JPEG);
httpHeaders.setContentLength(bytes.length);
httpHeaders.setContentDispositionFormData("attachment", downloadedFileName);
return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
}
}
AWS S3 Controller
import fitapet.backend.fit_a_pet.service.S3Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
@RestController
@Slf4j
public class S3Controller {
@Autowired
private S3Service s3Service;
@PostMapping(path = "/teams", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity uploadPetImage(
@RequestPart(value = "fileName") String fileName,
@RequestPart(value = "file", required = false) MultipartFile multipartFile
) throws IOException {
String extend = multipartFile.getOriginalFilename().substring(multipartFile.getOriginalFilename().lastIndexOf("."));
String url = s3Service.upload(fileName,multipartFile,extend);
log.info(url);
return new ResponseEntity(url,null, HttpStatus.OK);
}
@GetMapping(path = "/teams/{fileName}")
public ResponseEntity<byte[]> getPetImage(
@PathVariable String fileName
) throws IOException {
return s3Service.download(fileName);
}
}
간단하게 MultipartFile을 File로 형변환하고, 로컬에 잠시 저장한 후, S3로 업로드하는 POST 요청과 특정 파일을 다운로드할 수 있는 GET 요청을 만들었다.
테스트
POST Method - S3 Upload
기존에 작성했었던 Swagger Docs가 있어서, 그거로 테스트해보자한다.
Controller에서 POST 메서드를 작성할 때,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
이렇게 작성했었는데, 스웨거에서 테스트할 때 Multipart form 데이터를 받기위해서 설정해주었다.
위의 코드들을 잘 작성했다면, 이렇게 2개의 Swagger 문서가 추가되었을 것이다.
만약 Swagger를 사용하지 않는다면, 번거롭더라도 PostMan같은 Utility를 사용해서 테스트해야한다.
Controller에서 선언한 것처럼 멀티파트 폼 데이터를 사용해서 file를 지정하고, 저장하고자하는 fileName을 작성한다.
그런 다음 실행해 보면,
이런 결과를 얻을 수 있다.
해당 URL을 복사해서 접속해보면 정상적으로 조회할 수 있다는 것을 알 수 있다.
GET Method - S3 Download
GET 메서드는 파일 이름을 확장자까지 입력해서 넣은 뒤 실행하면, AWS S3에서 해당 데이터를 조회하여 download하는 메서드이다.
실행 결과는 다음과 같다.
정리
처음에 작성할 때는, 간단하게 나중에 내가 보려고 적을려고 했는데 적다보니 이것저것 다 적게 된 것같다.
처음에 스프링부트랑 연동할 때, 에러가 너무 많아서 고생을 많이 했던 것같다.
이 글을 보는 사람들은 스트레스 받지 않고 빠르게 S3를 구축했으면 좋겠다는 마음에 최대한 자세하게 적어두었다.
'Backend > Framework' 카테고리의 다른 글
[Spring Boot] Spring Boot의 API 요청 처리 흐름 완벽 해부 (0) | 2024.07.10 |
---|---|
[Spring Boot] Spring Framework의 구성 요소와 배경 (0) | 2024.07.09 |
[Springboot] 로그인 구현 & JWT (2) - 로그인 구현 (1) | 2023.11.15 |
[Springboot] 로그인 구현 & JWT (1) - JWT 개념 (0) | 2023.11.09 |
[Springboot] Springboot에서 Redis를 사용해보자! (0) | 2023.10.15 |