AWS S3(Amazon Simple Storage Service)란?
AWS에서 제공하는 객체 스토리지 서비스로, 인터넷을 통해 언제 어디서나 원하는 양의 데이터를 저장하고 검색할 수 있는 단순하면서도 강력한 클라우드 스토리지 솔루션이다.
S3는 다음과 같은 특징을 가지고 있다.
- 무제한 확장성: 데이터 용량에 제한이 없으며, 필요한 만큼 저장 공간을 사용할 수 있다.
- 높은 내구성과 가용성: 거의 모든 시간 동안 안정적으로 서비스에 접근할 수 있다.
- 강력한 보안 기능: 암호화, 액세스 제어 등 다양한 보안 기능을 제공한다.
- 글로벌 서비스: 전 세계 어디서든 빠르게 액세스할 수 있다.
- 비용 효율성: 사용한 만큼만 비용을 지불하는 구조로, 초기 비용 없이 시작할 수 있다.
S3를 사용한 이유
케이크 도안 이미지 업로드 기능을 구현할 때 S3를 선택한 데는 몇 가지 이유가 있었다.
- 서버 부하 감소: 파일 저장과 제공을 S3가 담당하므로 서버의 부하가 줄어든다. 이미지가 꽤 많이 업로드될 것으로 예상했기 때문에 이 부분이 중요했다.
- 확장성: 사용자가 증가하거나 파일 크기가 커져도 추가 인프라 구성 없이 자동으로 확장된다. 서비스가 성장하면서 이미지 수가 늘어나도 별도의 작업 없이 계속 사용할 수 있다는 점이 좋았다.
- CDN 통합: Amazon CloudFront와 쉽게 통합되어 전 세계 사용자에게 빠른 속도로 콘텐츠를 제공할 수 있다. 당장은 필요 없었지만, 나중에 서비스가 커지면 도입할 생각이었다.
- 백업 및 복구: 자동 버전 관리와 복제 기능으로 데이터 손실을 방지한다. 이 기능은 나중에 알게 됐는데, 정말 유용하다고 생각했다.
- 비용 효율성: 자체 스토리지 인프라를 구축하고 유지하는 것보다 비용이 저렴하다. 스타트업 환경에서는 이 부분이 의외로 중요한 결정 요소였다.
Spring Boot에서 S3 활용하기
이제 실제로 내가 AWS S3를 어떻게 활용했는지 공유해보려 한다.
프로젝트는 케이크 도안 이미지를 업로드하고 관리하는 기능이 필요했다.
1. AWS S3 설정 구성
먼저 AWS S3를 사용하기 위한 설정 클래스를 만들었다.
@Configuration
public class AmazonS3Config {
@Value("${cloud.aws.credentials.accessKey}")
private String accessKey;
@Value("${cloud.aws.credentials.secretKey}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 amazonS3() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
이 부분은 생각보다 간단했다. application.properties 파일에 AWS 자격 증명과 리전 정보를 설정해두고, 위 클래스에서 불러와서 사용하기만 하면 된다. 스프링의 의존성 주입 덕분에 이후 어느 클래스에서든 S3 클라이언트를 쉽게 사용할 수 있었다.
2. S3 서비스 구현
다음으로 S3 관련 기능을 제공하는 서비스 클래스를 구현했다:
@Service
public class S3Service {
private final AmazonS3 s3Client;
@Autowired
public S3Service(AmazonS3 s3Client) {
this.s3Client = s3Client;
}
@Value("${cloud.aws.s3.bucket}")
private String bucketName;
public String uploadFile(String key, InputStream inputStream, long contentLength, String contentType) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(contentType);
metadata.setContentLength(contentLength);
try {
s3Client.putObject(bucketName, key, inputStream, metadata);
return s3Client.getUrl(bucketName, key).toString();
} catch (AmazonServiceException e) {
throw new RuntimeException("AWS S3 서비스 오류 발생: " + e.getMessage(), e);
} catch (SdkClientException e) {
throw new RuntimeException("AWS S3 클라이언트 오류 발생: " + e.getMessage(), e);
}
}
}
이 서비스 클래스는 세 가지 파일 업로드 메서드를 만들었다.
- 스트림 기반 업로드
- MultipartFile을 사용한 업로드
- 테스트용 파일 업로드
3. 컨트롤러에서 활용
마지막으로 RESTful API 컨트롤러에서 S3 서비스를 활용해 사용자가 업로드한 케이크 도안 이미지를 처리했다.
@RestController
public class JuUserCakeDesignController {
@Autowired
private JuUserCakeDesignService juUserCakeDesignService;
@Autowired
private S3Service s3Service;
@PostMapping("/api/add/user/cakeDesign")
public JsonResult addVenderCakeDesign(@RequestParam("files") List<MultipartFile> files,
@RequestParam("cakeDesignTitle") String cakeDesignTitle,
/* 다른 매개변수들 */, HttpServletRequest request) {
if (files == null || files.isEmpty()) {
throw new IllegalArgumentException("파일이 비어있습니다.");
}
try {
// 1. 토큰에서 업체 member_id 추출
int memberId = JwtUtil.getNoFromHeader(request);
// 2. 도안 데이터 먼저 저장
JuVenderCakeDesignVo juVenderCakeDesignVo = new JuVenderCakeDesignVo();
// 속성 설정...
// 도안 데이터 저장 및 생성된 cakeDesignId 가져오기
int cakeDesignId = juUserCakeDesignService.exeAddUserCakeDesign(juVenderCakeDesignVo);
// 3. 각 이미지 파일 처리
for (MultipartFile file : files) {
if (!file.isEmpty()) {
// 이미지 업로드 처리
String key = "images/" + UUID.randomUUID() + "_" + file.getOriginalFilename();
String imageUrl = s3Service.uploadFile(key, file.getInputStream(), file.getSize(),
file.getContentType());
// 이미지 데이터 저장
JuVenderCakeDesignVo imageVo = new JuVenderCakeDesignVo();
imageVo.setCakeDesignId(cakeDesignId);
imageVo.setCakeDesignImageUrl(imageUrl);
// 서비스 호출로 이미지 데이터 저장
juUserCakeDesignService.saveUserCakeDesignImage(imageVo);
}
}
return JsonResult.success("도안 등록이 완료되었습니다.");
} catch (IOException e) {
throw new RuntimeException("파일 업로드 중 오류가 발생했습니다.", e);
}
}
}
여기서는 여러 개의 이미지 파일을 한 번에 처리하는 게 조금 까다로웠다. 각 파일마다 고유한 키를 생성하기 위해 UUID를 사용했고, 모든 이미지는 'images/' 폴더 아래에 저장되도록 구성했다.
또 한 가지 신경 썼던 부분은 파일 업로드와 데이터베이스 저장을 트랜잭션으로 묶는 것이었다. 파일은 업로드됐는데 DB 저장에 실패하면 고아 파일이 생길 수 있기 때문이다.
사실 처음에는 단순히 파일명만 사용했다가 같은 이름의 파일이 업로드되면서 덮어쓰기 문제가 발생했다. 그래서 UUID와 원본 파일명을 조합해서 사용하는 방식으로 바꿨더니 잘 해결됐다.
S3 활용 시 고려사항
프로젝트에서 S3를 활용하며 다음과 같은 점을 특히 신경 썼다.
- 파일명 충돌 방지: UUID를 사용하여 각 파일명이 고유하도록 했다. 이건 정말 중요한 부분이었다.
- 폴더 구조: 'images/' 접두사를 사용하여 S3 버킷 내에서 파일을 논리적으로 구조화했다. 나중에 다른 타입의 파일도 저장할 수 있도록 미리 구조를 잡아뒀다.
- 예외 처리: AWS 서비스 예외와 SDK 클라이언트 예외를 구분하여 처리했다. 각 예외별로 다른 대응이 필요할 수 있어서 세분화했다.
- 보안: AWS 자격 증명을 application.properties에 직접 넣지 않고, 환경 변수로 관리했다. 이건 깃허브에 코드를 올릴 때 중요한 정보가 노출되지 않도록 하기 위함이었다.
마무리
S3를 SpringBoot 프로젝트에 도입하면서 파일 관리에 대한 고민을 많이 덜 수 있었다. 특히 케이크 도안 이미지처럼 점점 늘어나는 파일들을 관리하는 데 있어서 S3는 최적의 선택이었다고 생각한다.
처음에는 설정이 좀 복잡하게 느껴졌지만, 일단 구축해놓으니 정말 편하게 사용할 수 있었다. 앞으로 다른 프로젝트에서도 파일 저장소가 필요하다면 당연히 S3를 첫 번째로 고려할 것 같다.
'SpringBoot' 카테고리의 다른 글
[SpringBoot] JWT 토큰 구현 및 활용 (0) | 2025.03.06 |
---|---|
[SpringBoot] 스프링부트 어노테이션(Annotation) 정리 (0) | 2025.03.05 |
[SpringBoot] Twilio를 활용한 기념일 알림 기능 개발 (0) | 2025.02.23 |
[SpringBoot] Twilio를 활용한 React & Spring Boot 휴대폰 인증 기능 개발 (0) | 2025.02.21 |
[SpringBoot] Spring Boot + React를 활용한 OAuth 2.0 로그인 구현 (카카오 기준) (0) | 2025.02.15 |