SpringBoot에서 AWS S3 파일 업로드 및 다운로드 유틸 개발

application.properties 파일 수정

S3 버킷, 액세스 키 정보 추가

### AWS AccessKey ###
cloud.aws.credentials.access-key=*****PNGQDJOTPZ5RQFE
cloud.aws.credentials.secret-key=*****WX23F/l2M1wSGApyP+hLQpQCMi5kbMHqjax
cloud.aws.s3.bucket.name=버킷명
#aws.s3.bucket.url=https://버킷명.s3.ap-northeast-2.amazonaws.com

build.gradle 파일 수정

AWS dependencies 추가

implementation 'com.amazonaws:aws-java-sdk:1.12.328'

S3 파일 업로드 및 다운로드 유틸 개발

S3Util.java

package com.chunjae.archive_cms.common.util;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ListObjectsRequest;
import com.amazonaws.services.s3.model.ObjectListing;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.S3ObjectInputStream;
import com.amazonaws.util.IOUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;

@Slf4j
@Component
public class S3Util {

    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    @Value("${cloud.aws.s3.bucket.name}")
    private String bucketName;

    public AmazonS3 s3Client;

    @PostConstruct
    public void S3Util() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        this.s3Client = AmazonS3ClientBuilder
                        .standard()
                        .withCredentials(new AWSStaticCredentialsProvider(credentials))
                        .withRegion("ap-northeast-2")
                        .build();
    }

    public Map<String, Object> uploadFile(String s3FilePath, File file) throws Exception {
        Map<String, Object> res = new HashMap<>();

        String s3FileName = UUID.randomUUID().toString();

        // 확장자 추가
        String orgFileName = file.getName();
        String fileExt = orgFileName.substring(orgFileName.lastIndexOf(".") + 1);
        s3FileName = s3FileName + "." + fileExt;

        // S3 파일 업로드
        s3Client.putObject(bucketName, s3FilePath + "/" + s3FileName, file);

        res.put("s3FilePath", s3FilePath);
        res.put("s3FileName", s3FileName);
        res.put("s3FileSize", file.length());
        res.put("orgFileName", orgFileName);

        return res;
    }

    public Map<String, Object> uploadFile(String s3FilePath, MultipartFile multipartFile) throws Exception {
        File file = convertMultipartFileToFile(multipartFile);

        Map<String, Object> res = new HashMap<>();

        String s3FileName = UUID.randomUUID().toString();

        // 확장자 추가
        String orgFileName = file.getName();
        String fileExt = orgFileName.substring(orgFileName.lastIndexOf(".") + 1);
        s3FileName = s3FileName + "." + fileExt;

        // S3 파일 업로드
        s3Client.putObject(bucketName, s3FilePath + "/" + s3FileName, file);

        // 파일 삭제
        file.delete();

        res.put("s3FilePath", s3FilePath);
        res.put("s3FileName", s3FileName);
        res.put("s3FileSize", file.length());
        res.put("orgFileName", orgFileName);

        return res;
    }

    public File convertMultipartFileToFile(MultipartFile mFile) throws Exception {
        File file = new File(mFile.getOriginalFilename());
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(mFile.getBytes());
        // FileOutputStream을 닫아주지 않으면 추후 file 삭제 등의 처리가 정상 동작하지 않을 수 있다.
        fos.close();

        return file;
    }

    public Map<String, Object> getSignedUrl(String key) throws Exception {
        Map<String, Object> res = new HashMap<>();

        long remainingTime = 1000L * 60 * 10;
        Date expiry = new Date(System.currentTimeMillis() + remainingTime);

        // "/"로 시작하면 파일을 가져올 때에 오류가 발생한다.
        if (key.startsWith("/")) {
            key = key.substring(1);
        }
        URL url = s3Client.generatePresignedUrl(bucketName, key, expiry);

        res.put("file_url", url);
        res.put("file_expiry", expiry);

        return res;
    }

    public void downloadFile(String s3FilePath, String fileName, HttpServletResponse response) throws Exception {

        // 다운로드 파일명을 null로 보내면 S3 파일명으로 다운로드
        if(fileName == null) {
            fileName = s3FilePath.substring(s3FilePath.lastIndexOf("/") + 1);
        }

        S3Object fullObject = s3Client.getObject(bucketName, s3FilePath);

        S3ObjectInputStream objectInputStream = fullObject.getObjectContent();
        byte[] bytes = IOUtils.toByteArray(objectInputStream);

        fileName = URLEncoder.encode(fileName, "UTF-8");
        fileName = fileName.replaceAll("\\+", "%20");

        response.setContentType("application/octet-stream;charset=UTF-8");
        response.setHeader("Content-Transfer-Encoding", "binary");
        response.setHeader( "Content-Disposition", "attachment; filename=\"" + fileName + "\";" );
        response.setHeader("Content-Length", String.valueOf(fullObject.getObjectMetadata().getContentLength()));
        response.setHeader("Set-Cookie", "fileDownload=true; path=/");
        FileCopyUtils.copy(bytes, response.getOutputStream());
    }

    public Integer getMaxFolderName(String folderName) throws Exception {

        ListObjectsRequest listObjectsRequest = new ListObjectsRequest()
                .withBucketName(bucketName)
                .withDelimiter("/")
                .withPrefix(folderName);

        ObjectListing objectListing = s3Client.listObjects(listObjectsRequest);

        List<Integer> folderNameList = new ArrayList<>();

        for (String commonPrefixes : objectListing.getCommonPrefixes()) {
            folderNameList.add(Integer.valueOf(commonPrefixes.split("/")[1]));
        }

        Integer maxFolderName = 0;

        if(folderNameList.size() > 0) {
            maxFolderName = Collections.max(folderNameList);
        }

        return maxFolderName;
    }
}

uploadFile 함수는 File, MultipartFile 모두 받을 수 있도록 오버로딩 하였습니다.


S3 파일 업로드 예시

S3 파일 업로드 호출부

// 폴더 경로에 몇 번째 폴더까지 있는지 확인 후 새 폴더명 번호 생성
int folderCnt = 0;
folderCnt = s3Util.getMaxFolderName("폴더명/") +1;

// S3 파일 업로드
Map<String, Object> fileUploadResult = s3Util.uploadFile("폴더명/" + folderCnt + "/하위폴더명", file);

평소 업로드 폴더는 월별로 관리하고, 일괄 업로드 시 순번 폴더를 활용하면 좋습니다.


S3 파일 URL 사용 예시

S3 파일 URL 생성 호출부

Map<String, Object> urlMap = s3Util.getSignedUrl("폴더명/하위폴더명/파일명.확장자");
urlMap.get("file_url");

S3 파일 URL 사용

<img src="${SignedUrl}"/>

JSP에서 이미지 경로로 S3 파일 URL을 사용하는 예시입니다.


S3 파일 다운로드 예시

S3 파일 다운로드 호출부

@GetMapping("/downloadS3File")
public void downloadS3File(@RequestParam Map<String, Object> param, HttpServletResponse response) throws Exception {

    // 2번째 파라미터 null = S3 파일명 그대로 다운로드
    boolean success = s3Util.downloadFile("폴더명/하위폴더명/파일명.확장자", "바꿀 파일명.확장자", response);

    log.info("파일 다운 여부 : " + success);
}

S3 파일을 다운로드하는 Test Controller 예시입니다.

Categories:

Updated:

Leave a comment