AWS S3 파일 업로드,다운로드
👀 개요
이전 포스팅에서 S3에 대해서 간단하게 다뤘었다.
요번에는 S3에 파일을 올리는 과정을 포스팅하려 한다.
❗ S3에 대해서는 이전 포스팅 참고 S3란?
🦺 S3 버킷 생성에 대해서는 이전 포스팅 참고 S3 버킷 생성
🗑 S3 파일 업로드
- 버킷 생성
일단 나는 아래 그림처럼 file-upload-test-lee라는 명칭으로 퍼블릭한 버킷을 하나 생성했다.

해당 버킷을 클릭하여 들어가면 아무런 객체도 없는 상태이다.

그리고 iam 권한에 사용자에게 S3 FullAccess 권한을 부여해야 합니다. (이것이 있어야 S3에 파일 업로드를 할 수 있습니다.)
일단 aws 상에서의 작업은 이 정도만 해주면 된다.
이제 실제 애플리케이션에서 aws s3 서비스를 구현해 보자.
- AmazonS3ClientBuilder 를 사용한 업로드 구현
의존성 추가
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.5.RELEASE'프로젝트 구조
common
    ㄴconfig
        ㄴAmazonS3Config
controller
    ㄴCheckController
aws
    ㄴAwsSecret
    ㄴSecretManagerBuild
service
    ㄴS3Service   
resources
    ㄴapplication.yml
    ㄴstatic.image.test
        ㄴmey.jpg      - AmazonS3Config
- CheckController
- AwsSecret
- SecretManagerBuild
- S3Service
- application
- mey.jpg: 로컬 테스트용 이미지파일을 넣어줬다.
먼저 나는 s3에 접근하기 위해 필요한 사용자의 정보를 secretmanager를 통해 받아올 것이기 때문에 위와 같이 구조를 만들어 주었다.
해당 부분에 대해서는 이전 포스팅을 참고 하자.
application.yml
secret:
  name:
    #_dev arn
    key: #시크릿 매니저 arn
    crawler: #시크릿 매니저 arn
    ecm: #시크릿 매니저 arn
cloud:
  aws:
    region:
      static: ap-northeast-2
    stack:
      auto: false
    s3:
      bucket: file-upload-test-leeapplication.yml 에 aws 관련 정보를 기재해 준다.
AmazonS3Config
@Slf4j
@Configuration
public class AmazonS3Config {
    /**
     * aws 가용 지역
     */
    @Value("${cloud.aws.region.static}")
    private String region;
    /**
     * secretmanager key arn
     */
    @Value("${secret.name.key}")
    private String secretName;
    /**
     * s3 접근을 위한 위한 사용자 bean
     */
    @Bean
    public AmazonS3Client amazonS3Client() {
        log.info("------------------------------------->AmazonS3Client");
        AwsSecret secretKeys = SecretManagerBuild.getSecret(secretName,region);
        String accessKey = secretKeys.getAws_ac_key();
        String secretKey = secretKeys.getAws_se_key();
        BasicAWSCredentials awsCreds = new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client) AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCreds))
                .build();
    }
}AmazonS3ClientBuilder 를통해 접근 정보를 만들어 준다.
S3Service
@Slf4j
@RequiredArgsConstructor
@Component
public class S3Service {
    /**
     * s3 접근 정보
     */
    private final AmazonS3Client amazonS3Client;
    /**
     * S3 버킷 명
     */
    @Value("${cloud.aws.s3.bucket}")
    public String bucket;
    /**
     * 객체 url을 읽어온다
     * @param {string} bucket 버킷명
     * @param {string} fileName 파일명
     * @returns {string} imageUrl 이미지 url
     */
    public String read(String fileName) {
        String imageUrl = amazonS3Client.getUrl(bucket, fileName).toString();
        return imageUrl;
    }
    /**
     * S3 파일 업로드를 위한 경로 재정의
     * @param {File} uploadFile 업로드할 파일
     * @param {string} dirName 경로명
     * @returns {string} uploadImageUrl
     */
    public String uploadMultipart(MultipartFile multipartFile, String dirName) throws IOException {
        File uploadFile = convert(multipartFile)  // 파일 변환할 수 없으면 에러
                .orElseThrow(() -> new IllegalArgumentException("error: MultipartFile -> File convert fail"));
        return upload(uploadFile, dirName);
    }
    /**
     * S3 파일 업로드를 위한 경로 재정의
     * @param {File} uploadFile 업로드할 파일
     * @param {string} dirName 경로명
     * @returns {string} uploadImageUrl
     */
    public String upload(File uploadFile, String dirName) {
        String fileName = dirName + "/" + uploadFile.getName();
        String uploadImageUrl = putS3(uploadFile, fileName);
        removeNewFile(uploadFile);
        return uploadImageUrl;
    }
    /**
     * S3 업로드
     * @param {File} uploadFile 업로드할 파일
     * @param {string} fileName 파일명
     * @returns {string} awsS3UrlName 업로드된 url
     */
    private String putS3(File uploadFile, String fileName) {
        amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, uploadFile).withCannedAcl(CannedAccessControlList.PublicRead));
        String awsS3UrlName = amazonS3Client.getUrl(bucket, fileName).toString();
        return awsS3UrlName;
    }
    /**
     * S3 다운로드
     * @param {string} fileName 파일명
     * @returns {S3Object} obj 객체
     */
    public ResponseEntity<byte[]> download(String storedFileName) throws IOException{
        S3Object o = amazonS3Client.getObject(new GetObjectRequest(bucket, storedFileName));
        S3ObjectInputStream objectInputStream = o.getObjectContent();
        byte[] bytes = IOUtils.toByteArray(objectInputStream);
        String fileName = URLEncoder.encode(storedFileName, "UTF-8").replaceAll("\\+", "%20");
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        httpHeaders.setContentLength(bytes.length);
        httpHeaders.setContentDispositionFormData("attachment", fileName);
        return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
    }
    // 로컬에 저장된 이미지 지우기
    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();
    }
}S3에 접근 후 내가 필요한 작업들을 정의해준다.
CheckController
@RestController
public class CheckController {
    @Autowired
    private S3Service s3Service;
    //multipartfile 업로드시
    @GetMapping("/fileUpload")
    public String check(@RequestParam("file") MultipartFile file){
        try {
            s3Service.uploadMultipart(file,"uploadTest");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "OK";
    }
    //서버에 저장되어 있는 파일 업로드시
    @GetMapping("/localUpload")
    public String fileUpload(){
        try {
            String fileName = "mey.jpg";
            File file = ResourceUtils.getFile("classpath:static/image/test/"+fileName);
            s3Service.upload(file,"uploadTest");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return "OK";
    }
    //S3에 저장된 파일 다운로드
    @GetMapping("/localDown")
    public ResponseEntity<byte[]> fileDownload() throws  IOException{
        return s3Service.download("uploadTest/mey.jpg");
    }
}나는 두 가지 방식을 테스트하려 한다
첫 번째 resources 영역에 내가 미리 넣어둔 이미지 파일을 업로드하는 방식
두 번째 포스트 맨으로 multipartfile을 보내 업로드하는 방식
사실 둘 다 로컬에 한번 저장하는 과정이 필요하다 s3 service를 보면 더 자세히 알 수 있다.
- 포스트 맨을 통한 테스트
먼저 첫 번째 방식을 테스트해보자 아래와 같이 요청을 했을 때

내가 생성한 버킷에 설정한 폴더명이 생기고 클릭해보면 로컬에 넣어놨던 파일이 저장되어 있다.


두 번째 방식도 테스트를 해보자
아래와 같이 파일을 넣어서 요청해 보면

같은 버킷의 폴더에 아래와 같이 내가 보낸 파일이 업로드된 걸 볼 수 있다.

- S3 객체 다운로드 테스트
브라우저 창을 하나 열고 다운로드 테스트를 해보자
아래처럼 내가 방금 업로드한 파일이 잘 다운로드되는 걸 확인할 수 있다.

🌭마무리
이처럼 간단하게 springboot를 사용하여 s3에 접근 후 파일 업로드 다운로드를 구현해 보았다.
굉장히 간단한 예제이므로 공식문서 등을 참고하여 좀 더 여러 방식으로 사용이 가능하다 생각된다.