안녕하세요 데브당에입니다.
오늘은 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를 참고해주시기 바랍니다.
'Programming > Web' 카테고리의 다른 글
[Web] MVC 패턴, Model1, Model2 란? 구조와 장단점까지 알아보기 (0) | 2022.01.06 |
---|---|
[Web] REST API, 기초부터 정확히 이해하기 (1) | 2022.01.05 |