1. AWS Lambda ์์ฑ
Lambda ์ฝ์์์ ์ ํจ์ ์์ฑ์ ํด์ค์ผํจ
๋ฐฑ์๋ ์๋ฒ์ ๋์ผํ Java17 ๋ฐํ์์ AWS ์ฝ์์์ ์ง์ ์ฝ๋ ํธ์ง์ด ๋ถ๊ฐ๋ฅํ๊ณ JAR ํ์ผ์ ์ ๋ก๋ ํด์ผํจ
๋ํ, Lambda์์ ์์ฒญ์ ๋ณด๋ผ CloudRun ํ๊ฒฝ์ด Python flask server ์ด๋ฏ๋ก Python ๋ฐํ์์ ์ ํํด์ค
(๋๋จธ์ง๋ ๊ธฐ๋ณธ ์ค์ ๊ทธ๋๋ก์ด๊ณ ํ์์ฑ์ ๋ง๊ฒ ์์์ ์ ํํ๋ฉด๋จ)
์์ฑํ ํจ์๋ฅผ ํด๋ฆญํ๊ณ ์ฝ์์์ ์ฝ๋๋ฅผ ๋๋ฅด๋ฉด ์ด๋ ๊ฒ lambda_handler ํจ์ ์ฝ๋๋ฅผ ์ดํด๋ณผ ์ ์์
์ฐ์ ์ ํ ์คํธ์ฉ๋์ด๊ธฐ์ ๋ณ๋ค๋ฅธ ๋ก์ง์ ์์ฑํ์ง ์๊ณ statusCode๋ฅผ 202๋ก ๋ฐ๊ฟ ์ ๋์ํ๋์ง ํ์ธํด๋ณด๊ธฐ๋กํจ
=> ์ฝ๋๋ฅผ ์์ ํ์์ผ๋ฉด ์ผ์ชฝ์ Deploy๋ฅผ ๋๋ฌ์ผ ๋ฐ์๋จ!
๋ํ, ํ์ฌ ํ
์คํธ์์๋ ์๊ด์์ง๋ง ์์ฑํ ํจ์๋ฅผ ํด๋ฆญ ํ ์ฝ์์ ๊ตฌ์ฑ ํญ์์ ํธ์ง์ ๋๋ฅธ ํ ์ ํ ์๊ฐ์ max์ธ 15๋ถ์ผ๋ก ๋ณ๊ฒฝํ์์
(ํ์๋ CloudRun cold start ๋ฌธ์ ํด๊ฒฐ์ ์ํด ๋ก์ง์ ์์ฑํด์ผํ๋ฏ๋ก)
2. ํจ์ URL ์์ฑ
์ด์ ์ด Lambda๋ฅผ HTTP ์์ฒญ์ผ๋ก ํธ์ถํด์ผํ๋ฏ๋ก API Gateway ๋๋ Lambda URL์ ์ฌ์ฉํด์ผํ๋๋ฐ
์์ฑํ ํจ์์ ์ฝ์์์ ๊ตฌ์ฑ์ ์ ํํ ํ ํจ์ URL์ ํด๋ฆญํ๋ฉด ํจ์ URL ์์ฑ์ด ์์
์ธ์ฆ ์ ํ์ NONE์ผ๋ก ์ค์ ํ๊ณ ๊ธฐ๋ณธ ์ค์ ๊ทธ๋๋ก ์ ์ฅํ๋ฉด
(์ฌ์ค AWS_IAM ์ธ์ฆ ์ ํ์ ์ฌ์ฉํ๋๊ฒ ๊ถ์ฅ๋์ง๋ง ๋น ๋ฅธ MVP ๊ฐ๋ฐ ๋ฐ Lambda ์ฐ๋ ํ
์คํธ๋ฅผ ์ํด NONE์ผ๋ก ์ค์ ํ์๊ณ ์ถํ ํ๋ก๋์
ํ๊ฒฝ์์ AWS_IAM ์ธ์ฆ ์ ํ์ผ๋ก Lambda๋ฅผ ํธ์ถํ๋ ๋ก์ง๋ ์ถ๊ฐํ๊ฒ ์)
ํจ์ URL์ด ์๊ธด ๊ฒ์ ํ์ธํ ์ ์์
3. Lambda ํธ์ถ์ ์ํ Spring Boot ์ค์
aws:
access-key: YOUR_AWS_ACCESS_KEY
secret-key: YOUR_AWS_SECRET_KEY
region: YOUR_AWS_REGION
lambda:
function-name: YOUR_LAMBDA_FUNCTION_NAME
endpoint: YOUR_LAMBDA_FUNCTION_URL
access-key : AWS IAM ์ฌ์ฉ์์ ์ก์ธ์ค ํค ID
secret-key : AWS IAM ์ฌ์ฉ์์ ๋น๋ฐ ์์ธ์ค ํค
region : Lambda ํจ์๊ฐ ์์นํ AWS ๋ฆฌ์
function-name : ํธ์ถํ Lambda ํจ์์ ์ด๋ฆ
endpoint : Lambda ํจ์์ URL
์ด๋ ๊ฒ application.yml์ ์ธ์ฆ์ ๋ณด๋ค์ ์ถ๊ฐํด์ฃผ๋ฉด๋จ
(ํ์ฌ๋ ์ธ์ฆ์ ํ์ด NONE์ด๋ฏ๋ก endpoint๋ง ์์ผ๋ฉด๋จ)
AWS IAM ํค๋ IAM ์ฝ์์ ์์ธ์ค ํค ๋ง๋ค๊ธฐ๋ฅผ ํตํด ์์ฑ ๊ฐ๋ฅ
4. Spring Boot์์ Lambda ํธ์ถ ์ฝ๋ ์์ฑ
DTO๋ Controller ๋จ ์ฝ๋๋ AI ๋ชจ๋ธ ํธ์ถ์ ์ํ ์์์ผ๋ก ๋ง์ถฐ์ ธ์๊ธฐ์ Service๋จ์์ Lambda๋ฅผ ํธ์ถํ๋ ๋ถ๋ถ์ ์ฝ๋๋ง ๊ฐ์ ธ์์
// AI ๋ชจ๋ธ ์๋น์ค ํธ์ถ
private suspend fun callAiModelService(voicepackRequest: VoicepackRequest, voiceFile: MultipartFile) {
val aiModelRequest = AIModelRequest(
voicepackId = voicepackRequest.id,
voiceFile = voiceFile
)
logger.info("AI ๋ชจ๋ธ ์์ฒญ: requestId={}, request={}", voicepackRequest.id, aiModelRequest)
try {
val response = httpClient.post(lambdaEndpoint) {
contentType(ContentType.MultiPart.FormData)
setBody(
MultiPartFormDataContent(
formData {
append("voicepackId", aiModelRequest.voicepackId)
append("voiceFile", aiModelRequest.voiceFile.bytes, Headers.build {
append(HttpHeaders.ContentDisposition, "form-data; name=\"voiceFile\"; filename=\"audio.wav\"")
append(HttpHeaders.ContentType, "audio/wav") // ํ์ ์ ํ์ผ ํ์ฅ์ ๋ณ๊ฒฝ ๊ฐ๋ฅ
})
}
)
)
}
// Lambda๊ฐ 202 Accepted๋ฅผ ๋ฐํํ๋ฉด ์ฑ๊ณต์ผ๋ก ๊ฐ์ฃผ
if (response.status == HttpStatusCode.Accepted) {
logger.info("AI ๋ชจ๋ธ ์๋ต: requestId={}, response={}", voicepackRequest.id, response.body<String>())
} else {
val errorBody = response.body<String>()
logger.error("AI ๋ชจ๋ธ ์๋น์ค ์ค๋ฅ: HTTP ${response.status.value}, ์๋ต: $errorBody")
throw RuntimeException("AI ๋ชจ๋ธ ์๋น์ค ํธ์ถ ์คํจ: HTTP ${response.status.value}, ์๋ต: $errorBody")
}
} catch (e: Exception) {
logger.error("AI ๋ชจ๋ธ ์๋น์ค ํธ์ถ ์ค ์์ธ ๋ฐ์: ${e.message}", e)
throw RuntimeException("AI ๋ชจ๋ธ ์๋น์ค ํธ์ถ ์ค ์ค๋ฅ ๋ฐ์: ${e.message}", e)
}
}
์ฃผ์ ์ฝ๋๋ฅผ ์ฐจ๋ก๋ก ์ดํด๋ณด๋ฉด
httpClient.post๋ฅผ ํตํด Lambda์ ๋คํธ์ํฌ ์์ฒญ์ ๋ณด๋ด๋๋ฐ, ์๋ต์ด ์ฌ ๋๊น์ง ๊ธฐ๋ค๋ ค์ผ ํ๋ฏ๋ก (์๊ฐ์ด ์กฐ๊ธ ๊ฑธ๋ฆฌ๋ ์์ ์ผ๋ก ๊ฐ์ )
์ฝ๋ฃจํด(๋น๋๊ธฐ ํจ์)์์ ์ฌ์ฉํ๋ suspend๋ฅผ ํตํด ์๋ต์ด ์ฌ ๋ ๊น์ง ๋ค๋ฅธ ์์ ์ ์ฒ๋ฆฌํ๊ฒ๋ ํ์์
(suspend๊ฐ ์์ผ๋ฉด ๋ฉ์ธ ์ค๋ ๋๊ฐ blocking ๋ผ์ ๋ค๋ฅธ ์์ ์ ๋ชปํจ)
์ถํ CloudRun์ ๋ชจ๋ธ์ ํธ์ถํ Lambda๋ก HTTP POST ์์ฒญ์ ๋ณด๋ด๊ธฐ ์ํด httpClient.post๋ก lambdaEndpoint (์๊น ์์ฑํ ํจ์ url) ์ ์์ฒญ์ ๋ณด๋ด๊ฒ ํ๊ณ ์์
์ด ๋ HTTP ์์ฒญ์ ๋ํด ๋ฐ์ดํฐ๊ฐ ์ด๋ค ํ์์ธ์ง ์๋ ค์ฃผ๊ธฐ ์ํด contentType์ผ๋ก Multipart.FormData(์์ฑ ํ์ผ) ๋ฅผ ์ค์ ํ์์
๊ทธ ํ setBody๋ฅผ ํตํด ์์ฒญ ๋ณธ๋ฌธ์ ๋ณด๋ผ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ์๋๋ฐ
MultiPartFormDataContent๋ฅผ ํตํด multipart ํ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ด์ฃผ์๊ณ form-data์ append๋ก ํญ๋ชฉ์ ์ถ๊ฐํจ
๋ฐ์๋ ๋๋ค์ ์๋ต์ ๋ฐ๋ผ ๋ค์ ์์ ์ ์ํํ๋๋ก if-else ๋ฌธ์ ๋ฃ์ด๋จ๋๋ฐ
try {
// AI ๋ชจ๋ธ ์๋น์ค ํธ์ถ ๋ฐ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ
callAiModelService(voicepackRequest, request.voiceFile)
// ๋ณํ ์ฑ๊ณต ์ ์ฒ๋ฆฌ
handleSuccessfulConversion(voicepackRequest)
return VoicepackConvertResponse(voicepackRequest.id, VoicepackRequestStatus.COMPLETED.name)
} catch (e: Exception) {
// ์คํจ ์ ์ฒ๋ฆฌ
handleFailedConversion(voicepackRequest, e)
return VoicepackConvertResponse(voicepackRequest.id, VoicepackRequestStatus.FAILED.name)
}
์์ ๊ฐ์ด callAiModelService๋ฅผ ํธ์ถํ๋ ๋ค๋ฅธ ํจ์์์ ์๋ฌ๋ฅผ ๋์ง์ง ์์๋ค๋ฉด ์ฑ๊ณต ์ ์ฒ๋ฆฌ ๋ก์ง์ ์ํํ๊ฒ๋จ
5. ํ ์คํธ
์ง๊ธ Lambda ํจ์๋ ์์์ ์์ฑํ๋๋ก 202์ฝ๋๋ฅผ ๋ฐํํ๊ฒ ํด๋จ์ผ๋ฏ๋ก ์์ฒญ์ ๋ณด๋ด๋ณด๋ฉด
์์ ๊ฐ์ด ์ฑ๊ณต ๋ก์ง์ด ์ํ๋๋๊ฑธ ํ์ธํ ์ ์์
์ง๊ธ์ Lambda ํธ์ถ ํ ์คํธ์ฉ์ด๋ผ ๋จ์ํ 202๊ฐ ๋ฐํ๋์ง๋ง ์ด์ ์ด Lambda์์ CloudRun์ ๋ชจ๋ธ์ ํธ์ถํ๊ฒํ์ฌ cold start ๋ฐ์ ์ ๋ฐฑ์๋์ ์์ฒญ์ด ๊ณ์ํด์ cold start๊ฐ ๋๋ ๋ ๊น์ง ๊ธฐ๋ค๋ฆฌ๋ฉฐ ์ก๊ณ ์์ ํ์๊ฐ ์์ด์ง
'๐ซ Backend > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
NginX๋ฅผ ํตํด Spring Boot ์๋ฒ์ HTTPS ์ ์ฉํ๊ธฐ (0) | 2025.03.24 |
---|---|
Spring Boot์์ Supabase ์ฌ์ฉํ๊ธฐ (Kotlin) (0) | 2025.02.25 |
Spring boot ์์ H2 ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฌ์ฉํ๊ธฐ (Kotlin) (0) | 2025.02.20 |
JPA ํบ์๋ณด๊ธฐ (0) | 2025.02.16 |
[Spring Boot] Docker๋ฅผ ์ด์ฉํด EC2์ ๋ฐฐํฌํด๋ณด๊ธฐ (4) | 2024.08.27 |