GitHub 자세히보기

Programming/Web

[ Web] SpringBoot AWS S3 적용하여 파일 업로드하기

devdange 2022. 2. 25. 20:30

안녕하세요 데브당에입니다.

오늘은 AWS S3 Bucket를 이용하여 파일 업로드하는 법에 대해 알아보겠습니다.

 

들어가며

저는 프로젝트에서 프로필 사진 업로드, 게시판 파일첨부, 수업자료 업로드 부분에 S3 버킷을 적용했습니다.

버킷 생성 및 설정, 스프링부트 환경설정만 잘 설정해둔다면 코드상의 로직은 비슷하게 구성되기 때문에 비교적 수월하게 구현할 수 있을거라 생각합니다.

 

먼저 저희 프로젝트에서 구현했던 게시판 화면을 보여드리겠습니다.

게시글 등록시 파일을 첨부할 수 있고, 조회시에는 다운로드 받을 수 있도록 구현했습니다.

 

파일 업로드
파일 다운로드

 

그럼 지금부터 S3 버킷 생성부터 Springboot 환경설정 및 Service, Controller 작성, React 코드까지 알아보겠습니다.

 

S3 버킷 생성

버킷 생성
버킷만들기-1
버킷만들기-2
버킷만들기 - 3
버킷 정책 - 1
버킷 정책 - 2
정책 생성기 - 1
정책 생성기 - 2
정책 생성기 - 3
버킷 정책 - 3
IAM KEY 생성 - 1
IAM KEY 생성 - 2
IAM KEY 생성 - 3
IAM KEY 생성 - 4
IAM KEY 생성 - 5

 

IAM KEY 생성 - 6

 

[SpringBoot-1] 환경설정 

  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
  2. 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