SpringBoot: 3.1.3
Build: Gradle
Java: 17
OS: Window
📌 미니 프로젝트를 진행하면서 이미지 저장이 필요하게 되었다.
이미지는 S3에 저장하고 저장된 URL만 DB에 저장해서 불러오기, 수정, 삭제를 구현할 수 있도록 S3를 만들어보고 적용까지 시도해봤다.
S3 버킷 생성 방법이 기록된 여러 블로그를 보고 S3를 생성 및 적용을 했는데 AWS 웹사이트가 업데이트 주기가 짧은지건지 설명과 안맞는 부분과 부족한 부분이 있어서 상당히 힘들었다.
🙋♀ 그래서 나는 나중에 이 블로그를 방문하는 사람들이 어느 부분이 달라졌고 달라진 부분의 설정을 어떻게 찾아보면 되는지 비교할 수 있도록 현제 버전에서의 자세한 내용을 기록해보려고 한다.
👉 S3는 Simple Storage Service의 약자로 주로 파일을 저장하는데 주로 사용된다.
파일 서버는 트래픽이 증가함에 따라 서버 인프라 및 용량 계획을 변경해야 되는데, S3가 확장 및 성능 부분을 대신 처리해준다.
여러 영역에 여러 데이터 복사본을 저장하므로 한 영역이 다운되더라도 데이터를 사용할 수 있고, 복구가 가능하다
파일과 파일정보로 구성된 저장단위로 그냥 파일이라 생각하면 된다.
다수의 객체를 관리하는 컨테이너로 파일시스템이라 보면된다.
⭐ Amazon S3는 계정 가입일을 기준을 12개월 동안 무료로 사용이 가능하며, 제한 범위는 아래와 같으니 과금방지를 위해 참고하자.
🔽🔽🔽
S3를 설정할 때 가장 일반적인 설정 방법을 선택했다.
사용자에 따라 액세스 차단 설정을 통해 필요한 항목에 대한 액세스를 제한하거나 Spring Security를 사용하여 파일 조작 권한을 부여할 수 있는데 아직 그 부분을 공부하지 않았으므로 나는 이 방법을 선택했다 🌝
남은 설정은 이렇게 기본값으로 설정하고 버킷 만들기 를 클릭해서 S3 버킷을 생성하면 된다.
따라서 S3용 IAM을 생성하고 엑세스 키값을 반드시 저장해야한다!!
사용자 이름을 입력하고 다음을 누른다
권한 옵션 : 직접 정책 연결
권한 정책 : AmazonS3FullAccess
으로 설정하고
다음을 눌러서 생성까지 완료한다.
📌 23.09.28 기준)
다른 사이에서는 IAM 생성시 엑세스 키-값이 자동으로 생성된다고 나왔지만 현재 기준으로는 IAM 생성 후 엑세스 키를 직접 발급해줘야 한다.
다른 블로그의 설명에서 아무리 찾아봐도 엑세스키가 자동발급이 안되서 헤맸는데 아주 간단하게 발급이 가능하니 아래의 방법으로 진행하면 된다.
엑세스 키 만들기 클릭
사용 사례를 선택하게 나오는데 말 그대로 사용 사례와 대안을 띄워주는 기능만 하기때문에 뭘 선택해도 상관 없다.
참고로 나는 이걸 선택했다.
다음 클릭
이 부분은 선택사항이라 빈칸으로 넘어가도 된다.
나는 AWS의 다른 기능들을 추가하면서 이 사용자가 어디에 연결되어 있는지 헷갈리는 경우가 많았던지라 왠만하면 구분이 가능하게 태그를 추가하는 편이다.
엑세스 키 값이 발급되었다.
꼭 파일로 저장하자!!
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
cloud.aws.credentials.accessKey=IAM의 엑세스 키
cloud.aws.credentials.secretKey=IAM의 시크릿 키
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
cloud.aws.s3.bucket=S3버킷이름(test-bucket-9876543)
@Configuration
public class S3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String accessSecret;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3 s3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, accessSecret);
return AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region).build();
}
}
@Slf4j
@Service
public class S3Uploader {
private final AmazonS3 amazonS3;
private final String bucket;
public S3Uploader(AmazonS3 amazonS3, @Value("${cloud.aws.s3.bucket}") String bucket) {
this.amazonS3 = amazonS3;
this.bucket = bucket;
}
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
// 파일 이름에서 공백을 제거한 새로운 파일 이름 생성
String originalFileName = multipartFile.getOriginalFilename();
// UUID를 파일명에 추가
String uuid = UUID.randomUUID().toString();
String uniqueFileName = uuid + "_" + originalFileName.replaceAll("\\s", "_");
String fileName = dirName + "/" + uniqueFileName;
log.info("fileName: " + fileName);
File uploadFile = convert(multipartFile);
String uploadImageUrl = putS3(uploadFile, fileName);
removeNewFile(uploadFile);
return uploadImageUrl;
}
private File convert(MultipartFile file) throws IOException {
String originalFileName = file.getOriginalFilename();
String uuid = UUID.randomUUID().toString();
String uniqueFileName = uuid + "_" + originalFileName.replaceAll("\\s", "_");
File convertFile = new File(uniqueFileName);
if (convertFile.createNewFile()) {
try (FileOutputStream fos = new FileOutputStream(convertFile)) {
fos.write(file.getBytes());
} catch (IOException e) {
log.error("파일 변환 중 오류 발생: {}", e.getMessage());
throw e;
}
return convertFile;
}
throw new IllegalArgumentException(String.format("파일 변환에 실패했습니다. %s", originalFileName));
}
private String putS3(File uploadFile, String fileName) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
.withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3.getUrl(bucket, fileName).toString();
}
private void removeNewFile(File targetFile) {
if (targetFile.delete()) {
log.info("파일이 삭제되었습니다.");
} else {
log.info("파일이 삭제되지 못했습니다.");
}
}
public void deleteFile(String fileName) {
try {
// URL 디코딩을 통해 원래의 파일 이름을 가져옵니다.
String decodedFileName = URLDecoder.decode(fileName, "UTF-8");
log.info("Deleting file from S3: " + decodedFileName);
amazonS3.deleteObject(bucket, decodedFileName);
} catch (UnsupportedEncodingException e) {
log.error("Error while decoding the file name: {}", e.getMessage());
}
}
public String updateFile(MultipartFile newFile, String oldFileName, String dirName) throws IOException {
// 기존 파일 삭제
log.info("S3 oldFileName: " + oldFileName);
deleteFile(oldFileName);
// 새 파일 업로드
return upload(newFile, dirName);
}
}
이렇게 S3의 설정을 마무리 한 후 entity와 서비스 로직은 필요에 맞게 설정하면 된다.
🔽 실행결과
🔎 위의 내용을 참고로 entity와 service, controller가 포함된 파일을 올렸다.
어떤식으로 동작하는지 파악하고 필요에 따라 커스텀하는 용도로 사용하면 좋을것 같다.
안녕하세요. 정성이 넘치는 글 잘 보았습니다.
특히 과금 방지를 위한 내용이 글 상단에 올라와 있다는 점이
대단히 인상적이었습니다.
이후 S3 사용 시 꼭 참고하도록 하겠습니다.