- Spring Boot에서 CloudFront 및 S3로 이미지 업로드하기
- CloudFront란?
- S3 및 CloudFront 설정하기
- Spring Boot에서 이미지 업로드 구현
흔히 정적 데이터를 저장하고 배포하기 위한 용도로 S3 버킷을 많이 사용한다. 이미지를 저장하거나, 혹은 프론트엔드를 S3에 저장하고 배포하기도 한다. 여기에 CloudFront를 붙이고, Spring Boot에서 이미지 업로드를 구현하는 방법을 알아보았다.
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/Introduction.html
Amazon CloudFront는 .html, .css, .js 및 이미지 파일과 같은 정적 및 동적 웹 콘텐츠를 사용자에게 더 빨리 배포하도록 지원하는 웹 서비스입니다. CloudFront는 엣지 로케이션이라고 하는 데이터 센터의 전 세계 네트워크를 통해 콘텐츠를 제공합니다. CloudFront를 통해 서비스하는 콘텐츠를 사용자가 요청하면 지연 시간이 가장 낮은 엣지 로케이션으로 요청이 라우팅되므로 가능한 최고의 성능으로 콘텐츠가 제공됩니다.
콘텐츠가 이미 지연 시간이 가장 낮은 엣지 로케이션에 있는 경우 CloudFront가 콘텐츠를 즉시 제공합니다.
콘텐츠가 엣지 로케이션에 없는 경우 CloudFront는 콘텐츠의 최종 버전에 대한 소스로 지정된 오리진(Amazon S3 버킷, MediaPackage 채널, HTTP 서버(예: 웹 서버) 등)에서 콘텐츠를 검색합니다.
AWS 공식 문서에서 발췌하였다. S3 접근에 대해 고가용성을 보장하고, SSL 인증서 지원을 통해 보안적인 측면도 강화할 수 있다. S3 버킷은 퍼블릭 액세스를 막아 놓고, cloudfront를 통해 접근하도록 해 보안 강화가 가능하다.
우선 S3 버킷을 생성한다.

버킷 이름이 url 주소가 되므로 이름은 글로벌 네임스페이스, 즉 전 세계의 모든 버킷 이름 중에 고유해야 한다.

소유권은 위와 같이 설정한다.

기본적으로 퍼블릭 액세스가 차단되어 있는데, 그대로 둔다. 가능하면 버킷은 프라이빗으로 유지하는 것이 좋다. 퍼블릭 액세스를 허용해 생기는 문제는 다음 영상을 참고해보자.
https://www.youtube.com/watch?v=propgtDEMgM (출처 : 코딩애플 유튜브)
이 외의 설정은 건드리지 않고 넘어간다.
다음으로 CloudFront에 접속해 배포를 생성한다.


Origin domain은 앞서 생성한 S3를 선택한다. 선택하면 이름이 오리진에 맞추어 자동으로 채워질 것이고, 원본 액세스 항목이 추가로 나타날 것이다. 원본 액세스 제어 설정(권장)을 선택한다.
그 후 Create new OAC를 선택한다.

서명 요청(권장)을 선택하고, Create를 클릭한다.
이후 내려보면 설정 부분이 있는데,

가격 분류를 지역에 맞추어 선택한다. 나는 아시아 태평양 - 서울(ap-northeast-2) 리전을 사용하고 있어 북미, 유럽, 아시아, 중동 및 아프리카에서 사용을 선택했다.
IPv6 설정도 있는데, 이것은 필요에 따라 켜고 끄면 된다.

cloudfront 주소를 통해 이미지 경로로 접속해 보면, 정상적으로 이미지에 접근이 되는 것을 확인할 수 있다.
우선 스프링부트에서 구현을 하기 전에, S3 버킷에 권한을 가진 IAM 사용자를 생성해 준다. 정책은 일단 테스트 용도로 사용하기 위해 AmazonS3FullAccess를 직접 연결해 주었다.
그리고 액세스 키를 생성해야 한다. 사용자 탭에서 생성한 IAM 유저로 들어가면 액세스 키 만들기 하이퍼링크가 있다.
사용 사례는 기타로 선택하고, 적절한 설명 태그를 넣어주면 액세스 키와 비밀 액세스 키가 나올 것이다. .csv 파일로 저장해도 되고, 복사해서 저장해두어도 된다. 이 창을 벗어나면 비밀 액세스 키는 다시는 알 수 없으니 주의해야 한다.
프로젝트를 생성하고, applciation.properties 파일에 다음 내용을 추가한다.
s3.access-key=${AWS_ACCESS_KEY_ID}
s3.secret-key=${AWS_SECRET_ACCESS_KEY}
s3.bucket-name=${AWS_S3_BUCKET_NAME}
s3.region=${AWS_REGION}
상황에 맞추어 적절히 수정하면 된다. 모든 정보는 환경변수로 관리한다. 버킷명과 특히 시크릿 액세스 키는 절대 노출되지 않도록 해야 한다.
@Service
public class FileUploadService {
@Value("${s3.bucket-name")
private String bucketName;
@Value("${s3.region")
private String region;
@Value("${s3.access-key}")
private String accessKeyId;
@Value("${s3.secret-key}")
private String secretAccessKey;
public void uploadFile(MultipartFile file) throws IOException {
AwsBasicCredentials awsCreds = AwsBasicCredentials.create(
accessKeyId,
secretAccessKey);
S3Client s3Client = S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsCreds))
.build();
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketName)
.key("uploads/" + file.getOriginalFilename())
.build();
PutObjectResponse response = s3Client.putObject(putObjectRequest, software.amazon.awssdk.core.sync.RequestBody.fromBytes(file.getBytes()));
System.out.println("Upload response: " + response);
}
}
위와 같은 서비스 클래스를 생성한다. 이를 통해 버킷에 업로드를 진행할 경우, 버킷의 uploads/ 경로에 이미지가 업로드 될 것이다.
@RestController
@RequestMapping("/api/upload")
public class S3Controller {
@Autowired
private FileUploadService fileUploadService;
@PostMapping
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) {
try {
fileUploadService.uploadFile(file);
return new ResponseEntity<>("File uploaded successfully!", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>("File upload failed: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
컨트롤러는 위와 같이 작성했다. try-catch 로직이 컨트롤러에 있는 것은 이상하지만, 학습 용도의 코드이므로 러프하게 작성했다.
이후 postman으로 테스트를 진행해 보았다.

form-data로 요청을 보낸다.
key는 RequestParam으로 설정된 이름인 file로 설정하고, File 타입을 선택한 다음 value에서 이미지를 업로드한다.
만약 이미지만 업로드 하는 것이 아니라, json 요청 body가 필요하다면, @RequestPart 어노테이션을 활용할 수 있다.
@RequestPart("data") RequestDto requestDto,
@RequestPart("file") MultipartFile file
이렇게 받을 수 있다.
요청 결과 upload 폴더에 이미지가 저장되었다.
