BackEnd/Spring

์Šคํ”„๋ง๋ถ€ํŠธ S3 PreSignedURL์„ ํ†ตํ•œ ์ด๋ฏธ์ง€ ์ €์žฅ

PgmJUN 2024. 6. 21. 01:04

 

 

 

๐Ÿค” ์ ์šฉ์„ ๊ณ ๋ คํ•œ ์ด์œ 

“์šฐ๋ฆฌ์ง‘ ๋ฐ˜๋ ค๋™๋ฌผ์˜ AI ํ”„๋กœํ•„ ์‚ฌ์ง„๊ด€” ํŽซํŠœ๋””์˜ค ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฐ˜๋ ค๋™๋ฌผ AI ํ”„๋กœํ•„ ์‚ฌ์ง„์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด Stable Diffusion ๋ชจ๋ธ์— ํ•™์Šต์‹œํ‚ฌ 10~12์žฅ์˜ ์ด๋ฏธ์ง€๋ฅผ S3์— ์ €์žฅํ•ด์•ผํ•˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•˜์˜€๋‹ค.

 

์ผ๋ฐ˜์ ์œผ๋กœ ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ์— ํด๋ผ์ด์–ธํŠธ→์„œ๋ฒ„ ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ „์†กํ•˜๊ณ  ์„œ๋ฒ„→S3 ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅ์‹œํ‚ค๋Š” ๊ตฌ์กฐ๋ฅผ ๋Œ€์ฒด์ ์œผ๋กœ ๋งŽ์ด ๋ด์™”์„ ๊ฒƒ์ด๋‹ค.

 

ํ•˜์ง€๋งŒ 10์žฅ~12์žฅ์ด๋‚˜ ๋˜๋Š” ์ด๋ฏธ์ง€๋ฅผ ์„œ๋ฒ„๋กœ ์ „์†กํ•˜๋Š” ์ผ์€ ์ผ๋ฐ˜์ ์€ ์š”์ฒญ๊ณผ๋Š” ๋‹ค๋ฅด๊ฒŒ ์„œ๋ฒ„์— ๊ฐ€ํ•˜๋Š” ๋ถ€ํ•˜๊ฐ€ ์ปท๊ณ , ๊ธˆ์ „์ ์ธ ์ด์Šˆ๋กœ AWS ํ”„๋ฆฌํ‹ฐ์–ด์˜ t2.micro๋ฅผ ์‚ฌ์šฉ ์ค‘์ธ ํ˜„์žฌ ์ธํ”„๋ผ ํ™˜๊ฒฝ์—์„œ ์ตœ๋Œ€ํ•œ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๋Š” ์ž‘์—…์€ ๋ถˆ๊ฐ€ํ”ผํ–ˆ๋‹ค.

๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€ ์ €์žฅ์˜ ์ฑ…์ž„์„ ํด๋ผ์ด์–ธํŠธ ์ธก์— ์œ„์ž„ํ•˜์—ฌ ์„œ๋ฒ„์— ๊ฐ€ํ•ด์ง€๋Š” ๋ถ€ํ•˜๋ฅผ ๊ฐ์†Œ์‹œํ‚ค๊ณ ์ž PreSignedURL ๋ฐฉ์‹์„ ๋„์ž…ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜์˜€๋‹ค.

 

 


 

๐Ÿ’ญ PreSignedURL์ด๋ž€?

PreSignedURL์€ S3 ๋ฒ„ํ‚ท์˜ ํŠน์ • ๋””๋ ‰ํ† ๋ฆฌURL์— ๋Œ€ํ•˜์—ฌ ๋ฏธ๋ฆฌ ์ ‘๊ทผ์— ๋Œ€ํ•œ ์„œ๋ช…(๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๋Š” ์ผ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค)์„ ๋ฐ›์€ ํ›„, ์„œ๋ช…๋œ URL, ์ฆ‰ PreSignedURL ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋„˜๊ฒจ์ฃผ์–ด ํŠน์ • ๊ถŒํ•œ์— ๋Œ€ํ•œ ์š”๊ตฌ์—†์ด S3์— ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์‹์ด๋‹ค.

์›๋ž˜๋Š” S3์— ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๋ ค๋ฉด S3 ๊ด€๋ จ ๊ถŒํ•œ์„ ๊ฐ€์ง„ IAM ๋˜๋Š” ๋ฃจํŠธ ๊ณ„์ •์œผ๋กœ ์ ‘๊ทผํ•ด์•ผ ํ•˜์ง€๋งŒ, ๋ฏธ๋ฆฌ ์„œ๋ช…์„ ํ†ตํ•ด ๊ถŒํ•œ์„ ๊ฐ€์ง„ PreSignedURL์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ถŒํ•œ์— ๋Œ€ํ•œ ์ฆ๋ช…์—†์ด ์„œ๋ช…๋œ S3 ๋ฒ„ํ‚ท ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ์— ํŒŒ์ผ ์—…๋กœ๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

 

 

๋ฏธ๋ฆฌ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•œ URL์ด๋ฉด ๋ณด์•ˆ์ ์œผ๋กœ ์•ˆ ์ข‹์ง€ ์•Š์„๊นŒ?

 

PreSignedURL์€ ์ƒ์„ฑ ์‹œ์— ๋งŒ๋ฃŒ ์ผ์ž๋ฅผ ์ •ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ์ „ํ•œ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ํ™œ์šฉํ•  IAM ์‚ฌ์šฉ์ž๋Š” AWS ์„œ๋ช… ๋ฒ„์ „ 4๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์ตœ๋Œ€ 7์ผ ๋™์•ˆ ์œ ํšจํ•˜๋‹ค๊ณ  ๋ฌธ์„œ์— ์ ํ˜€์žˆ๋‹ค.

๋ฌผ๋ก  ์ตœ๋Œ“๊ฐ’์ธ 7์ผ์„ ๋‹ค ์‚ฌ์šฉํ•  ๊ฑด ์•„๋‹ˆ๊ณ , ์—…๋กœ๋“œ์— ํ•„์š”ํ•œ ์‹œ๊ฐ„์—์„œ ์กฐ๊ธˆ ๋„‰๋„‰ํ•˜๊ฒŒ ์žก์€ 1~2๋ถ„ ์ •๋„์˜ ์‹œ๊ฐ„์œผ๋กœ ์ œํ•œํ•  ์˜ˆ์ •์ด๋‹ค.

 

 

 

์—…๋กœ๋“œ๋œ ํŒŒ์ผ์˜ ์†Œ์œ ๊ถŒ

PreSignedURL ๋ฐฉ์‹์„ ํ†ตํ•ด ๊ถŒํ•œ์ด ์—†๋Š” ์œ ์ €๋„ S3์— ํŒŒ์ผ์„ ์˜ฌ๋ฆด ์ˆ˜ ์žˆ์ง€๋งŒ, ํŒŒ์ผ์˜ ์†Œ์œ ๊ถŒ์€ ๋ฒ„ํ‚ท ์†Œ์œ ์ž๊ฐ€ ๊ฐ์ฒด๋ฅผ ์†Œ์œ ํ•˜๊ฒŒ ๋œ๋‹ค.

 

 

AWS Docs

์ž์„ธํ•œ ๋‚ด์šฉ์€ ๋ฌธ์„œ๋ฅผ ํ†ตํ•ด ์‚ดํŽด๋ณด์ž

โ™ป๏ธ PreSignedURL ๋™์ž‘ ํ”Œ๋กœ์šฐ

 

ํŽซํŠœ๋””์˜ค์—์„  ํด๋ผ์ด์–ธํŠธ์—์„œ PreSignedURL์„ ์š”์ฒญํ•˜๋Š” API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด, ๋กœ์ง์„ ํ†ตํ•ด ์„œ๋ช…๋œ URL๊ณผ ์ €์žฅ๋  Image์˜ ๊ฒฝ๋กœ ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•  ์˜ˆ์ •์ด๋‹ค.

 

์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ์ง€, ๊ทธ๋ฆผ์„ ํ†ตํ•ด ์ดํ•ดํ•ด๋ณด์ž

 

 

 

1. PreSignedURL ์š”์ฒญ

ํด๋ผ์ด์–ธํŠธ์—์„œ ํŒŒ์ผ ์ €์žฅ์„ ์œ„ํ•ด S3 ๊ด€๋ จ ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” WAS์—๊ฒŒ PreSignedURL ๋ฐœ๊ธ‰์„ ์š”์ฒญํ•œ๋‹ค.

  • PreSignedURL์€ ๊ฐ ํŒŒ์ผ ๋ณ„๋กœ ์ƒ์„ฑํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— 2๊ฐœ ์ด์ƒ์˜ ํŒŒ์ผ์„ ์ €์žฅํ•  ์˜ˆ์ •์ด๋ผ๋ฉด, ํ•ด๋‹น ๊ณผ์ •์—์„œ ํŒŒ์ผ ๊ฐœ์ˆ˜์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ WAS์— ํ•จ๊ป˜ ๋„˜๊ธด๋‹ค.

 

2. PreSignedURL ์ƒ์„ฑ ์š”์ฒญ

WAS๋Š” ํŒŒ์ผ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ํŒŒ์ผ์„ ์ €์žฅํ•  S3 ๋ฒ„ํ‚ท ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์ง€์ • ํ›„, ์š”์ฒญ์— ๋‹ด์•„ S3์— PreSignedURL ์ƒ์„ฑ์„ ์š”์ฒญํ•œ๋‹ค.

 

3. PreSignedURL ๋ฐœ๊ธ‰

S3๋Š” WAS์˜ ๊ถŒํ•œ์„ ํ™•์ธ ํ›„, ์š”์ฒญ๋ฐ›์€ S3 ๋ฒ„ํ‚ท ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ PreSignedURL์„ ๋ฐœ๊ธ‰ํ•˜์—ฌ WAS์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.

 

4. ๋ฐœ๊ธ‰๋ฐ›์€ PreSignedURL, ImageURI, S3DirectoryPath ์ „๋‹ฌ

WAS๋Š” ๋ฐœ๊ธ‰๋ฐ›์€ PreSignedURL๊ณผ ImageURI, S3DirectoryPath ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.

์—ฌ๊ธฐ์„œ ImageURI ๋ž€, ์ด๋ฏธ์ง€๊ฐ€ S3์— ์ €์žฅ๋˜์—ˆ์„ ๋•Œ ํ•ด๋‹น ์ด๋ฏธ์ง€์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ์ฃผ์†Œ์ด๋‹ค. WAS๋Š” ์ด๋ฏธ์ง€ ์ €์žฅ์˜ ์—ญํ• ์„ Client์—๊ฒŒ ๋„˜๊ธฐ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ๋Š” ์ง€ ํ™•์ธํ•  ๋ฐฉ๋ฒ•์ด ์—†๋‹ค.

 

๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ๊ฐ€ PreSignedURL์„ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ์ €์žฅํ•œ ํ›„, ์ €์žฅ์— ์„ฑ๊ณตํ•œ Image์˜ URI๋ฅผ ์„œ๋ฒ„์—๊ฒŒ ์ „๋‹ฌํ•˜์—ฌ ํ•ด๋‹น ์ด๋ฏธ์ง€๊ฐ€ ์—…๋กœ๋“œ์— ์„ฑ๊ณตํ•˜์˜€์Œ์„ ์ „๋‹ฌํ•˜๋„๋ก ํ•˜์˜€๋‹ค.

  • ImageURI๋Š” ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋‹ค์‹œ ์ „๋‹ฌ๋ฐ›์€ ํ›„์— DB์— ์ €์žฅ๋˜์–ด ๋ชจ๋ธ ํŒŒ์ธํŠœ๋‹ ๋“ฑ์— ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉ๋œ๋‹ค.

S3DirectoryPath๋Š” ์ด๋ฏธ์ง€๊ฐ€ ์ €์žฅ๋  S3์˜ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ์ด๋‹ค. ํ•ด๋‹น ๋””๋ ‰ํ† ๋ฆฌ ์•ˆ์— ์ด๋ฏธ์ง€๋“ค์ด ์ €์žฅ๋˜๋ฉฐ, ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ด๋ฏธ์ง€ ์‚ญ์ œ ์š”์ฒญ ๋“ฑ์— ์‚ฌ์šฉ๋œ๋‹ค.

 

5. PreSignedURL์„ ํ†ตํ•ด ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ

ํด๋ผ์ด์–ธํŠธ๋Š” PreSignedURL์„ ํ†ตํ•ด ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œํ•œ๋‹ค. ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ๋Š” PreSignedURL์— PUT ๋ฉ”์„œ๋“œ๋กœ ๋ฐ”์ด๋„ˆ๋ฆฌ ํŒŒ์ผ์„ ์ „์†กํ•œ๋‹ค.

  • ๋งŒ์•ฝ ์ด๋•Œ ์—…๋กœ๋“œ ์‹คํŒจ ๋“ฑ์˜ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ํด๋ผ์ด์–ธํŠธ์—์„  ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์— ๋Œ€ํ•œ ์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ ํ›„, 4๋ฒˆ ๊ณผ์ •์—์„œ ๋ฐ›์€ S3DirectoryPath๋ฅผ WAS์˜ ์ด๋ฏธ์ง€ ์‚ญ์ œ API์— ์š”์ฒญํ•˜์—ฌ ํ•ด๋‹น ๋””๋ ‰ํ† ๋ฆฌ์™€ ๋””๋ ‰ํ† ๋ฆฌ์— ์ €์žฅ๋œ ์ด๋ฏธ์ง€๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค.

 

6. ImageURI, S3DirectoryPath ์ „๋‹ฌ

ํด๋ผ์ด์–ธํŠธ๋Š” ์ด๋ฏธ์ง€๋ฅผ ์—…๋กœ๋“œ ํ›„ ImageURI์™€ S3DirectoryPath๋ฅผ WAS์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.

WAS๋Š” ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์• ์™„๋™๋ฌผ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ , ์ดํ›„ ์ด๋ฏธ์ง€ ๊ด€๋ จ ์ž‘์—…์— ํ™œ์šฉํ•œ๋‹ค.

 

 


 

๐Ÿ†š ์•ฑ์—์„œ ์ง์ ‘ S3์— ์—…๋กœ๋“œ VS ์„œ๋ฒ„์—์„œ PreSignedURL ๋ฐœ๊ธ‰๋ฐ›์•„ ์—…๋กœ๋“œ

 

PreSignedURL์„ ๋„์ž…ํ•˜๊ธฐ๋กœ ํ–ˆ๋˜ ๊ฐœ๋ฐœ ์ดˆ๊ธฐ์—๋Š” ์ด๋Ÿฐ ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

 

์„œ๋ฒ„์—์„œ PreSignedURL์„ ๋ฐœ๊ธ‰๋ฐ›์ง€ ์•Š๊ณ  ์•ฑ์—์„œ ์ง์ ‘ S3์— ์—…๋กœ๋“œํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด PreSignedURL์„ ๋ฐœ๊ธ‰ํ•˜๋Š” ๋ณต์žกํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ  ์†๋„๋„ ๋”์šฑ ๊ฐœ์„ ๋˜์ง€ ์•Š์„๊นŒ?

 

์ด๋Ÿฌํ•œ ๊ณ ๋ฏผ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์ธํ„ฐ๋„ท ์—ฌ๋Ÿฌ ๊ธ€๋“ค์„ ์ฐพ์•„๋ณธ ๊ฒฐ๊ณผ, ์œ„์ฒ˜๋Ÿผ ๊ตณ์ด ์„œ๋ฒ„์— PreSignedURL์„ ์š”์ฒญํ•˜์ง€ ์•Š๊ณ  ์•ฑ์—์„œ ์ง์ ‘ S3์— ์—…๋กœ๋“œํ•˜๋Š” ๋ฐฉ์‹ ๋˜ํ•œ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๊ฒŒ ๋˜์–ด ๊ณ ๋ฏผ์„ ์‹œ์ž‘ํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.

 

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด ๋ณธ์ธ์€ ์ด ๋ฌธ์ œ์— ๋Œ€ํ•œ ๊นŠ์€ ๊ณ ๋ฏผ์˜ ๋์— ์„œ๋ฒ„์—์„œ PreSignedURL์„ ๋ฐœ๊ธ‰๋ฐ›์•„ ์—…๋กœ๋“œํ•˜๋Š” ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ๊ณ , ๊ฐ™์€ ๊ณ ๋ฏผ์„ ํ•  ๋ˆ„๊ตฐ๊ฐ€๋ฅผ ์œ„ํ•ด ๋‚ด ์ƒ๊ฐ์„ ๊ธฐ๋กํ•ด๋‘๊ณ ์ž ํ•œ๋‹ค.

 

 

์™œ ์„œ๋ฒ„์—์„œ PreSignedURL์„ ๋ฐœ๊ธ‰ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์„๊นŒ?

S3์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•ด์„  S3์— ๋Œ€ํ•œ Access ๊ถŒํ•œ์„ ๊ฐ€์ง„ AWS IAM์˜ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์‚ฌ์šฉ์ž์˜ ๋กœ์ปฌ์— ์ง์ ‘์ ์œผ๋กœ ์„ค์น˜๋˜๋Š” ์•ฑ์— ํฌํ•จํ•˜์—ฌ ๊ฐœ๋ฐœํ•ด์•ผํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ์•ž์œผ๋กœ ๋งŒ๋“ค์–ด์งˆ AIํ”„๋กœํ•„ ์ด๋ฏธ์ง€์™€ ์‚ฌ์šฉ์ž๊ฐ€ ๋“ฑ๋กํ•œ ๋ณธ์ธ์˜ ์• ์™„๋™๋ฌผ ์‚ฌ์ง„ ๋“ฑ์˜ ์ฃผ์š” ์ •๋ณด๊ฐ€ ๋‹ด๊ธธ S3์— ๋Œ€ํ•œ ์ฃผ์š” ๊ถŒํ•œ ์ •๋ณด๋ฅผ ์•ฑ์— ์ง€๋‹ˆ๊ฒŒ ํ•˜๋Š” ๊ฒƒ์€ ๋ณด์•ˆ์ ์œผ๋กœ ํฐ ๋ฌธ์ œ๋ฅผ ๋ถˆ๋Ÿฌ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

๋•Œ๋ฌธ์— ์•ฑ๋ณด๋‹ค ํ›จ์”ฌ ์•ˆ์ „ํ•œ ํ™˜๊ฒฝ์ธ WAS์— S3์— ๋Œ€ํ•œ ์ฃผ์š” ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉด์„œ ํ•„์š”ํ•  ๋•Œ API ํ˜ธ์ถœ์„ ํ†ตํ•ด PreSignedURL์„ ๋ฐœ๊ธ‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜์˜€๋‹ค.


 


 

โœจ ๊ตฌํ˜„

 

S3 ๋ฒ„ํ‚ท ์ƒ์„ฑ

 

ap-northeast-2 ์„ ํƒ ํ›„, ๋ฒ„ํ‚ท ์ด๋ฆ„ ์ž…๋ ฅ

 

 

 

  • ACL ๋น„ํ™œ์„ฑํ™”๋จ ์„ ํƒ
  • ๋ฒ„ํ‚ท ์†Œ์œ ์ž ์„ ํ˜ธ
๐Ÿšจ ํ•ด๋‹น ์„ค์ •์„ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด ACL ๊ด€๋ จ 400๋ฒˆ๋Œ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒ

 

 

 

  • ์™ธ๋ถ€์—์„œ ์ด๋ฏธ์ง€ URI๋ฅผ ํ†ตํ•ด ์ด๋ฏธ์ง€์— ์ ‘๊ทผํ•˜๋„๋ก ํ•  ์˜ˆ์ •์ด๋ฏ€๋กœ, ๋ฒ„ํ‚ท์„ ํผ๋ธ”๋ฆญ ์—‘์„ธ์Šค๋กœ ๋ณ€๊ฒฝ

์—ฌ๊ธฐ๊นŒ์ง€ ๋”ฐ๋ผ์™”๋‹ค๋ฉด, ๊ทธ๋Œ€๋กœ ๋ฒ„ํ‚ท ๋งŒ๋“ค๊ธฐ ์„ ํƒ

 

 

S3 ๋ฒ„ํ‚ท ๊ถŒํ•œ ์„ค์ •

 

PreSignedURL์„ ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„  ๋งŒ๋“ค์–ด์ง„ ๋ฒ„ํ‚ท์˜ ๊ถŒํ•œ์„ ์ˆ˜์ •ํ•ด์•ผํ•จ.

 

 

 

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:Put*",
                "s3:Get*"
            ],
            "Resource": "arn:aws:s3:::{ํ˜„์žฌ_๋ฒ„ํ‚ท_์ด๋ฆ„}/*"
        }
    ]
}
  • ๋ฒ„ํ‚ท์ด Put ๋˜๋Š” Get ์ž‘์—…์„ ํ—ˆ์šฉํ•˜๋„๋ก ์ •์ฑ… ์„ค์ •

 

 


[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "HEAD",
            "GET",
            "PUT",
            "POST"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]

 

 

HEAD, GET, PUT, POST ๋ฉ”์„œ๋“œ์— ๋Œ€ํ•ด CORS ์„ค์ •

ํ•ด๋‹น ์„ค์ •์„ ํ•ด์ฃผ์ง€ ์•Š์œผ๋ฉด CORS ์—๋Ÿฌ ๋ฐœ์ƒ

 

 

IAM ์ƒ์„ฑ

๋‹ค์Œ์œผ๋กœ S3์— PreSignedURL ์š”์ฒญ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•œ ๊ถŒํ•œ์„ ์–ป๊ธฐ ์œ„ํ•ด AWS IAM์„ ํ†ตํ•ด S3FullAccess ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•ด์•ผํ•œ๋‹ค.

 

 

AWS IAM์—์„œ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ํด๋ฆญ

 

 

 

S3FullAccess ๊ถŒํ•œ ์„ ํƒ

 

 

 

์‚ฌ์šฉ์ž ์ƒ์„ฑ ํด๋ฆญ

 

 

 

์ƒ์„ฑ๋œ ์‚ฌ์šฉ์ž๋ฅผ ํด๋ฆญํ•˜๊ณ  ๋ณด์•ˆ ์ž๊ฒฉ ์ฆ๋ช… ํด๋ฆญ

 

 

 

ํ•ด๋‹น ์‚ฌ์šฉ์ž๋กœ ์—‘์„ธ์Šคํ•˜๊ธฐ ์œ„ํ•œ ์—‘์„ธ์Šค ํ‚ค ์ƒ์„ฑ

 

 

 

์‚ฌ์šฉ ์‚ฌ๋ก€๋Š” CLI ์„ ํƒ

 

 

 

ํ•„์š”ํ•˜๋‹ค๋ฉด ์„ค๋ช… ํƒœ๊ทธ๋ฅผ ์ถ”๊ฐ€ ํ›„, ์—‘์„ธ์Šค ํ‚ค ๋งŒ๋“ค๊ธฐ ํด๋ฆญ ํ›„ ๋งŒ๋“ค์–ด์ง„ ์—‘์„ธ์Šค ํ‚ค๋ฅผ ์ €์žฅํ•œ๋‹ค.

 

 

build.gralde

// AWS
implementation "org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE"

 

spring-cloud-starter-aws ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ implementation

 

 

application.yml

cloud:
  aws:
    credentials:
      accessKey: ${AWS_ACCESS_KEY_DEV} # S3 ๊ด€๋ จ ๊ถŒํ•œ์„ ๊ฐ€์ง„ AWS IAM Access Key
      secretKey: ${AWS_SECRET_KEY_DEV} # S3 ๊ด€๋ จ ๊ถŒํ•œ์„ ๊ฐ€์ง„ AWS IAM Secret Key
    s3:
      bucket: ${AWS_S3_BUCKET_NAME} # S3 ๋ฒ„ํ‚ท ์ด๋ฆ„
      path: ${AWS_S3_FILE_DEFAULT_PATH} # S3์—์„œ ์ด๋ฏธ์ง€ ์ €์žฅ์— ํ™œ์šฉํ•  ๊ธฐ๋ณธ ๊ฒฝ๋กœ
    region:
      static: ${AWS_REGION_DEV} # AWS Region
    stack:
      auto: false

 

application.yml์— PreSignedURL ๊ตฌํ˜„์— ํ•„์š”ํ•œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋“ฑ๋ก

 

 

S3Config

@Configuration
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 amazonS3() {
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonS3ClientBuilder.standard()
                .withRegion(region)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }
}
  • S3Config์—์„œ S3FullAccess ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž์˜ accessKey, secretKey์™€ region์„ ํ†ตํ•ด AmazonS3 Bean ์ƒ์„ฑ
  • AmazonS3 Bean์„ PreSignedURL ์ƒ์„ฑ์— ์‚ฌ์šฉ

 

 

S3FileService

@Service
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class S3FileService {
    private final AmazonS3 amazonS3;

    @Value("${cloud.aws.s3.bucket}")
    private String bucketName;
    @Value("${cloud.aws.s3.path}")
    private String s3DefaultPath;
    private static final String BUCKET_DIRECTORY_NAME = "petProfile";
    private static final String FILE_NAME_PREFIX = "Petudio_";


    /**
     * presigned url ๋ฐœ๊ธ‰
     *
     * @param s3DirectoryPath ํŒŒ์ผ์„ ์ €์žฅํ•  S3 ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ
     * @param index           ์ด๋ฏธ์ง€ ์ˆœ์„œ
     * @return presigned url
     */
    public String getPreSignedUrl(String s3DirectoryPath, int index) {
        try {
            String filePath = createFilePath(s3DirectoryPath, index);

            GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePreSignedUrlRequest(bucketName,
                    filePath);
            URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest);
            return url.toString();
        } catch (Exception exception) {
            throw new BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION,
                    String.format("S3 ๋ฒ„ํ‚ท์˜ ๋””๋ ‰ํ† ๋ฆฌ(%s/%d) ์— ๋Œ€ํ•œ PreSignedURL์„ ์ƒ์„ฑํ•˜๋Š” ๊ณผ์ •์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.", s3DirectoryPath, index));
        }
    }

    /**
     * ํŒŒ์ผ์˜ ์ „์ฒด ๊ฒฝ๋กœ๋ฅผ ์ƒ์„ฑ
     *
     * @param index ์ด๋ฏธ์ง€ ์ˆœ์„œ
     * @return ํŒŒ์ผ์˜ ์ „์ฒด ๊ฒฝ๋กœ
     */
    private String createFilePath(String directoryPath, int index) {
        // ๊ฒฝ๋กœ: {BUCKET_DIRECTORY_NAME}/{memberId}/Petudio_{fileId}/{index}
        return String.format("%s/%d", directoryPath, index);
    }

    /**
     * ํŒŒ์ผ ์—…๋กœ๋“œ์šฉ(PUT) presigned url ์ƒ์„ฑ
     *
     * @param bucket   ๋ฒ„ํ‚ท ์ด๋ฆ„
     * @param filePath S3 ์—…๋กœ๋“œ์šฉ ํŒŒ์ผ ๊ฒฝ๋กœ
     * @return presigned url
     */
    private GeneratePresignedUrlRequest getGeneratePreSignedUrlRequest(String bucket, String filePath) {
        GeneratePresignedUrlRequest generatePresignedUrlRequest =
                new GeneratePresignedUrlRequest(bucket, filePath)
                        .withMethod(HttpMethod.PUT)
                        .withExpiration(getPreSignedUrlExpiration());
        generatePresignedUrlRequest.addRequestParameter(
                Headers.S3_CANNED_ACL,
                CannedAccessControlList.PublicRead.toString());
        return generatePresignedUrlRequest;
    }

    /**
     * presigned url ์œ ํšจ ๊ธฐ๊ฐ„ ์„ค์ •
     *
     * @return ์œ ํšจ๊ธฐ๊ฐ„
     */
    private Date getPreSignedUrlExpiration() {
        Date expiration = new Date();
        long expTimeMillis = expiration.getTime();
        expTimeMillis += 1000 * 60 * 2;
        expiration.setTime(expTimeMillis);
        return expiration;
    }

    /**
     * ํŒŒ์ผ์„ ์ €์žฅํ•  ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์ƒ์„ฑ
     *
     * @param memberId ํšŒ์› Primary Key
     * @return ํŒŒ์ผ์˜ ์ „์ฒด ๊ฒฝ๋กœ
     */
    public String createS3DirectoryPath(Long memberId) {
        return String.format("%s/%d/%s", BUCKET_DIRECTORY_NAME, memberId, FILE_NAME_PREFIX + createFileId());
    }

    /**
     * ํŒŒ์ผ ๊ณ ์œ  ID๋ฅผ ์ƒ์„ฑ
     *
     * @return 36์ž๋ฆฌ์˜ UUID
     */
    private String createFileId() {
        return UUID.randomUUID().toString();
    }

    /**
     * s3 ํŒŒ์ผ ์ ‘๊ทผ URI ์ƒ์„ฑ
     *
     * @param filePath S3 ์—…๋กœ๋“œ์šฉ ํŒŒ์ผ ๊ฒฝ๋กœ
     * @return s3 ํŒŒ์ผ ์ ‘๊ทผ URI
     */
    public String createImageUri(String filePath) {
        return s3DefaultPath + filePath;
    }


    /**
     * s3 Directory ๋ฐ์ดํ„ฐ ์‚ญ์ œ
     *
     * @param s3DirectoryPath ํŒŒ์ผ ์‚ญ์ œ๋ฅผ ์œ„ํ•œ S3 ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ
     */
    public void deleteImagesByS3DirectoryPath(String s3DirectoryPath) {
        try {
            // s3DirectoryPath์— ์†ํ•œ ๊ฐ์ฒด๋“ค ๋‚˜์—ด
            ListObjectsV2Request listObjectsRequest = new ListObjectsV2Request()
                    .withBucketName(bucketName)
                    .withPrefix(s3DirectoryPath);
            ListObjectsV2Result objectsResult = amazonS3.listObjectsV2(listObjectsRequest);

            // s3DirectoryPath ๋‚ด์˜ ๊ฐ์ฒด๋“ค ์‚ญ์ œ
            for (S3ObjectSummary objectSummary : objectsResult.getObjectSummaries()) {
                String key = objectSummary.getKey();
                amazonS3.deleteObject(new DeleteObjectRequest(bucketName, key));
            }
        } catch (Exception exception) {
            throw new BadGatewayException(ErrorCode.BAD_GATEWAY_EXCEPTION,
                    String.format("S3 ๋ฒ„ํ‚ท์˜ ๋””๋ ‰ํ† ๋ฆฌ(%s) ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๊ณผ์ •์—์„œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.", s3DirectoryPath));
        }
    }
}

 

S3์˜ ํŒŒ์ผ์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด๋กœ PreSignedURL ์ƒ์„ฑ, S3 ํŒŒ์ผ ์‚ญ์ œ ๋“ฑ์˜ ์—ญํ• ์„ ํ•จ

 

 

PetController

    // ๋ฐ˜๋ ค๋™๋ฌผ ์ด๋ฏธ์ง€ ์ €์žฅ์šฉ S3 PreSignedURL ์š”์ฒญ
    @GetMapping("/pet/images/presigned-url")
    public ApiResponse<CreatePetImagesUploadUrlsResponse> createPreSignedUrlForSavePetImages(
            @MemberId final Long memberId, @RequestBody @Valid final CreatePetImagesUploadUrlsRequest request) {
        return ApiResponse.success(petQueryService.createPreSignedUrlForSavePetImages(memberId, request));
    }

 

PreSignedURL ์š”์ฒญ์„ ๋ฐ›๋Š” Controller ๋กœ์ง์ด๋‹ค.

 

 

CreatePetImagesUploadUrlsRequest

public record CreatePetImagesUploadUrlsRequest(
        @Min(value = 10, message = "์ด๋ฏธ์ง€ ๊ฐฏ์ˆ˜๋Š” 10๊ฐœ ์ด์ƒ, 12๊ฐœ ์ดํ•˜์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
        @Max(value = 12, message = "์ด๋ฏธ์ง€ ๊ฐฏ์ˆ˜๋Š” 10๊ฐœ ์ด์ƒ, 12๊ฐœ ์ดํ•˜์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
        short imageAmount
) {}


์ด๋ฏธ์ง€๋Š” 10~12๊ฐœ ์ด์–ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— Validation ๋กœ์ง ๊ตฌํ˜„

 

 

PetService

public CreatePetImagesUploadUrlsResponse createPreSignedUrlForSavePetImages(Long memberId,
                                                                                CreatePetImagesUploadUrlsRequest request) {
        String s3DirectoryPath = s3FileService.createS3DirectoryPath(memberId);
        List<PetImageUploadInfo> petImageUploadInfos = new ArrayList<>();
        
        // s3Directory ๊ฒฝ๋กœ๋ฅผ ํ†ตํ•ด, ์š”์ฒญ๋ฐ›์€ ์ด๋ฏธ์ง€ ๊ฐฏ์ˆ˜ ๋งŒํผ PreSignedURL, ImageURI ์ƒ์„ฑ  
        for (int i = 1; i <= request.imageAmount(); i++) {
            String s3FilePath = s3DirectoryPath + "/" + i;
            petImageUploadInfos.add(
                    new PetImageUploadInfo(s3FileService.getPreSignedUrl(s3DirectoryPath, i),
                            s3FileService.getImageUri(s3FilePath)));
        }

        return new CreatePetImagesUploadUrlsResponse(s3DirectoryPath, petImageUploadInfos);
    }


์ž…๋ ฅ๋ฐ›์€ ์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜๋งŒํผ PreSignedURL, ImageURI๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ Service ๋ฉ”์„œ๋“œ

  • S3FileService์˜ createS3DirectoryPath๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํŒŒ์ผ์„ ์ €์žฅํ•  ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ s3DirectoryPath๋ฅผ ์ƒ์„ฑ ํ›„, ์Šฌ๋ž˜์‹œ(/) + index ๋ฅผ ๋ง๋ถ™์—ฌ ๊ฐ ํŒŒ์ผ์˜ s3filePath๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  s3DirectoryPath, s3filePath ๋ฅผ ํ™œ์šฉํ•˜์—ฌ PreSignedURL, ImageURI๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๋งŒ๋“ค์–ด์ง„ ๊ฒฝ๋กœ์™€ s3DirectoryPath๋ฅผ CreatePetImagesUploadUrlsResponse DTO ๊ฐ์ฒด์— ๋‹ด์•„ ๋ณด๋‚ธ๋‹ค.

 

 

CreatePetImagesUploadUrlsResponse

public record CreatePetImagesUploadUrlsResponse(
        String s3DirectoryPath,
        List<PetImageUploadInfo> petImageUploadInfos) {
}

 

 

PetImageUploadInfo

public record PetImageUploadInfo(String preSignedUrl, String imageUri) {
}

 

 


 

โœ… ํ…Œ์ŠคํŠธ

 

Postman

 

 

Postman์œผ๋กœ ํ•ด๋‹น API์— ์š”์ฒญ์„ ๋ณด๋‚ธ ๊ฒฐ๊ณผ ์ด๋ ‡๊ฒŒ 10๊ฐœ์˜ PreSignedURL์ด ์ •์ƒ์ ์œผ๋กœ ํ˜ธ์ถœ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

 

S3

 

ํด๋ผ์ด์–ธํŠธ์—์„œ PreSignedURL์„ ํ†ตํ•ด S3์— ์ด๋ฏธ์ง€ ์ €์žฅ์„ ์‹œ๋„ํ•ด๋ณด์•˜๋Š”๋ฐ ์ด๋ฏธ์ง€๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Œ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

 

ImageUri

 

PreSignedURL ์š”์ฒญ ์‹œ์— ํ•จ๊ป˜ ๋ฐ›์€ imageURI๋ฅผ ํ†ตํ•ด์„œ๋„ ์ •์ƒ์ ์œผ๋กœ ์ด๋ฏธ์ง€์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

 


 

์ถ”ํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ

์ง€๊ธˆ์œผ๋กœ๋„ ๊ดœ์ฐฎ์€ ๊ฒƒ ๊ฐ™๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์ง€๋งŒ, ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ๋ถ€๋ถ„์ด ์•„์ง ๋‚จ์•„์žˆ์—ˆ๋‹ค.

PreSignedURL์„ ํ†ตํ•œ ์ด๋ฏธ์ง€ ์ €์žฅ์— ์‹คํŒจํ•˜์—ฌ, ์‚ญ์ œ API๊ฐ€ ์š”์ฒญ๋˜์—ˆ์„ ๋•Œ ๋งŒ์•ฝ ์‚ญ์ œ API ๋˜ํ•œ ์‹คํŒจํ•œ๋‹ค๋ฉด ์ž˜๋ชป ์ €์žฅ๋œ ์ด๋ฏธ์ง€๋Š” ๊ณ„์†ํ•ด์„œ S3์— ๋‚จ์•„์žˆ๊ฒŒ ๋œ๋‹ค.

 

์ด๋Ÿฌํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ์„ ์œ„ํ•œ ๊ฐœ์„  ๋ฐฉํ–ฅ์„ ๊ณ ๋ฏผํ•  ํ•„์š”๊ฐ€ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

ํ–ฅํ›„์— ๊ฐœ์„ ์ด ์™„๋ฃŒ๋œ๋‹ค๋ฉด ์ด๊ณณ์— ๊ธฐ๋กํ•˜๊ฒ ๋‹ค.