본문 바로가기

환영합니다. 이 블로그 번째 방문자입니다.
Back-end

Springboot와 AWS S3 연동하기(+Controller, formData)

Springboot에서 파일(사진, 영상 등)을 외부 저장소(AWS S3)에 업로드할 때.

 

1. aws 계정 생성 → 처음이라면 프리티어로 5GB까지 업로드가 무료

2. aws s3 버킷 생성

aws region을 seoul로 설정
access 범위 해제

이 과정에서 ACL 접근을 허용할꺼냐 이런거 물어보는 것도 Enable ACL 선택!

create bucket을 선택하면 끝!

 

생성된 bucket을 선택하여 Permissions → Bucket Policy 의 아래의 내용 추가

 

 

3. IAM 계정 생성

상단 네브바의 계정 → Security credentials → Access management → Users → Add users

add users 클릭
User name만 작성
접근 권한 설정
Create user 클릭

 

4. IAM access key, secret key 발급

springboot에서 S3에 연결할 때, 내가 접근 권한이 있는 사용자라는 것을 알려주는 방법

생성된 유저 클릭
Security credentials → access keys 선택
목적에 맞게 선택
발급된 access, secret key 확인 및 .csv 파일 다운로드 → 시크릿 키는 두 번다시 발급되지 않으니 꼭 복사!!!

 

5. Springboot와 S3 연동

(1) build.gradle 에 spring-cloud-starter-aws 의존성 추가

// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-aws
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.1.RELEASE'

 

(2) application.yml 에 하위 내용 추가

spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 50MB

cloud:
  aws:
    s3:
      bucket: mention-bucket.bucket
    region:
      static: ap-northeast-2
      auto: false
    stack:
      auto: false
    credentials:
      access-key: {발급받은 access key}
      secret-key: {발급받은 secret key}

 

(3) {모듈명}Application에 static 변수 추가

@SpringBootApplication
@EnableEurekaClient
public class TeamServiceApplication {
	static {
		System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true");
	}
	public static void main(String[] args) {
		SpringApplication.run(TeamServiceApplication.class, args);
	}

}

 

(4) S3Config.java 추가

package com.ssafy.teamservice.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class S3Config {
    @Value("${cloud.aws.credentials.access-key}")
    private String iamAccessKey;
    @Value("${cloud.aws.credentials.secret-key}")
    private String iamSecretKey;
    @Value("${cloud.aws.region.static}")
    private String region;
    @Bean
    public AmazonS3Client amazonS3Client(){
        BasicAWSCredentials awsCredentials = new BasicAWSCredentials(iamAccessKey, iamSecretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region).enablePathStyleAccess()
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }
}

 

(5) S3Uploader.java 추가

package com.ssafy.teamservice.utils;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;

@Component
@Slf4j
public class S3Uploader {
    @Autowired
    private AmazonS3Client amazonS3Client;
    @Value("${cloud.aws.s3.bucket}")
    private String bucket;

    /**
     * 로컬 경로에 저장
     */
    public String uploadFileToS3(MultipartFile multipartFile, String filePath) {
        // MultipartFile -> File 로 변환
        File uploadFile = null;
        try {
            uploadFile = convert(multipartFile)
                    .orElseThrow(() -> new IllegalArgumentException("[error]: MultipartFile -> 파일 변환 실패"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // S3에 저장된 파일 이름
        String fileName = filePath + "/" + UUID.randomUUID();

        // s3로 업로드 후 로컬 파일 삭제
        String uploadImageUrl = putS3(uploadFile, fileName);
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }


    /**
     * S3로 업로드
     * @param uploadFile : 업로드할 파일
     * @param fileName : 업로드할 파일 이름
     * @return 업로드 경로
     */
    public String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(
                CannedAccessControlList.PublicRead));
        return amazonS3Client.getUrl(bucket, fileName).toString();
    }

    /**
     * S3에 있는 파일 삭제
     * 영어 파일만 삭제 가능 -> 한글 이름 파일은 안됨
     */
    public void deleteS3(String filePath) throws Exception {
        try{
            String key = filePath.substring(56); // 폴더/파일.확장자

            try {
                amazonS3Client.deleteObject(bucket, key);
            } catch (AmazonServiceException e) {
                log.info(e.getErrorMessage());
            }

        } catch (Exception exception) {
            log.info(exception.getMessage());
        }
        log.info("[S3Uploader] : S3에 있는 파일 삭제");
    }

    /**
     * 로컬에 저장된 파일 지우기
     * @param targetFile : 저장된 파일
     */
    private void removeNewFile(File targetFile) {
        if (targetFile.delete()) {
            log.info("[파일 업로드] : 파일 삭제 성공");
            return;
        }
        log.info("[파일 업로드] : 파일 삭제 실패");
    }

    /**
     * 로컬에 파일 업로드 및 변환
     * @param file : 업로드할 파일
     */
    private Optional<File> convert(MultipartFile file) throws IOException {
        // 로컬에서 저장할 파일 경로 : user.dir => 현재 디렉토리 기준
        String dirPath = System.getProperty("user.dir") + "/" + file.getOriginalFilename();
        File convertFile = new File(dirPath);

        if (convertFile.createNewFile()) {
            // FileOutputStream 데이터를 파일에 바이트 스트림으로 저장
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            }
            return Optional.of(convertFile);
        }

        return Optional.empty();
    }
}

 

(6) Controller.java 

// Service.java

package com.ssafy.teamservice.service;

import com.ssafy.teamservice.utils.S3Uploader;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
public class TeamServiceImpl implements TeamService{
    private final S3Uploader s3Uploader;

    public TeamServiceImpl(S3Uploader s3Uploader) {
        this.s3Uploader = s3Uploader;
    }

    @Override
    @Transactional
    public void createTeam(String name, MultipartFile file) {
        String url = "";
        if(file != null)  url = s3Uploader.uploadFileToS3(file, "static/team-image");

    }
}

"static/team-image"는 S3 버킷 내에서 파일을 저장할 폴더를 지정해주면 된다.

이미 폴더가 존재하면 해당 폴더 하위에 파일을 저장하고, 존재하지 않는다면 폴더를 생성하여 저장한다.

 

// Controller.java

/**
 * 그룹(팀) 생성
 * @param name
 * @param file
 * @return
 */
@PostMapping(path = "/teams", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity createTeam(
        @RequestPart(value = "name") String name,
        @RequestPart(value = "file", required = false) MultipartFile file
){
    teamServiceImpl.createTeam(name, file);
    return new ResponseEntity(null, HttpStatus.OK);
}

 

(7) Postman 테스트

 

(8) 클라이언트 입력 : React 기준

→ formData 형식으로 받아서 백엔드에 넘겨야 한다.

let testData = {
  name: '그룹 생성 1'
}

const formData = new FormData()

// 기본 정보
formData.append(
  'info',
  new Blob([JSON.stringify(testData)], {
    type: 'application/json'
  })
)

// 파일 정보
formData.append('file', values.stack)
await axios
  .post(`/team-service/teams`, formData, {
    headers: {
      'Content-Type': `multipart/form-data`
    }
  })
  .then(() => console.log('[그룹 생성] >> 성공'))
  .catch((error) => {
    console.error(error)
  })
}