HyeLog

[Spring Boot + Amazon S3] - 이미지 μ—…λ‘œλ“œ, 쑰회 방법 λ³Έλ¬Έ

μ›Ή 개발/Spring Boot

[Spring Boot + Amazon S3] - 이미지 μ—…λ‘œλ“œ, 쑰회 방법

shj718 2023. 7. 6. 22:49

🌈 개발 ν™˜κ²½

이번 ν”„λ‘œμ νŠΈμ—μ„œ 이미지 νŒŒμΌμ„ Amazon S3에 μ—…λ‘œλ“œ(μ €μž₯) / 쑰회 / μ‚­μ œ ν•˜λŠ” κΈ°λŠ₯을 κ°œλ°œν•˜κ²Œ λ˜μ—ˆλ‹€.

 

참고둜 ν”„λ‘ νŠΈμ—”λ“œμ—μ„œλŠ” Next.js λ₯Ό μ‚¬μš©ν•˜κ³ , λ°±μ—”λ“œμ—μ„œλŠ” Spring Boot λ₯Ό μ‚¬μš©ν•˜μ—¬ κ°œλ°œμ„ μ§„ν–‰ν•˜κ³  μžˆλ‹€.

 

λ”°λΌμ„œ Spring Boot, Amazon S3 λ₯Ό μ‚¬μš©ν•˜μ—¬ 이미지 νŒŒμΌμ„ μ—…λ‘œλ“œ(μ €μž₯)ν•˜κ³  λ‹€μ‹œ 쑰회 및 μ‚­μ œν•˜λŠ” 방법에 λŒ€ν•΄ μ°¨κ·Όμ°¨κ·Ό 적어보렀 ν•œλ‹€.

λΉ„μŠ·ν•œ ν™˜κ²½μ—μ„œ κ°œλ°œν•˜λŠ” μ‚¬λžŒλ“€μ—κ²Œ 도움이 λ˜μ—ˆμœΌλ©΄ ν•˜λŠ” λ§ˆμŒμ΄λ‹€!

 

πŸ”₯ 전체적인 Flow

1. ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ 이미지 νŒŒμΌμ„ Form data λ‘œ 포μž₯ν•˜μ—¬ λ°±μ—”λ“œμ—κ²Œ 전달

2. λ°±μ—”λ“œμ—μ„œ ν•΄λ‹Ή 이미지λ₯Ό Amazon S3 에 μ—…λ‘œλ“œ, μ—…λ‘œλ“œλœ 이미지 μ •λ³΄λ₯Ό DB에 μ €μž₯

3. ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ μ΄λ―Έμ§€ μ‘°νšŒμ‹œ, λ°±μ—”λ“œμ—μ„œλŠ” S3에 μ €μž₯된 ν•΄λ‹Ή μ΄λ―Έμ§€μ˜ URL μ„ μƒμ„±

4. μƒμ„±λœ 이미지 URL을 ν”„λ‘ νŠΈμ—”λ“œμ—κ²Œ 전달

5. ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ ν•΄λ‹Ή URL둜 이미지λ₯Ό ν‘œμ‹œν•˜κ±°λ‚˜ λ‹€μš΄λ‘œλ“œ

 

 

참고둜 κ΅¬ν˜„μ— μ•žμ„œ S3 버킷 생성과 IAM μ‚¬μš©μž 섀정을 ν•΄μ•Ό ν•œλ‹€. ν•„μžλŠ” 이 링크λ₯Ό μ°Έκ³ ν–ˆλ‹€.

λ‹€λ§Œ, ν˜„μž¬λŠ” IAM μ‚¬μš©μž 생성을 μ™„λ£Œν•œ 이후에 μΆ”κ°€λ‘œ μ•‘μ„ΈμŠ€ ν‚€λ₯Ό λ§Œλ“€μ–΄μ•Ό ν•œλ‹€.

μ€‘μš”ν•œ 것은 버킷에 μ ‘κ·Όν•˜κΈ° μœ„ν•΄ ν•„μš”ν•œ μ•‘μ„ΈμŠ€ ν‚€(.csv 파일)λ₯Ό λ°˜λ“œμ‹œ μ €μž₯해두어야 ν•œλ‹€λŠ” 점이닀!!

 

μΆ”κ°€λ‘œ S3 λ²„ν‚·μ˜ Cors 섀정은 이 링크와 AWS 곡식 λ¬Έμ„œλ₯Ό μ°Έκ³ ν–ˆλ‹€.

 

 

이제, 본격적으둜 Spring Boot 에 κ΅¬ν˜„ν•΄λ³΄μž.

 

🌈 build.gradle

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

 

🌈 application-aws.yaml

우리 ν”„λ‘œμ νŠΈμ—μ„œλŠ” μ—¬λŸ¬κ°œμ˜ μ„€μ •νŒŒμΌ(application-database.yaml λ“±)을 μ‚¬μš©ν•˜κ³  있기 λ•Œλ¬Έμ— S3 κ΄€λ ¨ 섀정은 application-aws.yaml νŒŒμΌμ„ λ§Œλ“€μ–΄μ„œ ν•΄μ£Όμ—ˆλ‹€.

 

μ΄λŸ¬ν•œ μ„€μ • νŒŒμΌμ€ μ ˆλŒ€! κΉƒν—ˆλΈŒμ— λ…ΈμΆœλ˜λ©΄ μ•ˆλ˜κΈ° λ•Œλ¬Έμ— .gitignore 에 μΆ”κ°€ν•΄μ£Όκ³ , κΉƒν—ˆλΈŒ Actions 의 Secrets 에 application-aws.yaml νŒŒμΌ λ‚΄μš©μ„ μΆ”κ°€ν–ˆλ‹€.

 

spring:
  servlet:
    multipart:
      max-request-size: 10MB
      max-file-size: 10MB
cloud:
  aws:
    credentials:
      access-key: {μ•‘μ„ΈμŠ€ ν‚€}
      secret-key: {μ‹œν¬λ¦Ώ ν‚€}
    s3:
      bucket: {버킷 이름}
    region:
      static: ap-northeast-2
    stack:
      auto: false

 

🌈 IntelliJ μ„€μ •

 

Spring Cloud AWS μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•œ ν›„ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‹€ν–‰ν•˜λ©΄ μ½˜μ†”μ— μ—λŸ¬λ©”μ„Έμ§€κ°€ 좜λ ₯λœλ‹€.

이λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄ [Run] > [Edit Configurations] μ—μ„œ VM μ˜΅μ…˜μ— μ•„λž˜ λ‚΄μš©μ„ μΆ”κ°€ν•˜μž.

 

-Dcom.amazonaws.sdk.disableEc2Metadata=true

 

 

(μ°Έκ³ : https://thalals.tistory.com/289https://kim-jong-hyun.tistory.com/79)

 

🌈 AwsS3Config μž‘μ„±

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
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 AwsS3Config {

    @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
    public BasicAWSCredentials basicAWSCredentials() {
        return new BasicAWSCredentials(accessKey, secretKey);
    }

    @Bean
    public AmazonS3 amazonS3(BasicAWSCredentials basicAWSCredentials) {
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials))
                .build();
    }
}

 

🌈 AwsS3Controller μž‘μ„±

awsS3Service.upload() 의 λ‘λ²ˆμ§Έ μΈμžλŠ” 버킷 ν•˜μœ„μ— 생성할 폴더 이름이닀.

즉, μƒˆλ‘œμš΄ 이미지 νŒŒμΌμ„ μ—…λ‘œλ“œ ν•˜λ©΄ {버킷 이름}/test/ 폴더 μ•ˆμ— μƒμ„±λœλ‹€.

 

import repick.repickserver.domain.product.domain.AwsS3;
import repick.repickserver.domain.product.application.AwsS3Service;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@RestController
@RequestMapping("/s3")
@RequiredArgsConstructor
public class AwsS3Controller {

    private final AwsS3Service awsS3Service;

    @PostMapping("/resource")
    public AwsS3 upload(@RequestPart("file") MultipartFile multipartFile) throws IOException {
        return awsS3Service.upload(multipartFile,"test");
    }

    @DeleteMapping("/resource")
    public void remove(AwsS3 awsS3) {
        awsS3Service.remove(awsS3);
    }
}

 

🌈 AwsS3Service μž‘μ„±

S3 에 이미지 νŒŒμΌμ„ μ—…λ‘œλ“œ ν•˜λŠ” Flow λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

 

  1. MultipartFile 을 File 둜 λ³€ν™˜ν•΄μ„œ ν˜„μž¬ ν”„λ‘œμ νŠΈ κ²½λ‘œμ— μ—…λ‘œλ“œ ν•œλ‹€.
  2. λ³€ν™˜λœ File 을 S3에 μ—…λ‘œλ“œ ν•œλ‹€. (μ΄λ•Œ 파일λͺ…은 UUIDλ₯Ό λΆ™μ—¬ μƒˆλ‘­κ²Œ μ •ν•΄μ€€λ‹€.)
  3. S3에 μ—…λ‘œλ“œ 된 파일의 key 와 path λ₯Ό λ°˜ν™˜ν•œλ‹€. (μ•„λž˜μ—μ„œ μ„€λͺ…)
  4. λ³€ν™˜ κ³Όμ •μ—μ„œ ν˜„μž¬ ν”„λ‘œμ νŠΈ κ²½λ‘œμ— μƒμ„±λœ νŒŒμΌμ„ μ œκ±°ν•œλ‹€.

μ—¬κΈ°μ„œ key λŠ” κ°μ²΄ 이름(파일λͺ…) μ΄κ³ , path λŠ” 객체 URL μ΄λ‹€.

예λ₯Ό λ“€μ–΄, key λŠ” "example.jpg" 이고, path λŠ” "https://my-s3-bucket.s3.ap-northeast-2.amazonaws.com/example.jpg" 이닀.

 

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
import repick.repickserver.domain.product.domain.AwsS3;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
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;

@Service
@RequiredArgsConstructor
public class AwsS3Service {

    private final AmazonS3 amazonS3;

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

    public AwsS3 upload(MultipartFile multipartFile, String dirName) throws IOException {
        File file = convertMultipartFileToFile(multipartFile)
                .orElseThrow(() -> new IllegalArgumentException("MultipartFile -> File λ³€ν™˜ μ‹€νŒ¨"));

        return upload(file, dirName);
    }

    private AwsS3 upload(File file, String dirName) {
        String key = randomFileName(file, dirName);
        String path = putS3(file, key);
        removeFile(file); // λ‘œμ»¬μ— μƒμ„±λœ File μ‚­μ œ (MultipartFile -> File λ³€ν™˜ ν•  λ•Œ λ‘œμ»¬μ— 파일 생성됨)

        return AwsS3
                .builder()
                .key(key)
                .path(path)
                .build();
    }

    private String randomFileName(File file, String dirName) {
        return dirName + "/" + UUID.randomUUID() + file.getName();
    }

    private String putS3(File uploadFile, String fileName) {
        amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
                .withCannedAcl(CannedAccessControlList.PublicRead));
        return getS3(bucket, fileName);
    }

    private String getS3(String bucket, String fileName) {
        return amazonS3.getUrl(bucket, fileName).toString();
    }

    private void removeFile(File file) {
        file.delete();
    }

    public Optional<File> convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
        File file = new File(System.getProperty("user.dir") + "/" + multipartFile.getOriginalFilename());

        if (file.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(file)){
                fos.write(multipartFile.getBytes());
            }
            return Optional.of(file);
        }
        return Optional.empty();
    }

    public void remove(AwsS3 awsS3) {
        if (!amazonS3.doesObjectExist(bucket, awsS3.getKey())) {
            throw new AmazonS3Exception("객체 " +awsS3.getKey()+ " 이/κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.");
        }
        amazonS3.deleteObject(bucket, awsS3.getKey());
    }
}

 

🌈 AwsS3 μž‘μ„±

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class AwsS3 {

    private String key; // 파일λͺ…
    private String path; // 파일 경둜 (객체 URL)

    public AwsS3() {

    }

    @Builder
    public AwsS3(String key, String path) {
        this.key = key;
        this.path = path;
    }
}

 

🌈 ν…ŒμŠ€νŠΈ

이제 Postman 으둜 ν…ŒμŠ€νŠΈ ν•΄λ³΄μž.

 

이미지 μ—…λ‘œλ“œλ₯Ό μœ„ν•œ POST μš”μ²­μ„ 보내고,

 

 

AWS 에 κ°€μ„œ 버킷을 확인해보면 μ§œμž”! test 폴더 μ•ˆμ— 이미지가 잘 μ—…λ‘œλ“œ λ˜μ—ˆλ‹€. πŸ™Œ

 

 

객체 이름을 μ„ νƒν•΄μ„œ 객체 URL을 λˆ„λ₯΄λ©΄, 

 

 

μ΄λ ‡κ²Œ 방금 올린 사진이 잘 보인닀 😽

 

 

Postman μ‘λ‹΅μœΌλ‘œλŠ” 방금 μ—…λ‘œλ“œν•œ 이미지 파일의 key 와 path κ°€ 잘 μ˜€λŠ” 것을 확인할 수 μžˆλ‹€!!

 

 

 

 

 

μ°Έκ³ :

https://sennieworld.tistory.com/122

https://kim-jong-hyun.tistory.com/78