1. 의존성 추가
build.gradle
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
2. 환경변수 설정
application-s3.yml
AWS EB의 환경 속성 설정에 값을 입력하여 보안 유지
cloud:
aws:
credentials:
accessKey: ${AWS_ACCESS_KEY}
secretKey: ${AWS_SECRET_ACCESS_KEY}
s3:
bucket: ${BUCKET}
region:
static: ap-northeast-2
stack:
auto: false
spring:
servlet:
multipart:
maxFileSize: 3MB
maxRequestSize: 12MB
aws.credentials : AWS 접근 Key 정보 : IAM으로 발급받은 키 정보를 입력
aws.s3 : S3 bucket 이름
spring.servlet.multipart.maxFileSize : 파일 하나당 최대 용량
spring.servlet.multipart.maxRequestSize : 파일 하나당 최대 용량
AmazonS3Config
@Configuration
public class AmazonS3Config {
@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 awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
S3Service에서 사용할 amazonS3Client의 의존관계 주입을 위한 bean 객체 생성
Service
MultipartFile file = requestDto.getFile();
// s3저장 후 url 반환받음
String cafeImageUrl = s3Service.upload(file, "cafeImage");
CafeImage cafeImage = new CafeImage(file.getOriginalFilename(), cafeImageUrl, cafe);
cafeImageRepository.save(cafeImage);
s3Service.upload(file, 폴더명) : file 데이터와 폴더명을 매개변수로 넘겨줍니다 -> s3에 해당 폴더명으로 폴더를 생성하여 각 도메인에서 사용하는 폴더를 구분해줍니다.
반환받은 url을 데이터베이스에 저장해 줍니다.
*이미지 수정의 경우 s3에 저장된 기존의 이미지를 삭제 후 새로운 이미지를 저장하는 방식으로 구현하였습니다.
S3Service (이미지 한장인 경우)
@Slf4j
@RequiredArgsConstructor
@Component
public class S3Service {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
public String bucket; // S3 버킷 이름
// 최초 게시글 작성 시 업로드
public String upload(MultipartFile file, String dirName){
String fileName = dirName + "/" + createFileName(file.getOriginalFilename());
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try(InputStream inputStream = file.getInputStream()) {
amazonS3Client.putObject(new PutObjectRequest(bucket,fileName,inputStream,objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3Client.getUrl(bucket, fileName).toString();
}catch (IOException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,"파일 업로드 실패");
}
}
// 글 수정 시 기존 s3에 있는 이미지 정보 삭제,
public String reupload(MultipartFile file, String currentFilePath, String imageKey){
String fileName = currentFilePath + "/" + createFileName(file.getOriginalFilename()); // 파일명 랜덤화
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
amazonS3Client.deleteObject(bucket, imageKey);
try(InputStream inputStream = file.getInputStream()) {
amazonS3Client.putObject(new PutObjectRequest(bucket,fileName,inputStream,objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3Client.getUrl(bucket, fileName).toString();
}catch (IOException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,"파일 업로드에 실패");
}
}
private String createFileName(String fileName) {
// 먼저 파일 업로드 시, 파일명을 난수화하기 위해 random으로 돌립니다.
return UUID.randomUUID().toString().concat(getFileExtension(fileName));
}
private String getFileExtension(String fileName) {
// file 형식이 잘못된 경우를 확인하기 위해 만들어진 로직이며,
// 파일 타입과 상관없이 업로드할 수 있게 하기 위해 .의 존재 유무만 판단
try {
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일(" + fileName + ") 입니다.");
}
}
public void deleteImages(String imageKey) {
amazonS3Client.deleteObject(bucket, imageKey);
}
}
createFileName 메소드 : 파일명의 난수화를 통해 파일명 중복 방지
S3Service (이미지 여러장인 경우)
// 최초 게시글 작성 시 업로드 : 여러개
public List<String> upload(List<MultipartFile> files, String dirName){
List<String> urlList = new ArrayList<>();
return getUrlList(files, dirName, urlList);
}
// 글 수정 시 기존 s3에 있는 이미지 정보 삭제 후 새로 저장
// 덮어쓰기 방식으로도 처리 가능하지만 기존파일명에 덮어씌워지므로 파일 타입이 새로 저장한 파일과 일치하지 않음
public List<String> reupload(List<MultipartFile> files, String dirName, List<String> imageKeys){
List<String> urlList = new ArrayList<>();
for (String imageKey : imageKeys) {
amazonS3Client.deleteObject(bucket, imageKey);
}
return getUrlList(files, dirName, urlList);
}
/**
* s3에 저장 후 url 반환받는 메소드
*/
private List<String> getUrlList(List<MultipartFile> files, String dirName, List<String> urlList) {
for (MultipartFile file : files) {
String fileName = dirName + "/" + createFileName(file.getOriginalFilename());
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try(InputStream inputStream = file.getInputStream()) {
amazonS3Client.putObject(new PutObjectRequest(bucket,fileName,inputStream,objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
urlList.add(amazonS3Client.getUrl(bucket, fileName).toString());
}catch (IOException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,"파일 업로드 실패");
}
}
return urlList;
}
public void deleteImages(List<String> imageKeys) {
for (String imageKey : imageKeys) {
amazonS3Client.deleteObject(bucket, imageKey);
}
}
html
<ul>
<li>첨부파일<input type="file" th:field="*{file}" name="attachFile" ></li>
<li>이미지 파일들<input type="file" multiple="multiple" th:field="*{files}" name="imageFiles" ></li>
</ul>
dto
MultipartFile file;
private List<MultipartFile> files;
thymeleaf를 사용하였으므로 dto와 html의 타입 매칭을 통해 dto에 데이터를 받아왔습니다
============================================================================
s3 이미지 삭제 요청시 s3 access denied 에러 발생
->처음에는 s3 자체 권한 설정 문제라고 생각하여 s3에서의 권한 테스트를 수도없이 해봤지만 해결되지않아 미뤄두었다가 배포 설정에서도 같은 에러가 발생하여 다른 접근방법을 찾아보게 되었고 iam에 추가된 정책에 따른 접근 권한 제한 설정 때문임을 찾아내게 되었습니다.
해결 방법 포스팅
'프로그래밍 > JPA Project - sparta' 카테고리의 다른 글
타임리프 값 -> 자바스크립트에서 사용하기 (0) | 2022.07.10 |
---|---|
전체 조회 로직 구현 (페이징o) (0) | 2022.07.09 |
top5 조회 로직 구현 (페이징x) (0) | 2022.07.08 |
Post Method Refactoring - 카페 등록 api 리팩토링 (0) | 2022.07.06 |
Spring Security - 페이지, thymeleaf 접근 제한 (0) | 2022.07.04 |