들어가기 앞서 . . .
AWS S3에 대해서 공부를 했다.
그 전에 웹 서비스를 개발하며 파일을 저장할 때 AWS S3을 사용해본 경험이 있다.
하지만 이미 다른 팀원이 설정해놓고 구현해놓은 AWS S3를 사용했기때문에 S3가 어떤 기능을 제공하는 지 정도만 대충 알고있었지 직접 S3를 설정하고 코드로 구현해본 적은 없었다.
이번 기회에 S3에 대해 공부하고 실습까지 진행해 볼 예정이다.
AWS S3
S3란?
S3란 한 줄로 요약하자면 “파일 저장 서비스이다.”
우리가 핸드폰을 사용하면서 구글 드라이브, 네이버 MyBox를 사용하여 파일이나 이미지를 저장하는 경우가 있는데 이런 드라이브 역할을 한다고 생각하면 된다.
S3를 왜 사용하는걸까?
백엔드 서버를 구현하다보면 커뮤니티 등, 이미지 업로드 기능을 구현해야할 때가 많다.
그럼 이 이미지 파일을 어디에 저장해야할까?
정말 단순하게 생각하면 EC2 내부에 이미지 파일을 저장하면 편하다. 하지만 서비스를 직접 운영하다보면 EC2에 쌓이는 파일들이 방대해지고 지저분해진다.
우리가 휴대폰에 저장공간이 있는데도 불구하고 구글 드라이브나 iCloud에 사진을 옮기는 이유와 비슷하다.
S3는 파일 저장에 특화된 서비스이기때문에 파일을 다운받는 것에 대해서도 최적화 되어있는 서비스이다.
이러한 이유때문에 현업에서도 S3를 많이 사용한다.
S3 아키텍처 이해하기
파일 업로드 과정
파일 파일을 S3에 업로드 하는 과정을 알아보자.
사용자가 이미지혹은 파일을 업로드하는 API 요청을 EC2서버에 보내면 EC2는 S3에 이미지를 업로드하게 된다.
S3는 이미지를 저장 후 이미지가 저장된 S3 URL을 리턴하게 되고, EC2는 저장된 URL을 DB에 저장하게된다.
파일 다운로드 과정
파일을 업로드하는 과정을 알아봤다면, 이제 파일을 다운로드 받는 과정을 알아보자.
이미지 URL을 응답해준다.
EC2는 받은 이미지 URL을 사용자한테 응답을 하게되고, 사용자가 이미지 URL을 사용할 경우, S3로부터 이미지를 다운로드 받는다.
사용자가 이미지 URL을 사용하는 경우가 어떤 경우일까?
게시글을 조회할 때 이미지를 같이 반환해야하거나, 홈페이지에 접속했을 때, 이미지를 반환해주어야 하는경우 이미지를 다운로드 받는다.
좀 더 자세하게 설명하자면, 우리가 NAVER에 접속하게되면, 여러 이미지가 뜨는 것을 확인할 수 있는데 우리가 직접 이미지를 다운로드 받는다고 요청하는 것일까? 아니다.
사용자가 이미지가 포함된 페이지로 이동하면, 웹 페이지는 이미지를 자동으로 렌더링 해서 보여주는 것이다.
S3 용어
S3를 사용하는 실습을 들어가기 이전에, S3에서 사용하는 용어를 간단하게 정리하고 이해하자.
버킷(Bucket)
Github를 보면 여러 개의 Repository를 만들 수 있듯이, S3에도 여러 개의 저장소를 만들 수 있다. S3에서 하나의 저장소를 버킷(Bucket)이라고 부른다.
객체(Object)
S3에 업로드한 파일을 S3에서는 파일(File)이라고 부르지 않고, 객체라고 부른다.
즉, 객체(Object)란 S3 버킷에 업로드 된 파일을 의미한다.
AWS S3를 사용해보자
버킷 만들기
S3를 검색한 뒤, 버킷 만들기를 클릭하자
버킷을 생성했다!
버킷 정책 추가하기
버킷을 생성했으면 버킷 정책을 추가해주어야한다.
버킷에서의 정책이 무엇을 의미하는지 알아보자.
정책(Policy)이란?
정책이란 권한(Permission)을 정의하는 Json문서를 의미한다.
AWS는 기본적으로 대부분의 권한이 주어져있지 않다. AWS의 특정 소스에 접근하려면 권한을 허용해주어야 하는데, 권한을 허용할 때 작성해야 하는 게 정책(Policy)이다.
특정 서비스에서 상품 이미지를 모든 사용자에게 보여주고 싶다고 가정해보자.
- 버킷에서 상품 이미지를 다운로드해서 사용할 수 있어야 한다.
- 버킷에서 이미지 파일을 조회할 수 있어야 한다.
위에 맞게 정책을 추가해보자.
정책 추가하기
아까 생성한 버킷의 이름을 클릭해서 버킷의 상세 내용을 볼 수 있도록 들어가자.
들어가게 되면 이름 아래에 권한이라는 카테고리가 존재한다. 카테고리를 클릭해서 아래로 내려보자.
그러면 우리가 아까 이론을 공부했던 버킷 정책을 편집할 수 있는 칸이 나오게 되는데 편집을 눌러서 버킷 정책을 편집해보자.
새 문 추가를 눌러 서비스 선택에 S3를 검색한 뒤, 아래 사용 가능에 뜨는 S3를 눌러주자
getOb를 입력하게 되면 GetObject라는 것이 뜨는데, 이걸 추가해주자.
GetObject란 말 그대로 Object(파일 객체)를 읽을 수 있는(다운로드 받을 수 있는) 권한을 의미한다.
리소스 추가를 눌러서 리소스 유형을 ‘object’로 해주고, 리소스 ARN을 설정해주자.
리소스 ARN이란 Amazon Resource number을 의미하며 AWS에서 존재하는 리소스를 AWS만의 문법으로 표현해놓은 것을 의미한다.
위와같이 입력하면 AWS S3에 여러 리소스들이 있는데, 거기에 있는 seohaen-static-files 버킷에 있는 모든 객체에 대해서 리소스의 대상을 정할 것이다. 라는 뜻이다.
BucketName에 본인이 생성한 S3의 버킷 이름을 적어주고, ObjectName은 *을 입력해주자.
그럼 위와같이 정책이 추가된다. 여기서 Principal을 “*”로 변경해주자.
Principal은 어떤 사용자에게 권한을 줄 것인지 설정하는 부분이다.
우리는 위에서 요구사항을 정한 것 처럼, 모든 사용자에게 사진이 보여야하므로 Principal을 “*”모든 사용자로 변경해주었다.
변경사항 저장을 눌러 버킷 정책 설정은 완료했다.
S3에 파일을 업로드할 수 있도록 IAM에서 액세스 키 발급받기
IAM을 통해 S3에 파일을 업로드 할 수 있도록 액세스 키를 발급받아보자.
사용자를 생성한다.
설정은 아래와 같이 진행하면 된다.
IAM을 성공적으로 생성하였다.
방금 만든 사용자에 들어가 보안 자격 증명을 눌러보자.
액세스 키를 만들기 위해 액세스 키 만들기를 눌러준다.
필자는 외부 실행되는 애플리케이션을 선택해주었다. (SpringBoot 등)
설명 태그 값은 생략해도 된다. 액세스 키를 만들자!
인증된 사용자에 대해서 접근할 수 있어야하는데, 그때 액세스 키와 비밀 액세스 키를 사용해서 접근할 수 있기 때문에, 저장해두어야한다.
IAM을 통해서 S3에 접근할 수 있는 출입증을(액세스 키) 발급 받았다고 생각하면 쉽게 이해가 된다.
액세스 키 발급 완료 (현재 S3 삭제하여 노출되어도 괜찮다.)
S3를 활용해 SpringBoot 서버에 이미지 업로드 기능 구현하기
Springboot 연동하기
우리는 위의 과정을 거쳐서 S3 버킷을 생성하고, 버킷 정책도 설정해서 사용자가 이미지를 조회할 수 있도록 설정했다.
이제 SpringBoot 서버에 이미지 업로드 기능을 구현해보자.
Build.gradle에 의존성 추가
//S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
application.yml에 설정 정보 추가
cloud:
aws:
s3:
bucket: <버킷이름>
stack.auto: false
region.static: ap-northeast-2
credentials:
accessKey: <발급받은 accessKey>
secretKey: <발급받은 secretKey>
발급받은 액세스 키나 시크릿 키는 깃허브에 노출되면 해킹될 가능성이 있다. 해킹 시 과금 위험이 있으므로 gitignore에 application.yml이나 application.properties를 넣어서 github에 올라가지 않도록 설정하자.
cloud.aw.stack.auto=false
EC2에서 Spring Cloud 프로젝트를 실행시키면 기본적으로 CloudFormation 구성을 시작하기 때문에 설정한 CloudFormation이 없으면 프로젝트가 실행되지 않는다.
해당 기능을 사용하지 않도록 false로 설정해주었다.
cloud.aws.region.static:ap-northeast-2
지역을 한국으로 고정한다.
Spring 설정 파일(config) 추가
AWS S3에 접근할 수 있는 AmazonS3Client Bean을 생성하는 설정 클래스이다.
AWS S3 접근을 위한 자격 증명(accessKey, secretKey)을 읽어온 뒤, AmazonS3Client를 Bean으로 등록하여 다른 클래스에서 주입해서 사용 가능하게 한다.
@Configuration
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();
}
}
코드해석
//AWS 접근 자격 정보를 객체로 만듦
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
//지역(region)과 인증 정보를 기반으로 AmazonS3Client 객체를 생성
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
Spring Controller 추가
@RestController
@RequestMapping("/upload")
public class FileUploadController {
@Value("${cloud.aws.region.static}")
private String region;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3Client amazonS3Client;
public FileUploadController(AmazonS3Client amazonS3Client) {
this.amazonS3Client = amazonS3Client;
}
@PostMapping
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
String originalFilename = file.getOriginalFilename();
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf('.'));
String uuid = UUID.randomUUID().toString();
String key = "profile/" + uuid + fileExtension;
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getSize());
amazonS3Client.putObject(bucket, key, file.getInputStream(), metadata);
String fileUrl = "https://" + bucket + ".s3." + region + ".amazonaws.com/" + key;
return ResponseEntity.ok(fileUrl);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
코드해석
//클라이언트가 file이라는 이름으로 업로드한 파일을 MultipartFile로 받으
@PostMapping
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file)
//파일의 원래 이름 받아옴
String originalFilename = file.getOriginalFilename();
//파일의 마지막 '.'문자를 이용하여 확장자를 추출 (ex) .jpg, .png, .pdf
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf('.'));
//파일의 이름이 중복되면 원래 파일을 덮어씌우기 때문에 중복 방지를 위해 UUID를 생성한다.
//중복 방지를 위해 고유한 UUID 문자열 생성 (ex) e4f3d170-7ecf-4f5f-a5de-093d3d3c01c4)
String uuid = UUID.randomUUID().toString();
//S3 버킷 내 저장 경로를 생성. 콘솔에선 profile 폴더 아래 파일이 들어간 것처럼 보인다.
//(ex)profile/e4f3d170-7ecf-4f5f-a5de-093d3d3c01c4.jpg
String key = "profile/" + uuid + fileExtension;
//S3에 저장할 파일의 메타데이터 설정
ObjectMetadata metadata = new ObjectMetadata();
//ContentType: image/jpeg, application/pdf 등
metadata.setContentType(file.getContentType());
//ContentLength: 파일의 크기 (바이트)
metadata.setContentLength(file.getSize());
//실제로 파일을 S3에 업로드
//file.getInputStream(): 파일 내용을 스트림으로 읽어서 업로드
amazonS3Client.putObject(bucket, key, file.getInputStream(), metadata);
//업로드된 파일의 접근 URL 생성
//(ex)<https://my-bucket.s3.ap-northeast-2.amazonaws.com/profile/uuid.jpg>
String fileUrl = "https://" + bucket + ".s3." + region + ".amazonaws.com/" + key;
성공적으로 S3를 설정한 뒤, 이미지를 업로드하는 기능까지 만들어봤다.
스프링 서버를 실행시켜 이미지를 업로드해보자.
파일을 선택한 뒤 업로드 버튼을 눌렀다.
localhost/upload로 이동하며 S3에 저장된 이미지의 url과, 해당 이미지의 사진이 성공적으로 도출되는걸 확인할 수 있었다.
AWS S3에서도 이미지가 성공적으로 저장된 것을 확인할 수 있었다.
마무리하며
예전에는 다른 사람이 세팅해놓은 걸 그냥 가져다 썼는데, 이번에 처음부터 버킷 만들고 정책 추가하고 IAM 키 발급까지 전부 해보니까 전체 구조가 머릿속에 그려진다.
특히 버킷 정책이랑 IAM 권한 설정이 왜 필요한지, 그리고 잘못 설정하면 바로 보안 구멍이 생긴다는 걸 확실히 느꼈다.
Spring Boot에서 파일 업로드 기능을 붙여보면서, URL이 바로 반환되고 곧바로 이미지를 확인할 수 있을 때의 그 짜릿함이 있었다!!!!!
앞으로는 S3를 그냥 저장소가 아니라, 권한 관리와 파일 경로 설계까지 신경 써야 하는 중요한 인프라 요소로 생각하게 될 것 같다.