[ Web] SpringBoot AWS S3 적용하여 파일 업로드하기
안녕하세요 데브당에입니다.
오늘은 AWS S3 Bucket를 이용하여 파일 업로드하는 법에 대해 알아보겠습니다.
들어가며
저는 프로젝트에서 프로필 사진 업로드, 게시판 파일첨부, 수업자료 업로드 부분에 S3 버킷을 적용했습니다.
버킷 생성 및 설정, 스프링부트 환경설정만 잘 설정해둔다면 코드상의 로직은 비슷하게 구성되기 때문에 비교적 수월하게 구현할 수 있을거라 생각합니다.
먼저 저희 프로젝트에서 구현했던 게시판 화면을 보여드리겠습니다.
게시글 등록시 파일을 첨부할 수 있고, 조회시에는 다운로드 받을 수 있도록 구현했습니다.
그럼 지금부터 S3 버킷 생성부터 Springboot 환경설정 및 Service, Controller 작성, React 코드까지 알아보겠습니다.
S3 버킷 생성
[SpringBoot-1] 환경설정
- application.properties(S3버킷 IAM 생성 시 다운받은 CSV에 있는 키 작성)
# S3 Bucket cloud.aws.credentials.accessKey=accessKey를입력해주세요 cloud.aws.credentials.secretKey=secretKey를입력해주세요 cloud.aws.stack.auto=false # AWS S3 Service bucket cloud.aws.s3.bucket=bucket-name을 작성해주세요 cloud.aws.region.static=ap-northeast-2 # AWS S3 Bucket URL cloud.aws.s3.bucket.url=bucket주소를작성해주세요 # multipart 사이즈 설정 spring.http.multipart.max-file-size=1024MB spring.http.multipart.max-request-size=1024MB
- gradle.build
// S3 Bucket compile 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' implementation 'com.amazonaws:aws-java-sdk:1.11.404'
[SpringBoot-2] Config 파일 생성
@Configuration
public class S3Config {
@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
@Primary
public BasicAWSCredentials awsCredentialsProvider(){
BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey);
return basicAWSCredentials;
}
@Bean
public AmazonS3 amazonS3() {
AmazonS3 s3Builder = AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider()))
.build();
return s3Builder;
}
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
[SpringBoot-3] AwsS3Service
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private final AmazonS3Client amazonS3Client;
private final NoticeFileRepository noticeFileRepository;
@Transactional
@Override
public List<String> uploadFile(User user, Notice notice, List<MultipartFile> multipartFile) {
List<String> fileNameList = new ArrayList<>();
multipartFile.forEach(file -> {
String fileName = 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));
} catch(IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");
}
// 원본파일이름과 변경된 파일이름을 DB에 저장 -> 다운로드시 필요
NoticeFile noticefile = NoticeFile.builder()
.newFileName(fileName)
.originFileName(file.getOriginalFilename())
.user(user)
.notice(notice)
.build();
noticeFileRepository.save(noticefile);
fileNameList.add(fileName);
});
return fileNameList;
}
[SpringBoot-4] NoticeService
private final NoticeRepository noticeRepository;
@Transactional
@Override
public int registerNotice(String accessToken, List<MultipartFile> multipartFile, NoticeRegisterRequestDto noticeRegisterRequestDto) {
// 게시글 정보인 RequestDto를 DB에 저장하기
Notice notice = Notice.builder()
.title(noticeRegisterRequestDto.getTitle())
.content(noticeRegisterRequestDto.getContent())
.hit(0)
.build();
noticeRepository.save(notice);
// 게시글 등록 시, 파일첨부를 안 할 수 있기 때문에 조건 추가
if(multipartFile != null)
awsS3Service.uploadFile(user, notice, multipartFile);
return 200;
}
[SpringBoot-5] Controller
@PostMapping(consumes = {"multipart/form-data"})
@ApiOperation(value = "알림장 등록하기", notes="<strong>선생님이 작성한 알림장을 등록한다.</strong>")
@ApiResponses({
@ApiResponse(code=201, message="알림장이 정상적으로 등록되었습니다."),
@ApiResponse(code=401, message="인증되지 않은 사용자입니다."),
@ApiResponse(code=408, message="학생입니다."),
@ApiResponse(code=409, message="알림장 등록을 실패했습니다. ")
})
public ResponseEntity<? extends BaseResponseDto> regist(
@ApiIgnore @RequestHeader("Authorization") String accessToken,
// 첨부파일은 multipart/form-data 형식으로 받기때문에 어노테이션 타입은 RequestPart로 작성
// 파일첨부를 안할수도 있기 때문에 required=false 로 설정
@ApiParam(value="파일(여러 파일 업로드 가능)") @RequestPart(required = false) List<MultipartFile> multipartFile,
// 게시글 등록정보도 FrontEnd에서 multipart/form-data 형식으로 받기때문에 어노테이션 타입은 RequestPart로 작성
@ApiParam(value = "등록할 알림장", required = true) @RequestPart NoticeRegisterRequestDto noticeRegisterRequestDto){
int result = noticeService.registerNotice(accessToken, multipartFile, noticeRegisterRequestDto);
if(result == 200)
return ResponseEntity.status(200).body(BaseResponseDto.of(200, "Success"));
else if(result==401)
return ResponseEntity.status(401).body(BaseResponseDto.of(401, "Fail"));
else if(result==408)
return ResponseEntity.status(408).body(BaseResponseDto.of(408, "Fail"));
else
return ResponseEntity.status(409).body(BaseResponseDto.of(409, "Fail"));
}
React 코드작성
let formData = new FormData();
if (data.files) {
for (let i = 0; i < data.files.length; i++) {
formData.append("multipartFile", data.files[i]);
}
}
formData.append(
"noticeRegisterRequestDto",
new Blob(
[
JSON.stringify({
title: data.title,
content: editorRef.current.getInstance().getHTML(),
}),
],
{ type: "application/json" }
)
);
마치며
S3 버킷을 처음 사용해본 것이었지만 기본적인 설정과 포맷을 알고나면 구현하는데는 어려움이 없는 것 같았습니다. 하지만 Controller가 정상적으로 동작하는지 확인하는 과정에서 어려움이 있었습니다. 저는 프로젝트 개발 시, API Docs Tool로 Swagger-ui 를 사용하고 있는데 첨부파일과 별도의 DTO를 함께 요청할때는 Swagger-ui에서 요청/응답을 확인할 수 없었습니다. 구글링을 하다보니 많은 사람들이 파일 업로드 시에 Postman이라는 Tool을 많이 사용한다는 것을 알게 되었고 저도 Postman을 사용해서 요청/응답이 정상적으로 이루어지는지 확인할 수 있었습니다.
FE<->BE 간에 데이터를 주고 받을 때도 주고받는 데이터의 type이 매칭되지 않아 많은 시행착오를 겪었습니다. 결과적으로 Controller에서는 파일과 DTO 모두 @RequesstPart 어노테이션을 붙여주었고, React 에서도 파일, DTO 모두 FormData에 append 해주어 파일 업로드 기능이 정상적으로 동작하게 했습니다.
전체 프로젝트 코드는 아래 GitHub를 참고해주시기 바랍니다.
GitHub - dayaeLee777/DrawingDream: 💫 세상에서 가장 편한 학교, Drawing Dream 에서 여러분의 꿈을 그려 보
💫 세상에서 가장 편한 학교, Drawing Dream 에서 여러분의 꿈을 그려 보세요. Contribute to dayaeLee777/DrawingDream development by creating an account on GitHub.
github.com