์คํ๋ง๋ถํธ S3 PreSignedURL์ ํตํ ์ด๋ฏธ์ง ์ ์ฅ
๐ค ์ ์ฉ์ ๊ณ ๋ คํ ์ด์
“์ฐ๋ฆฌ์ง ๋ฐ๋ ค๋๋ฌผ์ 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์ ๋จ์์๊ฒ ๋๋ค.
์ด๋ฌํ ๋ฌธ์ ํด๊ฒฐ์ ์ํ ๊ฐ์ ๋ฐฉํฅ์ ๊ณ ๋ฏผํ ํ์๊ฐ ์์ ๊ฒ ๊ฐ๋ค.
ํฅํ์ ๊ฐ์ ์ด ์๋ฃ๋๋ค๋ฉด ์ด๊ณณ์ ๊ธฐ๋กํ๊ฒ ๋ค.