[Spring] Amazon S3 ์ƒ์„ฑ๊ณผ Spring Boot์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ
Back-end

[Spring] Amazon S3 ์ƒ์„ฑ๊ณผ Spring Boot์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

728x90

๐Ÿ“‘ ๊ฐœ์š”

์ง„ํ–‰ ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค.

์ด๋ฏธ์ง€๋ฅผ Amazon S3์— ์ €์žฅํ•˜๊ธฐ๋กœ ํ•˜์˜€๊ณ ,

Amazon S3 ์ƒ์„ฑ๋ถ€ํ„ฐ Spring Boot์—์„œ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฒƒ๊นŒ์ง€ ์ž‘์„ฑํ•ด๋ณด๋ ค ํ•œ๋‹ค.

 

๐Ÿ“‘ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

SpringBoot : 3.3.5
JDK : 17
build Tools : gradle
Editor : InteliJ

 

๐Ÿ“‘ Amazon S3 ์„ค์ •

๐Ÿ‘‰ ๋ฒ„ํ‚ท ๋งŒ๋“ค๊ธฐ

๋ฒ„ํ‚ท ๋งŒ๋“ค ๋•Œ์˜ ์ž์„ธํ•œ ์„ค์ • ์ •๋ณด๋Š” ์ด ๋ธ”๋กœ๊ทธ์— ์ž˜ ๋‚˜์™€์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

 

(์•„๋ž˜์— ์ž‘์„ฑ๋˜์–ด ์žˆ๋Š” ์„ค์ • ์™ธ์˜ ์„ค์ •์€ ๋””ํดํŠธ ๊ฐ’์œผ๋กœ ๋†”๋‘๊ณ  ๋”ฐ๋กœ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•˜๋‹ค.)

 

์œ„์˜ ๊ฒ€์ƒ‰์ฐฝ์— S3๋ฅผ ๊ฒ€์ƒ‰ํ•˜์—ฌ Amazon S3๋กœ ์ด๋™ํ•ด์ฃผ๊ณ  ์™ผ์ชฝ ๋„ค๋น„๊ฒŒ์ด์…˜๋ฐ”์—์„œ ๋ฒ„ํ‚ท์„ ํด๋ฆญ

 

๋ฒ„ํ‚ท ๋งŒ๋“ค๊ธฐ ํด๋ฆญ

 

๋ฒ„ํ‚ท ์ด๋ฆ„ ์ž‘์„ฑ

 

๊ฐ์ฒด ์†Œ์œ ๊ถŒ์—์„œ ACL ํ™œ์„ฑํ™”๋จ์œผ๋กœ ๋ณ€๊ฒฝ

 

์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Spring ์„œ๋ฒ„ ์ชฝ ์†Œ์Šค์ฝ”๋“œ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฐ ์กฐํšŒ๊ฐ€ ์ด๋ฃจ์–ด์ง€๋Š”๋ฐ,

ํ•ด๋‹น ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋•Œ IAM ๊ณ„์ •์„ ์ƒ์„ฑํ•˜์—ฌ ๋ฒ„ํ‚ท์— ์ ‘๊ทผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ACL ํ™œ์„ฑํ™”๋กœ ์„ ํƒ์ค€๋‹ค.

 

๋ชจ๋“  ํผ๋ธ”๋ฆญ ์—‘์„ธ์Šค๋ฅผ ์ฐจ๋‹จํ•˜๋ฉด ์™ธ๋ถ€์—์„œ ํŒŒ์ผ์„ ์ฝ์ง€ ๋ชปํ•˜๊ฒŒ ํ•œ๋‹ค๋Š” ์˜๋ฏธ์ด๋‹ค.

 

์šฐ๋ฆฌ๋Š” S3์— ์ €์žฅ๋œ ์ด๋ฏธ์ง€๋ฅผ ์šฐ๋ฆฌ์˜ ํ”„๋กœ์ ํŠธ์—์„œ ์ฝ์–ด์™€ ๋„์›Œ์ฃผ์–ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์—

์—‘์„ธ์Šค ์ฐจ๋‹จ ์„ค์ •์„ ํ’€์–ด์ฃผ์—ˆ๋‹ค.

 

๊ธฐ๋ณธ ์•”ํ˜ธํ™”๋ฅผ ํ™œ์„ฑํ™” ํ•˜๋ฉด ๋ฒ„ํ‚ท์— ์ €์žฅ๋˜๋Š” ๋ชจ๋“  ์ƒˆ ๊ฐ์ฒด๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๊ฐ์ฒด๋ฅผ ๋‹ค์šด๋กœ๋“œํ•  ๋•Œ ์•”ํ˜ธ๋ฅผ ๋ณตํ˜ธํ™”ํ•ด์„œ ์ œ๊ณตํ•ด์ค€๋‹ค.

 

(์ด ๋ถ€๋ถ„์— ๋Œ€ํ•ด์„  ์ข€ ๋” ์ž์„ธํžˆ ์•Œ์•„๋ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„ ์ผ๋‹จ ์ผ๋ฐ˜์ ์ธ ์„ค์ • ๋ฐฉ๋ฒ•์„ ๋”ฐ๋ผ ๋น„ํ™œ์„ฑํ™” ํ–ˆ๋‹ค.)

 

๊ทธ๋ฆฌ๊ณ  ๋ฒ„ํ‚ท ๋งŒ๋“ค๊ธฐ ํด๋ฆญ

 

withbee-travel ๋ฒ„ํ‚ท์ด ์ƒ์„ฑ๋œ ๊ฒƒ์„ ํ™•์ธ ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿ‘‰ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ํ…Œ์ŠคํŠธ

์ƒ์„ฑํ•œ ๋ฒ„ํ‚ท์— ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ•ด๋ณด๊ณ  ์ž˜ ๋“ค์–ด๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž

 

์œ„์—์„œ ๋งŒ๋“  ๋ฒ„ํ‚ท ์ด๋ฆ„์„ ๋ˆŒ๋Ÿฌ์ค€๋‹ค.

 

์—…๋กœ๋“œ ํด๋ฆญ

 

์•„๋ฌด ์ด๋ฏธ์ง€๋‚˜ ๋“œ๋ž˜๊ทธ ํ•ด์™€ ํŒŒ์ผ์„ ์—…๋กœ๋“œ ํ•ด์ค€๋‹ค.

 

ํŒŒ์ผ ์—…๋กœ๋“œ ๋œ ๊ฒƒ์„ ํ™•์ธํ•œ ํ›„, ์—…๋กœ๋“œ ํด๋ฆญ

 

์—…๋กœ๋“œ๊ฐ€ ์ž˜ ๋œ ๊ฒƒ์„ ํ™•์ธํ•œ ํ›„,

์—…๋กœ๋“œํ•œ ํŒŒ์ผ์„ ํด๋ฆญํ•ด ํŒŒ์ผ ์ƒ์„ธ๋กœ ์ด๋™ํ•ด์ค€๋‹ค.

 

๊ฐ์ฒด URL์„ ๋ณต์‚ฌํ•ด์„œ, ์ด๋ฏธ์ง€๊ฐ€ ์ž˜ ๋„์›Œ์ง€๋Š”์ง€ ํ™•์ธ

 

๊ทธ๋Ÿผ ์ด๋Ÿฐ ์‹์œผ๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์—†๋‹ค๋Š” ํ™”๋ฉด์ด ๋‚˜์˜จ๋‹ค.

 

๋ฒ„ํ‚ท ์ •์ฑ…์„ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.

 

๐Ÿ‘‰ ๋ฒ„ํ‚ท ์ •์ฑ… ์„ค์ •

๋ฒ„ํ‚ท์˜ ๊ถŒํ•œ ํƒญ์œผ๋กœ ์ด๋™

 

๋ฐ‘์œผ๋กœ ๋‚ด๋ฆฌ๋‹ค๋ณด๋ฉด ๋‚˜์˜ค๋Š” ๋ฒ„ํ‚ท ์ •์ฑ…์˜ ํŽธ์ง‘ ํด๋ฆญ

 

๋ฒ„ํ‚ท ARN ๋ณต์‚ฌ ํ›„ ์ •์ฑ… ์ƒ์„ฑ๊ธฐ ํด๋ฆญ

 

๋‹ค์Œ์˜ ์ •๋ณด๋“ค์„ ์ž…๋ ฅํ•ด์ค€๋‹ค.

Select Type of Policy: S3 Bucket Policy

Principal: *

Actions: GetObject

Amazon Resource Name: {๋ณต์‚ฌํ•œ ARN}/*

 

์ž…๋ ฅ ํ›„ Add Statement ํด๋ฆญ

 

๋ฐ‘์— ์ƒ๊ธด ์ •์ฑ…์„ ํ™•์ธ ํ›„, Generate Policy ํด๋ฆญ

 

๊ทธ๋Ÿผ json ํ˜•์‹์œผ๋กœ ๋œ ์ •์ฑ…์ด ๋œฌ๋‹ค.

ํ•ด๋‹น json ๋ณต์‚ฌ

 

๋‹ค์‹œ ๋ฒ„ํ‚ท ์ •์ฑ… ํŽธ์ง‘ ์ฐฝ์œผ๋กœ ๋Œ์•„๊ฐ€์„œ

์—ฌ๊ธฐ์— ๋ณต์‚ฌํ•œ ๊ฑฐ ๋ถ™์—ฌ๋„ฃ์€ ํ›„ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ €์žฅ ํด๋ฆญ

 

๊ทธ๋ฆฌ๊ณ  ๋‹ค์‹œ ์ด๋ฏธ์ง€ ์ฃผ์†Œ๋กœ ๋“ค์–ด๊ฐ€๋ณด๋ฉด

์ด๋ฏธ์ง€๊ฐ€ ์ž˜ ๋กœ๋“œ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!

 

๐Ÿ‘‰ IAM ์‚ฌ์šฉ์ž ์ƒ์„ฑํ•˜๊ธฐ/ํŽธ์ง‘ํ•˜๊ธฐ

์ƒ์„ฑํ•˜๊ธฐ

S3์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” IAM ์‚ฌ์šฉ์ž์—๊ฒŒ S3์ ‘๊ทผ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๊ณ 

ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ์—‘์„ธ์Šค ํ‚ค, ์‹œํฌ๋ฆฟ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

 

์œ„์˜ ๊ฒ€์ƒ‰์— IAM์„ ๊ฒ€์ƒ‰ ํ›„ ์—‘์„ธ์Šค ๊ด€๋ฆฌ → ์‚ฌ์šฉ์ž๋กœ ์ด๋™ ํ›„ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํด๋ฆญ

 

์‚ฌ์šฉ์ž ์ด๋ฆ„ ์ž‘์„ฑ ํ›„ ๋‹ค์Œ ํด๋ฆญ

 

์ง์ ‘ ์ •์ฑ… ์—ฐ๊ฒฐ ์„ ํƒ ํ›„ AmazonS3FullAccess ์„ ํƒ ํ›„ ๋‹ค์Œ ํด๋ฆญ

 

์ž˜ ์„ ํƒ ๋˜์—ˆ๋Š”์ง€ ๊ฒ€ํ†  ํ›„ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํด๋ฆญ

 

ํŽธ์ง‘ํ•˜๊ธฐ

์‚ฌ์‹ค ๋‚˜๋Š” ์šฐ๋ฆฌFIS ์•„์นด๋ฐ๋ฏธ์—์„œ ์ œ๊ณตํ•ด์ค€ AWS ๊ณ„์ •์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ IAM๊ฐ€ ์žˆ์—ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ด๋ฏธ ์žˆ๋Š” IAM์— AmazonS3FullAccess ๊ถŒํ•œ๋งŒ ์ถ”๊ฐ€ํ•ด์คฌ๋‹ค.

 

๊ถŒํ•œ์„ ์ถ”๊ฐ€ํ•  ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ํด๋ฆญ

 

๊ถŒํ•œ ์ถ”๊ฐ€ ํด๋ฆญ

 

์ง์ ‘ ์ •์ฑ… ์—ฐ๊ฒฐ ์„ ํƒ ํ›„ AmazonS3FullAccess ์„ ํƒ ํ›„ ๋‹ค์Œ ํด๋ฆญ

 

๊ฒ€ํ†  ํ›„ ๊ถŒํ•œ ์ถ”๊ฐ€ ํด๋ฆญ

 

๐Ÿ‘‰ IAM ์—‘์„ธ์Šค ํ‚ค ์ƒ์„ฑํ•˜๊ธฐ

๋ฐฉ๊ธˆ ์ƒ์„ฑํ•œ ์‚ฌ์šฉ์ž(or ๊ถŒํ•œ ์ถ”๊ฐ€ํ•œ ์‚ฌ์šฉ์ž) ์ด๋ฆ„์„ ๋ˆŒ๋Ÿฌ ์ƒ์„ธ์— ๋“ค์–ด์˜จ ํ›„ ์—‘์„ธ์Šค ํ‚ค ๋งŒ๋“ค๊ธฐ ํด๋ฆญ

 

์ด ์ค‘์—์„œ ์•„๋ฌด๊ฑฐ๋‚˜ ํ•˜๋‚˜ ์„ ํƒํ•˜๊ณ  ๋‹ค์Œ ํด๋ฆญ

(๋‚˜๋Š” AWS ์ปดํ“จํŒ… ์„œ๋น„์Šค์—์„œ ์‹คํ–‰๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์„ ํƒํ–ˆ๋‹ค.)

 

์„ค๋ช… ํƒœ๊ทธ ๊ฐ’์€ ์„ ํƒ ์‚ฌํ•ญ์ด๋ผ ์ ์–ด์ฃผ์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

(๋‚˜๋Š” withbee-travel์˜ s3 ๋ฒ„ํ‚ท๊ณผ ์—ฐ๊ฒฐํ–ˆ๋‹ค๋Š” ๋œป์œผ๋กœ ํƒœ๊ทธ๋ฅผ ์ ์–ด์ฃผ์—ˆ๋‹ค.)

 

์—‘์„ธ์Šค ํ‚ค ๋งŒ๋“ค๊ธฐ ํด๋ฆญ

 

๋งŒ๋“ค์–ด์ง„ ์—‘์„ธ์Šค ํ‚ค๋ฅผ .csv ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ๋ฅผ ํด๋ฆญํ•ด์„œ ๋‹ค์šด ๋ฐ›์•„์ค€๋‹ค.

 

๐Ÿ“‘ Spring์— ์—ฐ๋™ํ•˜๊ธฐ

๐Ÿ‘‰ dependency ์ถ”๊ฐ€

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

 

๐Ÿ‘‰ properties ์ถ”๊ฐ€

์ผ๋‹จ, aws ๊ด€๋ จ properties๋ฅผ ๋”ฐ๋กœ ์ž‘์„ฑํ•ด์ฃผ๊ธฐ ์œ„ํ•ด

application.properties์— aws.properties๋„ ์ฝ์–ด์ฃผ๋ผ๊ณ  ์ž‘์„ฑํ•ด์ค€๋‹ค.

# properties ํŒŒ์ผ ์ถ”๊ฐ€
spring.config.import=aws.properties

 

๊ทธ๋ฆฌ๊ณ  aws.properties์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ๋“ค์„ ์ ์–ด์ค€๋‹ค.

# IAM ์•ก์„ธ์Šค ํ‚ค
cloud.aws.credentials.accessKey=

# IAM ๋น„๋ฐ€ ์•ก์„ธ์Šค ํ‚ค
cloud.aws.credentials.secretKey=

# ๋ฆฌ์ „ ์ •๋ณด
cloud.aws.region.static=ap-northeast-2

# ๋ฒ„ํ‚ท ์ด๋ฆ„
cloud.aws.s3.bucket=withbee-travel

# ์ด๋ฏธ์ง€ URL์˜ ๋„๋ฉ”์ธ ์ •๋ณด(์ด๋ฏธ์ง€๋ฅผ delete ํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š”ํ•จ)
cloud.aws.s3.bucket.domain=https://{๋ฒ„ํ‚ท ์ด๋ฆ„}.s3.{๋ฆฌ์ „ ์ •๋ณด}.amazonaws.com/

 

๐Ÿ‘‰ Config ์ž‘์„ฑ

package withbeetravel.config;

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 org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // ์„ค์ • ํŒŒ์ผ์„ ์ฝ๊ธฐ ์œ„ํ•œ annotation
public class S3Config {

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

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

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

    @Bean
    public AmazonS3 s3Client() {

        AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

        return AmazonS3ClientBuilder.standard()
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .withRegion(region)
                .build();
    }
}

 

๐Ÿ‘‰ S3Uploader ์ž‘์„ฑ

package withbeetravel.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.PutObjectRequest;
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.UUID;

@Service
public class S3Uploader {

    private final AmazonS3 amazonS3;
    private final String bucket;

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

    public S3Uploader(AmazonS3 amazonS3, @Value("${cloud.aws.s3.bucket}") String bucket) {
        this.amazonS3 = amazonS3;
        this.bucket = bucket;
    }

    // ์ด๋ฏธ์ง€ ์ €์žฅ
    // file := s3์— ์ €์žฅํ•  ์ด๋ฏธ์ง€ ํŒŒ์ผ
    // dirName := ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ €์žฅํ•  s3 ๋””๋ ‰ํ† ๋ฆฌ
    public String upload(MultipartFile file, String dirName) throws IOException {

        // ํŒŒ์ผ์˜ ์›๋ž˜ ์ด๋ฆ„์—์„œ ๊ณต๋ฐฑ์„ ์ œ๊ฑฐ
        String originalFileName = file.getOriginalFilename().replaceAll("\\s", "_");

        // ์œ ๋‹ˆํฌํ•œ ํŒŒ์ผ๋ช…์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด UUID๋ฅผ ํŒŒ์ผ๋ช…์— ์ถ”๊ฐ€
        String uuid = UUID.randomUUID().toString();
        String uniqueFileName = uuid + "_" + originalFileName;

        // ๋””๋ ‰ํ† ๋ฆฌ ์œ„์น˜์™€ ํŒŒ์ผ๋ช… ํ•ฉ์น˜๊ธฐ
        String fileName = dirName + "/" + uniqueFileName;

        // MultipartFile -> File
        File uploadFile =convert(file, uniqueFileName);

        // S3์— ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
        String uploadImageUrl = putS3(uploadFile, fileName);

        // ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
        uploadFile.delete();

        // S3์— ์ €์žฅ๋œ ์ด๋ฏธ์ง€์˜ URL ๋ฆฌํ„ด
        return uploadImageUrl;
    }

    // ์ด๋ฏธ์ง€ ์‚ญ์ œ
    // filName := ์‚ญ์ œํ•  ์ด๋ฏธ์ง€๋ช…(URL ํ˜•์‹)
    public void delete(String fileName) {
        if(fileName.startsWith(bucketDomain)) {
            amazonS3.deleteObject(bucket, fileName.substring(bucketDomain.length()));
        }
    }

    // ์ด๋ฏธ์ง€ ์ˆ˜์ •
    // newFile := ์ƒˆ๋กœ ์ €์žฅํ•  ์ด๋ฏธ์ง€ ํŒŒ์ผ
    // oldFileName := ๊ธฐ์กด์— ์ €์žฅ๋˜์–ด ์žˆ๋˜ ์ด๋ฏธ์ง€๋ช…(URL ํ˜•์‹)
    // dirName := ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ €์žฅํ•  s3 ๋””๋ ‰ํ† ๋ฆฌ
    public String update(MultipartFile newFile, String oldFileName, String dirName) throws IOException {
        // ๊ธฐ์กด ํŒŒ์ผ ์‚ญ์ œ
        delete(oldFileName);
        // ์ƒˆ ํŒŒ์ผ ์—…๋กœ๋“œ
        return upload(newFile, dirName);
    }

    // MultipartFile -> File
    private File convert(MultipartFile file, String uniqueFileName) throws IOException {

        File convertFile = new File(uniqueFileName);
        if (convertFile.createNewFile()) {
            try (FileOutputStream fos = new FileOutputStream(convertFile)) {
                fos.write(file.getBytes());
            } catch (IOException e) {
                System.err.println("ํŒŒ์ผ ๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: " +  e.getMessage());
                throw e;
            }
            return convertFile;
        }
        throw new IllegalArgumentException("ํŒŒ์ผ ๋ณ€ํ™˜์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. " + file.getOriginalFilename());
    }

    // S3์— ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
    private String putS3(File uploadFile, String fileName) {
        amazonS3.putObject(new PutObjectRequest(bucket, fileName, uploadFile)
                .withCannedAcl(CannedAccessControlList.PublicRead));
        return amazonS3.getUrl(bucket, fileName).toString();
    }
}

์‹ค์งˆ์ ์œผ๋กœ S3Uploader์—์„œ ํ˜ธ์ถœ๋˜๋Š” ๋ฉ”์„œ๋“œ๋Š” upload, delete, update์ด๋‹ค.

 

๐Ÿ‘‰ Service์—์„œ S3Uploader ์‚ฌ์šฉ

package withbeetravel.service;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import withbeetravel.domain.SharedPayment;
import withbeetravel.exception.CustomException;
import withbeetravel.exception.error.PaymentErrorCode;
import withbeetravel.exception.error.ValidationErrorCode;
import withbeetravel.repository.SharedPaymentRepository;

import java.io.IOException;

@Service
@RequiredArgsConstructor
public class SharedPaymentService {

    private final S3Uploader s3Uploader;

    private final SharedPaymentRepository sharedPaymentRepository;

    // S3์— ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•  ๊ฒฝ๋กœ
    private static final String SHARED_PAYMENT_IMAGE_DIR = "shared-payments";

    @Transactional
    public boolean addAndUpdatePaymentRecord(
            Long travelId,
            Long sharedPaymentId,
            MultipartFile image,
            String comment,
            boolean isMainImage) {

        // SharedPayment Entity ๊ฐ€์ ธ์˜ค๊ธฐ
        SharedPayment sharedPayment = sharedPaymentRepository.findById(sharedPaymentId)
                .orElseThrow(() -> new CustomException(PaymentErrorCode.SHARED_PAYMENT_NOT_FOUND));

        // ์ด๋ฏธ์ง€ ์ถ”๊ฐ€, ์ˆ˜์ •, ์‚ญ์ œ
        String paymentImage = sharedPayment.getPaymentImage(); // ์›๋ž˜ ์ด๋ฏธ์ง€
        if(image != null) { // image๊ฐ€ ์ƒˆ๋กœ ๋“ค์–ด์™”๋‹ค๋ฉด S3์— ์ €์žฅ ํ›„ Entity ๋ณ€๊ฒฝ

            // ์ด๋ฏธ์ง€ ์ €์žฅํ•  S3 ๋””๋ ‰ํ† ๋ฆฌ ์ •๋ณด
            String dirName = SHARED_PAYMENT_IMAGE_DIR + "/" + travelId;
            
            try {
                if(paymentImage != null) { // ํ•ด๋‹น ๊ณต๋™๊ฒฐ์ œ ๋‚ด์—ญ์— ์ด๋ฏธ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์—…๋ฐ์ดํŠธ
                    sharedPayment.updatePaymentImage(s3Uploader.update(image, paymentImage, dirName));
                } else { // ์—†๋‹ค๋ฉด ์—…๋กœ๋“œ
                    sharedPayment.updatePaymentImage(s3Uploader.upload(image, dirName));
                }
            } catch (IOException e) { // ์ด๋ฏธ์ง€ ์ €์žฅ์— ์‹คํŒจํ–ˆ์„ ๊ฒฝ์šฐ
                throw new CustomException(ValidationErrorCode.IMAGE_PROCESSING_FAILED);
            }
        }
        // image๊ฐ€ null๋กœ ๋“ค์–ด์™”๋‹ค๋ฉด, ๊ธฐ์กด ์ด๋ฏธ์ง€ ์‚ญ์ œ
        else {
            // S3์— ์ด๋ฏธ์ง€ ์‚ญ์ œ
            s3Uploader.delete(paymentImage);
            // entity์—์„œ ์ด๋ฏธ์ง€ ์ •๋ณด ์‚ญ์ œ
            sharedPayment.updatePaymentImage(null);
        }

        ...
    }
}

 

 

๐Ÿ“‘ ์ฐธ๊ณ 

https://jforj.tistory.com/260

https://velog.io/@yes_jihyeon/AWS-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EC%A0%80%EC%9E%A5%EC%9D%84-%EC%9C%84%ED%95%9C-S3-%EB%B2%84%ED%82%B7-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0-SpringBootGradle#4-%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90-%EC%97%B0%EB%8F%99%ED%95%98%EA%B8%B0

728x90