개발꿀팁
Spring boot에서 AWS S3 파일 업로드 하기
프로젝트 중 이미지업로드를 해야하는 부분이 있어 구글링해보니 생각보다 복잡해 포스팅을 작성하게 되었다. S3의 버킷을 이용하였고 비슷하지만 다른 2가지 방법을 정리하였다.
1. 사전 준비사항
AWS S3 버킷생성과 IAM 설정
2. 공통사항
의존성 추가
dependencies {
// Use ConfigurationProperties
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
//aws s3
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.5.RELEASE'
}
.gitignore(accessKey, SecretKey 보호를 위해 필수)
### my ignore ###
방법1
src/main/resources/application-credentials.yml
방법2
src/main/resources/aws.yml
S3을 이용한 파일업로드 방법1
FileuploadController.java
@RequiredArgsConstructor
@RestController
public class FileuploadController {
private final FileUploadService fileUploadService;
@PostMapping("/api/v1/upload")
public String uploadImage(@RequestPart MultipartFile file) throws IllegalAccessException {
return fileUploadService.uploadImage(file);
}
}
Post/api/v1/upload로 multipart/form-data요청을 받도록 컨트롤러 단에서 처리하는 로직
Spring MVC에서는 쉽게 @RequestPart 어노테이션을 이용해 multipart/form-data요청을 받을 수 있다
FileuploadService.java
@RequiredArgsConstructor
@Service
public class FileUploadService {
private final UploadService s3Service;
//Multipart를 통해 전송된 파일을 업로드하는 메소드
public String uploadImage(MultipartFile file) throws IllegalAccessException {
String fileName = createFileName(file.getOriginalFilename());
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try (InputStream inputStream = file.getInputStream()) {
s3Service.uploadFile(inputStream, objectMetadata, fileName);
} catch(IOException e) {
throw new IllegalAccessException(String.format("파일 변환 중에러가 발생하였습니다.(%s)", file.getOriginalFilename()));
}
return s3Service.getFileUrl(fileName);
}
//기존 확장자명을 유지한 채, 유니크한 파일의 이름을 생성하는 로직
private String createFileName(String originalFileName) throws IllegalAccessException {
return UUID.randomUUID().toString().concat(getFileExtension(originalFileName));
}
//파일의 확장자명을 가져오는 로직
private String getFileExtension(String fileName) throws IllegalAccessException {
try {
return fileName.substring(fileName.lastIndexOf("."));
} catch (StringIndexOutOfBoundsException e) {
throw new IllegalAccessException(String.format("잘못된 형식의 파일($s) 입니다", fileName));
}
}
}
서비스 로직에서는 클라이언트로 부터 받은 MultipartFile의 원래 이름을 랜덤으로 바꿔줌
이때, 확장자는 계속 유효해야함으로 유니크 키 + 확장자로 이름을 바꿔줌(S3에 저장할 때 이름이 같은 파일이 있으면 에러 발생함)
MultipartFile에서 InputStream을 가져와서 실제 AWS S3로 파일을 전송하는 로직은 S3Service로 위임
UploadService.java
public interface UploadService {
void uploadFile(InputStream inputStream, ObjectMetadata objectMetadata, String fileName);
String getFileUrl(String fileName);
}
AwsS3UploadService.java
@Component
@RequiredArgsConstructor
public class AwsS3UploadService implements UploadService {
private final AmazonS3 amazonS3;
private final S3Component component;
@Override
public void uploadFile(InputStream inputStream, ObjectMetadata objectMeTadata, String fileName){
amazonS3.putObject(new PutObjectRequest(component.getBucket(),fileName,inputStream,objectMeTadata).withCannedAcl(CannedAccessControlList.PublicRead));
}
@Override
public String getFileUrl(String fileName){
return amazonS3.getUrl(component.getBucket(),fileName).toString();
}
}
uploadFile()은 aws-cloud-starter-aws에서 제공하는 AmazonS3를 이용해서 파일을 업로드하고,
아래 getFileUrl()메소드는 업로드한 파일의 URI를 가져오는 메소드이다.
S3Component는 AWS S3를 위한 설정파일이 담긴 클래스 인데, AWS S3 환경변수를 먼저 프로퍼티에 추가한다.
application-aws.yml
cloud:
aws:
s3:
bucket: <AWS S3 버킷 이름>
region:
static: ap-northeast-2 //AWS 리전 정보
stack:
auto: false
credentials:
instanceProfile: true // AWS CLI에서 aws configure list 정보를 반영할건지 여부
AWS Console에서 생성한 S3의 버킷과 리전 정보를 Application-aws.yml을 만들어 위에 추가
application-credentials.yml
cloud:
aws:
credentials:
accessKey: <AWS IAM Access Key>
secretKey: <AWS IAM Secret Key>
그리고 다음과 같이 AWS IAM accessKey, secretKey를 application-credentials.yml을 만들어 추가
application.yml
spring:
profiles:
include:
- aws
- credentials
application-aws.yml과 application-credentials.yml을 application.yml에서 spring.profiles.include 해준다
S3Component.java
@Getter
@Setter
@ConfigurationProperties(prefix= "cloud.aws.s3")
@Component
public class S3Component {
private String bucket;
}
S3Component에서 @ConfigurationProperties를 통해 프로퍼티의 값을 불러온다.
출처 : https://willseungh0.tistory.com/2#application-credentials-yml
### com.amazonaws.SdkClientException: Failed to connect to service endpoint 오류발생 시 ###
### https://h-kkaemi.tistory.com/24 참고 ###
S3을 이용한 파일업로드 방법2
aws.yml
cloud:
aws:
credentials:
accessKey: IAM 사용자 엑세스 키
secretKey: IAM 사용자 비밀 엑세스 키
s3:
bucket: 버킷 이름
region:
static: ap-northeast-2
stack:
auto: false
S3config.java
@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();
}
}
config 디렉토리에서 설정 값을 넣기 위해서 AmazonS3Config 설정 클래스를 만들고 aws.yml 파일에 작성한 값들을 읽어와서 AmazonS3Client 객체를 만들어 Bean으로 주입
MainApplication.java
@SpringBootApplication
public class ImageApplication {
public static final String APPLICATION_LOCATIONS = "spring.config.location="
+ "classpath:application.yml,"
+ "classpath:aws.yml";
public static void main(String[] args) {
new SpringApplicationBuilder(ImageApplication.class)
.properties(APPLICATION_LOCATIONS)
.run(args);
}
}
위의 코드로 application.yml과 aws.yml 두개의 파일 모두를 설정 파일로 읽어서 사용
S3Uploader.java
@Slf4j
@RequiredArgsConstructor
@Component
public class S3Uploader {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
public String bucket; // S3 버킷 이름
public String upload(MultipartFile multipartFile, String dirName) throws IOException {
File uploadFile = convert(multipartFile) // 파일 변환할 수 없으면 에러
.orElseThrow(() -> new IllegalArgumentException("error: MultipartFile -> File convert fail"));
return upload(uploadFile, dirName);
}
// S3로 파일 업로드하기
private String upload(File uploadFile, String dirName) {
String fileName = dirName + "/" + UUID.randomUUID() + uploadFile.getName(); // S3에 저장된 파일 이름
String uploadImageUrl = putS3(uploadFile, fileName); // s3로 업로드
removeNewFile(uploadFile);
return uploadImageUrl;
}
// S3로 업로드
private String putS3(File uploadFile, String fileName) {
amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3Client.getUrl(bucket, fileName).toString();
}
// 로컬에 저장된 이미지 지우기
private void removeNewFile(File targetFile) {
if (targetFile.delete()) {
log.info("File delete success");
return;
}
log.info("File delete fail");
}
// 로컬에 파일 업로드 하기
private Optional<File> convert(MultipartFile file) throws IOException {
File convertFile = new File(System.getProperty("user.dir") + "/" + file.getOriginalFilename());
if (convertFile.createNewFile()) { // 바로 위에서 지정한 경로에 File이 생성됨 (경로가 잘못되었다면 생성 불가능)
try (FileOutputStream fos = new FileOutputStream(convertFile)) { // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장하기 위함
fos.write(file.getBytes());
}
return Optional.of(convertFile);
}
return Optional.empty();
}
}
파일 업로드는 하는 코드
- convert() 메소드에서 로컬 프로젝트에 사진 파일이 생성되지만, removeNewFile()을 통해서 바로 지우는 로직
- System.getProperty("user.dir"): 현재 프로젝트의 절대 경로를 꺼내올 수 있다
- 즉, 프로젝트 루트 경로 아래에 업로드한 사진 파일도 생성됨. 그리고 바로 removeNewFile를 통해서 로컬에 있는 파일은 삭제함
FileUploadController.java
@RequiredArgsConstructor
@RestController
public class FileUploadController {
private final S3Uploader s3Uploader;
@PostMapping("/images")
public String upload(@RequestParam("images") MultipartFile multipartFile) throws IOException {
s3Uploader.upload(multipartFile, "static");
return "test";
}
}
- 파일 업로드를 할 때는 MultipartFile을 사용
- 두 번째 매개변수의 이름에 따라 S3 Bucket 내부에 해당 이름의 디렉토리가 생성됨
출처 : https://devlog-wjdrbs96.tistory.com/323
PostMan으로 테스트하기
URI를 입력한 후 Headers에서 form-data로 보내기를 설정하고.
Body에서 파일을 업로드해서 post로 보내주면
주소가 들어오는 것을 확인할 수 있다.
S3에서도 확인 가능
'개발 하나둘셋 > Java & Spring' 카테고리의 다른 글
Spring Security (0) | 2021.12.11 |
---|---|
Spring Boot JWT 기본개념과 특징 (0) | 2021.12.10 |
[java] 메서드, 파라미터 이해하기 (0) | 2021.12.03 |
오버로딩과 오버라이딩 (0) | 2021.11.26 |
DI, IoC, Bean (0) | 2021.11.21 |