๊ด€๋ฆฌ ๋ฉ”๋‰ด

๐‘†๐‘ข๐‘›๐‘ โ„Ž๐‘–๐‘›๐‘’ ๐‘Ž๐‘“๐‘ก๐‘’๐‘Ÿ ๐‘Ÿ๐‘Ž๐‘–๐‘›โœง

[AWS] ์ฒจ๋ถ€ํŒŒ์ผ AWS S3์— ์ €์žฅ & ๋‹ค์šด๋กœ๋“œํ•˜๊ธฐ ๋ณธ๋ฌธ

๐—ฃ๐—ฟ๐—ผ๐—ด๐—ฟ๐—ฎ๐—บ๐—บ๐—ถ๐—ป๐—ด๐Ÿ’ป/๐€๐–๐’

[AWS] ์ฒจ๋ถ€ํŒŒ์ผ AWS S3์— ์ €์žฅ & ๋‹ค์šด๋กœ๋“œํ•˜๊ธฐ

๐ŸคRyusun๐Ÿค 2024. 3. 18. 19:45

๊ณผ๊ฑฐ ํ”„๋กœ์ ํŠธ์—์„œ ๋กœ์ปฌ์— ์ด๋ฏธ์ง€๋ž‘ ํŒŒ์ผ์„ ์ €์žฅ, ์กฐํšŒ, ๋‹ค์šด๋กœ๋“œ ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ์  ์žˆ๋‹ค.

์ด๊ฑธ ์ด์ œ S3์— ์ €์žฅํ•ด๋ณด๊ณ  ์‹ถ์–ด ํ•œ๋ฒˆ ๋ฆฌํŒฉํ† ๋ง์„ ํ•ด๋ณด์•˜๋‹ค.

 


์šฐ์„  ๋ฉ€ํ‹ฐํŒŒํŠธ(Multipart) ํŒŒ์ผ ๊ด€๋ จ ์„ค์ •์„ ํ•ด์ค˜์•ผํ•œ๋‹ค.

spring:
  servlet:
    multipart:
      enabled: true # ๋ฉ€ํ‹ฐํŒŒํŠธ ์—…๋กœ๋“œ ์ง€์›์—ฌ๋ถ€ (default: true)
      max-file-size: 100MB # ํ•œ๊ฐœ ํŒŒ์ผ์˜ ์ตœ๋Œ€ ์‚ฌ์ด์ฆˆ (default: 1MB)
      max-request-size: 100MB #

 

 

์ด์ œ S3๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผํ•œ๋‹ค.

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

 

๊ทธ๋ฆฌ๊ณ  S3 ๋ฒ„ํ‚ท๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

 

 

ํ•ด๋‹น ๋ฒ„ํ‚ท์€ ๋‹จ์ˆœ ํ”„๋กœ์ ํŠธ์šฉ์ด๋ฏ€๋กœ ๋ณด์•ˆ์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ํ•ด์ฃผ๊ฒ ๋‹ค.

๋ณด์•ˆ์ด ํ•„์š”ํ•œ๊ฒฝ์šฐ ํผ๋ธ”๋ฆญ ์•ก์„ธ์Šค๋ฅผ ์ฐจ๋‹จํ•˜๋ฉด ์•ˆ๋œ๋‹ค.

๋ฒ„ํ‚ท์„ ์ƒ์„ฑํ•œํ›„ IAM์œผ๋กœ ๊ฐ€์„œ s3 ์—ญํ• ์„ ๋ถ€์—ฌ๋ฐ›์„ ์‚ฌ์šฉ์ž๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

ํ›„์— ์ด ์‚ฌ์šฉ์ž์˜ accesskey๋ฅผ ๊ฐ€์ง€๊ณ  ๋ฒ„ํ‚ท์— ์—…๋กœ๋“œ๋ฅผ ํ• ๊ฒƒ์ด๋‹ค.

 

IAM ์‚ฌ์šฉ์ž๋ฅผ ๋งŒ๋“ ํ›„ S3FullAccess ์ •์ฑ…์„ ๋ถ€์—ฌํ•œ๋‹ค.

์ƒ์„ฑํ•œํ›„ ์˜ค๋ฅธ์ชฝ ์ƒ๋‹จ์˜ ์•ก์„ธ์Šค ํ‚ค๋ฅผ ๋งŒ๋“ค๊ธฐ๋ฅผ ๋ˆŒ๋Ÿฌ ์•ก์„ธ์Šคํ‚ค๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

๋ฐœ๊ธ‰๋ฐ›๋Š” access-key, secret-key๋ฅผ application.yml์— ๋„ฃ์–ด์ค€๋‹ค.

 

cloud:
  aws:
    s3:
      bucket: <S3 ๋ฒ„ํ‚ท ์ด๋ฆ„>
    credentials:
      access-key: <์ €์žฅํ•ด๋†“์€ ์•ก์„ธ์Šค ํ‚ค>
      secret-key: <์ €์žฅํ•ด๋†“์€ ๋น„๋ฐ€ ์•ก์„ธ์Šค ํ‚ค>
    region:
      static: ap-northeast-2
      auto: false
    stack:
      auto: false

 

cloud.aws.stack.auto=false

EC2์—์„œ Spring Cloud ํ”„๋กœ์ ํŠธ๋ฅผ ์‹คํ–‰์‹œํ‚ค๋ฉด ๊ธฐ๋ณธ์œผ๋กœ CloudFormation ๊ตฌ์„ฑ์„ ์‹œ์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ค์ •ํ•œ CloudFormation์ด ์—†์œผ๋ฉด ํ”„๋กœ์ ํŠธ ์‹คํ–‰์ด ๋˜์ง€ ์•Š๋Š”๋‹ค. ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋„๋ก false๋กœ ์„ค์ •ํ•œ๋‹ค.

 

 

S3Config.java

AWS S3(Simple Storage Service)์™€์˜ ํ†ต์‹ ์„ ์œ„ํ•œ AmazonS3Client ๋นˆ(bean)์„ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค.

 

@Configuration
public class S3Config {

    @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 AmazonS3Client amazonS3Client() {
        BasicAWSCredentials awsCredentials= new BasicAWSCredentials(accessKey, secretKey);
        return (AmazonS3Client)AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
                .build();
    }
}

 

 

Controller

    @ApiOperation(value = "๊ฒŒ์‹œ๊ธ€ ๋“ฑ๋ก", notes = "๊ฒŒ์‹œ๊ธ€์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.")
    @PostMapping("save")
    public ResponseEntity<String> create(
            @RequestPart(value = "board") BoardRequest request,
            @RequestPart(value = "image", required = false) MultipartFile image,
            @RequestPart(value = "file", required = false) MultipartFile file){
        
        try {
            //s3
            String fileName= image.getOriginalFilename();
            String fileUrl= "https://" + bucket + "/image" +fileName;
            ObjectMetadata metadata= new ObjectMetadata();
            metadata.setContentType(file.getContentType());
            metadata.setContentLength(file.getSize());
            amazonS3Client.putObject(bucket,fileName,file.getInputStream(),metadata);
            return ResponseEntity.ok(fileUrl);
        } catch (IOException e) {
            throw new AppException(ErrorCode.INTERNAL_SERVER_ERROR, "์ €์žฅ์— ์‹คํŒจํ•˜์˜€์Šต๋‹ˆ๋‹ค.");
        }
    }

 

S3์— ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ , ์—…๋กœ๋“œ๋œ ์ด๋ฏธ์ง€์˜ URL์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

 

  • String fileName = image.getOriginalFilename();
    • ์—…๋กœ๋“œํ•œ ํŒŒ์ผ์˜ ์›๋ณธ ์ด๋ฆ„์„ fileName ๋ณ€์ˆ˜์— ์ €์žฅ
  • String fileUrl = "https://" + bucket + "/image" + fileName;
    • fileUrl ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ์—…๋กœ๋“œํ•  ํŒŒ์ผ์˜ URL์„ ๊ตฌ์„ฑํ•œ๋‹ค. ์—ฌ๊ธฐ์„œ bucket์€ S3 ๋ฒ„ํ‚ท์˜ ์ด๋ฆ„์ด๋ฉฐ, fileName์€ ์—…๋กœ๋“œํ•  ํŒŒ์ผ์˜ ์ด๋ฆ„์ด๋‹ค. "/image"๋Š” ํŒŒ์ผ์„ ์ €์žฅํ•  S3 ๋‚ด์˜ ๊ฒฝ๋กœ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค. ์‹ค์ œ๋กœ S3 ๋ฒ„ํ‚ท ๋‚ด์—์„œ๋Š” ์ด ๊ฒฝ๋กœ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒŒ์ผ์ด ์ €์žฅ๋œ๋‹ค.
  • ObjectMetadata metadata = new ObjectMetadata();
    • ObjectMetadata ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ด ๊ฐ์ฒด๋Š” S3์— ์—…๋กœ๋“œํ•  ํŒŒ์ผ๊ณผ ๊ด€๋ จ๋œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ๋‹ค.
  • metadata.setContentType(file.getContentType());
    • ์—…๋กœ๋“œํ•  ํŒŒ์ผ์˜ ์ปจํ…์ธ  ํƒ€์ž…์„ ์„ค์ •ํ•œ๋‹ค. file.getContentType() ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—…๋กœ๋“œํ•œ ํŒŒ์ผ์˜ MIME ํƒ€์ž…์„ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์„ค์ •ํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๋ฏธ์ง€ ํŒŒ์ผ์ด๋ฉด image/jpeg ๋˜๋Š” image/png์™€ ๊ฐ™์€ ๊ฐ’์ด ๋œ๋‹ค.
  • metadata.setContentLength(file.getSize());
    • ์—…๋กœ๋“œํ•  ํŒŒ์ผ์˜ ํฌ๊ธฐ๋ฅผ ์„ค์ •ํ•œ๋‹ค. file.getSize() ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํŒŒ์ผ์˜ ํฌ๊ธฐ(๋ฐ”์ดํŠธ ๋‹จ์œ„)๋ฅผ ๊ฐ€์ ธ์˜จ ํ›„, ์ด๋ฅผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ ์ปจํ…์ธ  ๊ธธ์ด๋กœ ์„ค์ •ํ•œ๋‹ค.
  • amazonS3Client.putObject(bucket, fileName, file.getInputStream(), metadata);
    • AmazonS3Client์˜ putObject ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŒŒ์ผ์„ S3 ๋ฒ„ํ‚ท์— ์—…๋กœ๋“œํ•œ๋‹ค. ์ด ๋ฉ”์†Œ๋“œ๋Š” ๋ฒ„ํ‚ท ์ด๋ฆ„(bucket), ์ €์žฅ๋  ํŒŒ์ผ ์ด๋ฆ„(fileName), ์—…๋กœ๋“œํ•  ํŒŒ์ผ์˜ InputStream(์—ฌ๊ธฐ์„œ๋Š” file.getInputStream()), ๊ทธ๋ฆฌ๊ณ  ํŒŒ์ผ๊ณผ ๊ด€๋ จ๋œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(metadata)๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค.

 

์ด์ œ S3 ์—์„œ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜๋Š” REST API ์—”๋“œํฌ์ธํŠธ๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด์ž

    @ApiOperation(value = "์ฒจ๋ถ€ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ", notes = "S3์— ์ €์žฅ๋œ ์ฒจ๋ถ€ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.")
    @GetMapping("/s3/download/{fileName}")
    public ResponseEntity<byte[]> download(@PathVariable String fileName) throws IOException {
        S3Object o = amazonS3Client.getObject(new GetObjectRequest(bucket, fileName));
        S3ObjectInputStream objectInputStream = o.getObjectContent();
        byte[] bytes = IOUtils.toByteArray(objectInputStream);

        String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        httpHeaders.setContentLength(bytes.length);
        httpHeaders.setContentDispositionFormData("attachment", encodedFileName);

        // ํŒŒ์ผ์˜ ๋ฐ”์ดํŠธ ๋ฐฐ์—ด๊ณผ ์„ค์ •๋œ HTTP ํ—ค๋”๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ResponseEntity ๊ฐ์ฒด๋ฅผ ์ง์ ‘ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
        return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
    }
  • S3Object o = amazonS3Client.getObject(new GetObjectRequest(bucket, fileName));
    • S3 ๋ฒ„ํ‚ท์—์„œ bucket์€ S3 ๋ฒ„ํ‚ท์˜ ์ด๋ฆ„, fileName์€ ๋‹ค์šด๋กœ๋“œํ•˜๋ ค๋Š” ํŒŒ์ผ์˜ ์ด๋ฆ„์œผ๋กœ ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜จ๋‹ค.
  • S3ObjectInputStream objectInputStream = o.getObjectContent();:
    • ๊ฐ€์ ธ์˜จ S3 ๊ฐ์ฒด์—์„œ ์ž…๋ ฅ ์ŠคํŠธ๋ฆผ์„ ์–ป์–ด, ํŒŒ์ผ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š”๋‹ค.
  • byte[] bytes = IOUtils.toByteArray(objectInputStream);
    • Apache Commons IO ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ IOUtils.toByteArray ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž…๋ ฅ ์ŠคํŠธ๋ฆผ์„  ํŒŒ์ผ์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์€ ๋ฐ”์ดํŠธ ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜ํ™˜๋‹ค.
  • String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
    • ํŒŒ์ผ ์ด๋ฆ„์„ URL ์ธ์ฝ”๋”ฉํ•˜์—ฌ HTTP ํ—ค๋”์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค. ๊ณต๋ฐฑ์€ %20์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
  • httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
    • ์‘๋‹ต์˜ ์ฝ˜ํ…์ธ  ํƒ€์ž…์„ ์ด์ง„ ๋ฐ์ดํ„ฐ(APPLICATION_OCTET_STREAM)๋กœ ์„ค์ •ํ•œ๋‹ค.
  • httpHeaders.setContentLength(bytes.length);
    • ์‘๋‹ต ๋ณธ๋ฌธ์˜ ๊ธธ์ด(ํŒŒ์ผ ๋ฐ์ดํ„ฐ์˜ ํฌ๊ธฐ)๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • httpHeaders.setContentDispositionFormData("attachment", encodedFileName);
    • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‘๋‹ต์„ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ๋กœ ์ฒ˜๋ฆฌํ•˜๋„๋ก Content-Disposition ํ—ค๋”๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ, ์ธ์ฝ”๋”ฉ๋œ ํŒŒ์ผ ์ด๋ฆ„์„ ์‚ฌ์šฉํ•œ๋‹ค.
    •  

 

ํฌ์ŠคํŠธ๋งจ์—์„œ form-dataํ˜•์‹์œผ๋กœ ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ  api๋ฅผ ํ˜ธ์ถœํ•ด๋ณด์ž

 

 

 

์ž˜ ์ €์žฅ์ด ๋˜์—ˆ์œผ๋ฉด S3์— ์ €์žฅ๋œ ์ด๋ฏธ์ง€ URL์ด ๋ฐ˜ํ™˜๋˜๊ณ 

S3์—๋„ ์ž˜ ์ €์žฅ์ด ๋œ๋‹ค.

 

 

๋‹ค์šด๋กœ๋“œ๋„ ํ•ด๋ณด์ž

 

ํฌ์ŠคํŠธ๋งจ์œผ๋กœ api ๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด 200์ด ๋œจ๊ณ , ์‹ค์ œ ์›น์—์„œ url๋ฅผ ์ž…๋ ฅํ•˜๋ฉด ๋‹ค์šด์ด ๋ฐ›์•„์ง„๋‹ค.

 

 

 

 

 

๋~!!