AWS S3 정적 파일 가져오기/업로드/다운로드(with SpringBoot)

devdo·2023년 3월 30일
0

AWS

목록 보기
7/7
post-thumbnail

AWS S3 저장소(버킷)을 사용하여 정적인 파일(이미지)들은 여기에 저장할 수 있다.
SpringBoot의 파일업로드/다운로드 작업은 이 AWS S3 저장소에서 이루어질 수 있다. 사용방법을 정리해본다.


AWS S3 버킷 만들기

✅ 그전에 AWS Access key 설정!

s3 서비스를 이용할려면 Access key를 발급받아야 한다.

aws credentials
access-key, secret-key 을 확인할려면, 마이페이지 > 보안 자격 증명

IAM(보안 자격 증명) > 엑세스 키 만들기

여기서, access-key, secret-key 를 잘 저장해두자! S3 뿐만 아니라 AWS 서비스를 사용하기 위한 보안키인 것이다!


S3 버킷 퍼블릭 엑세스 > JSON 편집

이런식으로(403 권한 엑세스 거절) 퍼블릭으로 설정이 안되어 있으면 브라우저에서 위 표시가 난다.

aws s3 버킷 이미지 URL이 브라우저에서 노출이 될려면, 퍼블릭 엑세스 뿐만 아니라 다른 설정도 같이 해주어야 합니다!

aws s3 버킷 퍼블릭 엑세스는 버킷 정책 편집에서 진행해서 s3:Get Object 권한을 가져와야 한다!

✳️ 물론, 우리는 SpringBoot 에서 수정/삭제 기능까지 필요하기에 보기 권한 외 PutObject, DeleteObject 권한까지 추가할겁니다!

참고) https://repost.aws/ko/knowledge-center/s3-static-website-endpoint-error

저장할 버킷 상세로 들어가기 그다음 권한 탭에 들어가기

0) ACL 편집 활성화 (안해도 상관x)

1) 퍼블릭 액세스 차단은 해제!

2) 버킷 정책 편집하기

다음 페이지(Amazon S3 > 버킷 > {본인 버킷 저장소} > 버킷 정책 편집)로 들어가서 정책 생성기 버튼을 클릭해보자.

정책 생성기의 내용은 다음과 같이 적으면 된다.

  • Principal : *
  • Actions :
    1) DeleteObject
    2) GetObject
    3) PutObject
  • ARN : arn:aws:s3:::{본인 버킷 저장소 이름}/*

⚠️ ARN 뒤에 꼭! /* 붙여주어야 한다!

다 적었으면,
Add Statment > Generate Policy 를 누르면 된다.

그럼 Json 내용을 주는데

{
  "Id": "XXXXXXX218549",
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "XXXXXX7157978",
      "Action": [
        "s3:DeleteObject",
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": "arn:aws:s3:::XXXXXXX/*",
      "Principal": "*"
    }
  ]
}

이 내용을 아까 처음 버킷 정책 편집 페이지 정책 안에 넣어주고
저장해주면 된다!

이렇게 빨간색으로 퍼블릭 표시가 나오면 끝!

참고)
https://blog.naver.com/PostView.nhn?blogId=rkdudwl&logNo=222220577129


구현 예시

build.gradle

    // aws 추가
    implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

application.yml


spring:
  servlet:
    multipart:
      enabled: true
      file-size-threshold: 2MB  # 파일 임계값(메모리에 저장할 최대 크기)
      max-file-size: 5MB 	 	# 최대 파일 크기
      max-request-size: 10MB 	# 최대 요청 크기


cloud:
  aws:
    credentials:
      access-key: ㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌ
      secret-key: ㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌㅌ
    region:
      static: ap-northeast-2 # 서울
    stack:
      auto: false

application:
  bucket:
    name: fileupload221016  # s3 버킷이름

코드 구현

✳️코드 위치

AwsS3Config

@Configuration
@RequiredArgsConstructor
public class AwsS3Config {

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

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

    @Bean
    public AmazonS3 s3Client() {
        AWSCredentials credentials = new BasicAWSCredentials(accessKey, accessSecret);
        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region).build();
    }
}

AwsS3Util

@Slf4j
@Component
@RequiredArgsConstructor
public class AwsS3Util {

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

    private final AmazonS3 s3Client;


    public ResponseEntity<Resource> getFile(String fileName) throws IOException {
        // fileName = dbac534f-f3b6-4b33-9b83-e308e3c2c29d_e52319408af1ee349da788ec09ca6d92ff7bd70a3b99fa287c599037efee.jpg
        // https://mall-s3.s3.ap-northeast-2.amazonaws.com/dbac534f-f3b6-4b33-9b83-e308e3c2c29d_e52319408af1ee349da788ec09ca6d92ff7bd70a3b99fa287c599037efee.jpg
        // 로 전환!
        String urlStr = s3Client.getUrl(bucketName, fileName).toString();
        Resource resource;
        HttpHeaders headers = new HttpHeaders();
        try {
            URL url = new URL(urlStr);
            URLConnection urlConnection = url.openConnection();
            InputStream inputStream = urlConnection.getInputStream();
            resource = new InputStreamResource(inputStream);

            // MIME 타입 설정
            String mimeType = urlConnection.getContentType();
            if (mimeType == null) {
                Path path = Paths.get(fileName);
                mimeType = Files.probeContentType(path);
            }
            headers.add("Content-Type", mimeType);
        } catch (IOException e) {
            return ResponseEntity.internalServerError().build();
        }
        return ResponseEntity.ok().headers(headers).body(resource);

    }


    public String uploadFile(MultipartFile file) {

        if(file == null || file.isEmpty())
            return "";
        File fileObj = convertMultiPartFileToFile(file);
        String originalFilename = file.getOriginalFilename();
        String extension = getFileExtension(originalFilename);
        String fileName = UUID.randomUUID() + "." + extension;

        log.info("uploadFile fileName: {}", fileName);
        s3Client.putObject(new PutObjectRequest(bucketName, fileName, fileObj));
        fileObj.delete();
        return s3Client.getUrl(bucketName, fileName).toString();
    }

    public String uploadFiles(List<MultipartFile> files) {
        // 다중 업로드 && 리스트 ","을 기준으로 하나의 문자열 반환
        // files 갯수 0 이면 반환 ""
        if(files == null || files.size() == 0)
            return "";

        StringBuilder mergedUrl = new StringBuilder();
        for (int i = 0; i < files.size(); i++) {
            mergedUrl.append(uploadFile(files.get(i)));
            if(i < files.size() - 1) {
                mergedUrl.append(",");
            }
        }
        log.info("uploadFiles mergedUrl: {}", mergedUrl);
        return mergedUrl.toString();
    }


    public byte[] downloadFile(String image) {
    
        String filename = image.substring(image.lastIndexOf('/') + 1);
    
        S3Object s3Object = s3Client.getObject(bucketName, filename);
        S3ObjectInputStream inputStream = s3Object.getObjectContent();
        try {
            byte[] content = IOUtils.toByteArray(inputStream);
            return content;
        } catch (IOException e) {
            // e.printStackTrace();
            throw new IllegalStateException("aws s3 다운로드 error");
        }
    }


    public String deleteFile(String fileName) {
        s3Client.deleteObject(bucketName, fileName);
        return fileName + " removed ...";
    }


	// 쓰지 말자! File 객체 생성됨!
    private File convertMultiPartFileToFile(MultipartFile file) {
        File convertedFile = new File(file.getOriginalFilename());
        try (FileOutputStream fos = new FileOutputStream(convertedFile)) {
            fos.write(file.getBytes());
        } catch (IOException e) {
            log.error("Error converting multipartFile to file", e);
        }
        return convertedFile;
    }

    private static String getFileExtension(String originalFileName) {
        return originalFileName.substring(originalFileName.lastIndexOf(".") + 1);
    }

}

upload + 썸네일 이미지("s_")로 만들어서 진행!

build.gradle

implementation 'net.coobird:thumbnailator:0.4.19'
    /**
     * S3에 파일 업로드
     *
     * @param file 파일
     * @return 업로드된 파일 URL
     */
    public String uploadFile(MultipartFile file) {

        if (file.isEmpty()) {
            throw new IllegalArgumentException("File is empty");
        }

        String originalFilename = file.getOriginalFilename();
        String thumbnailFileName = "s_" + UUID.randomUUID().toString() + "-" + originalFilename;
        Path thumbnailPath = null;
        try {
            thumbnailPath = Paths.get(thumbnailFileName);
            // 썸네일 생성
            Thumbnails.of(file.getInputStream())
                    .size(400, 400)
                    .toFile(thumbnailPath.toFile());

            // S3에 썸네일 업로드
            s3Client.putObject(new PutObjectRequest(bucketName, thumbnailPath.toFile().getName(), thumbnailPath.toFile()));
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            // 썸네일 로컬 파일 삭제
            if (thumbnailPath != null && Files.exists(thumbnailPath)) {
                log.info("local thumbnailPath exist! {}", thumbnailPath);
                try {
                    Files.delete(thumbnailPath);

                } catch (IOException e) {
                    // 예외 발생 시 로그 남기기
                    log.error("Failed to delete local thumbnail file: {}", e.getMessage());
                }
            }
        }
        return thumbnailFileName;
    }

TestController

@Log4j2
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class TestController {

    private final AwsS3Util awsS3Util;

	// 파일 가져오기
	@GetMapping("/view/{fileName}")
    public ResponseEntity<Resource> viewFileGET(@PathVariable String fileName){

      return awsS3Util.getFile(fileName);
  }


	// 파일 업로드
    @PostMapping("/upload")
    public ResponseEntity<?> uploadFile(
            @RequestParam(value = "file") MultipartFile file
    ) {
        return new ResponseEntity<>(awsS3Util.uploadFile(file), HttpStatus.OK);
    }


    // 다중 업로드
    @PostMapping("/uploads")
    public ResponseEntity<?> uploadFiles(
            @RequestParam(value = "files") List<MultipartFile> files
    ) {
        return new ResponseEntity<>(awsS3Util.uploadFiles(files), HttpStatus.OK);
    }

    // 다운로드
    @GetMapping("/download")
    public ResponseEntity<ByteArrayResource> downloadFile(@RequestParam(value = "image") String image) {
    
    //  ex. image=https://board-example.s3.ap-northeast-2.amazonaws.com/2b8359b2-de59-4765-8da0-51f5d4e556c3.jpg
    
        byte[] data = awsS3Util.downloadFile(image);
        ByteArrayResource resource = new ByteArrayResource(data);
        return ResponseEntity
                .ok()
                .contentLength(data.length)
                .header("Content-type", "application/octet-stream")
                .header("Content-disposition", "attachment; filename=\"" + image + "\"")
                .body(resource);
    }


	// 파일 삭제
 	@DeleteMapping("/delete")
    public ResponseEntity<String> deleteFile(@RequestParam String image) {
        return new ResponseEntity<>(awsS3Util.deleteFile(image), HttpStatus.OK);
    }

}

실제 이미지 업로드 진행

    @PostMapping
    public ResponseEntity<?> createBoard(
            @Valid @RequestPart(value = "values") BoardCreateRequestDto requestDto,
            @RequestPart(value = "files", required = false) List<MultipartFile> files,
            @AuthenticationPrincipal PrincipalDetails principalDetails
    ) {
        log.info("createBoard principalDetails: {}", principalDetails);
        requestDto.setFiles(files);
        boardService.createBoard(requestDto);
        return new ResponseEntity<>(null, HttpStatus.CREATED);
    }

postman 확인

1) upload
form-data 형식 && file 이름으로 File 타입으로 보내준다!


upload 완료시, AWS S3 버킷 내부 레코드가 쌓이게 되는 것을 볼 수 있다!

이름을 클릭하면 저장된 url 링크로 브라우저에서 확인할 수 있다!

☑️ 썸네일 이미지 upload

s_ 로 시작한 image 파일 size 400, 400 으로 된 것을 확인할 수 있다!

2) download
send 버튼 화살표 아래에 Send And Download 버튼으로 전송해야한다!

3) delete




참고

profile
배운 것을 기록합니다.

0개의 댓글