BackEnd/Spring Boot

SQS + Lambda๋ฅผ ์ด์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ˆ˜์— ๋ฌด๊ด€ํ•œ ํ‘ธ์‹œ์•Œ๋ฆผ ์ „์†ก ํ™˜๊ฒฝ ๊ตฌ์ถ•ํ•˜๊ธฐ

ddonghyeo 2024. 8. 29. 01:30
๋ฒ„์ „
Spring Boot 3
AWS SDK for java v2
firebase-admin 6.5.0

0. ๋„์ž… ๋ฐฐ๊ฒฝ

๊ธฐ์กด ๊ตฌ์กฐ

๊ธฐ์กด์—๋Š” ํ‘ธ์‹œ ์•Œ๋ฆผ ์š”์ฒญ์— ๋”ฐ๋ผ์„œ ์„œ๋ฒ„ ๋‚ด์—์„œ ๋ฉ”์„ธ์ง€๋ฅผ ์ƒ์„ฑํ•˜๊ณ  Firebase๋กœ ๋ฉ”์„ธ์ง€๋ฅผ ์ „์†กํ–ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ, ์—ฌ๊ธฐ์„œ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ๋ณด๋‚ผ ์‚ฌ์šฉ์ž๊ฐ€ ๋งŒ๋ช…, 10๋งŒ๋ช…, 100๋งŒ๋ช…์ด ๋œ๋‹ค๋ฉด...

 

๋ชจ๋“  ์‚ฌ์šฉ์ž์— ๋Œ€ํ•œ ์•Œ๋ฆผ์„ ์ผ๊ด„์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.

 

์ผ๊ด„ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ Firebase์˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋ณด์ž.

 

com.google.firebase.FirebaseMessaging.sendAll()

 

๋จผ์ € ํ•œ๊บผ๋ฒˆ์— ๋ณด๋‚ด๋Š” sendAll์€ Deprecate๋˜์—ˆ๊ณ , ๋Œ€์‹  sendEach๋ฅผ ์จ์•ผํ•œ๋‹ค.

 

com.google.firebase.FirebaseMessaging.sendEach()

 

sendEach()๋Š” ๊ฐ ํ† ํฐ๋ณ„ ์—ฌ๋Ÿฌ ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ฐ ๋ฉ”์„ธ์ง€๋งˆ๋‹ค ๋ณ„๋„์˜ HTTP ์š”์ฒญ์„ ๋งŒ๋“ค์–ด๋‚ธ๋‹ค.

 

10๋งŒ๊ฐœ์˜ ์š”์ฒญ์ด๋ฉด 10๋งŒ๊ฐœ์˜ HTTP ์š”์ฒญ์ด ์ƒ์„ฑ๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

์‹ฌ์ง€์–ด, message์˜ ์–‘์€ 500๊ฐœ๊ฐ€ ์ œํ•œ์ด๊ธฐ ๋•Œ๋ฌธ์— 500๊ฐœ์”ฉ ๋‚˜๋ˆ ์„œ ํ˜ธ์ถœํ•ด์•ผ ํ•œ๋‹ค.

 

 

 

sendEachAsync()๋Š” ๋น„๋™๊ธฐ๋กœ ๋™์ž‘ํ•˜์ง€๋งŒ ๋ฆฌ์†Œ์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋งˆ์ฐฌ๊ฐ€์ง€์ด๋‹ค.

 

 

 

๊ฐœ์„ ํ•œ ๊ตฌ์กฐ

 

๋”ฐ๋ผ์„œ ๋‚˜๋Š” ํ† ํฐ๊ณผ ํ‘ธ์‹œ ์•Œ๋ฆผ ๋‚ด์šฉ์„ ๋‹ด์€ ๋ฉ”์„ธ์ง€๋ฅผ SQS์— ์ ์žฌํ•˜๊ณ ,

 

SQS์— ๋ฉ”์„ธ์ง€๊ฐ€ ๋„์ฐฉํ•˜๋ฉด Lambda๊ฐ€ ํŠธ๋ฆฌ๊ฑฐ๋˜์–ด ํŒŒ์ด์ฌ ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋Š” ๊ตฌ์กฐ๋กœ ๊ฐœ์„ ํ•ด ๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

 

firebase_admin ๋ชจ๋“ˆ์ด ์„ค์น˜๋˜์–ด ์žˆ๋Š” AWS Lambda์—์„œ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

 

๊ธฐ์กด ๊ตฌ์กฐ์™€๋Š” ๋‹ค๋ฅด๊ฒŒ, ์ „์†กํ•˜๋ ค๋Š” token์˜ ์–‘์ด ๋งŽ๋”๋ผ๋„ ์ œํ•œ์ด ์—†๊ณ , token์–‘์— ์ƒ๊ด€ ์—†์ด ์„œ๋ฒ„ ์ž…์žฅ์—์„œ๋Š” ๋ฌด์กฐ๊ฑด 1๋ฒˆ์˜ ์š”์ฒญ์œผ๋กœ๋งŒ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

 

๋งŒ ๋ช…์˜ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ‘ธ์‹œ์•Œ๋ฆผ์„ ์ „์†กํ•ด๋„ ํ•œ ๋ฒˆ์˜ HTTP ์š”์ฒญ์œผ๋กœ ํ•ด๊ฒฐํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

ํ•ด๋‹น ๊ตฌ์กฐ๋Š” ํ‘ธ์‹œ ์•Œ๋ฆผ ์ „์†ก์ด ์™„์ „ํžˆ ๋ถ„๋ฆฌ๋˜์–ด, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํšจ๊ณผ๋ฅผ ๊ธฐ๋Œ€ํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

  • ํ™•์žฅ์„ฑ (Scalability)
    • AWS SQS๋Š” ๋†’์€ ์ฒ˜๋ฆฌ๋Ÿ‰๊ณผ ํ™•์žฅ์„ฑ์„ ์ œ๊ณตํ•œ๋‹ค. ๋ฉ”์‹œ์ง€ ์–‘์ด ์ฆ๊ฐ€ํ•ด๋„ ์‰ฝ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
    • Lambda ํ•จ์ˆ˜๋Š” ์ž๋™์œผ๋กœ ์Šค์ผ€์ผ๋ง๋˜์–ด ๋™์‹œ์— ๋งŽ์€ ํ‘ธ์‹œ ์•Œ๋ฆผ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ (Decoupling)
    • ๋ฉ”์ธ ์„œ๋ฒ„์™€ ํ‘ธ์‹œ ์•Œ๋ฆผ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•จ์œผ๋กœ์จ ์‹œ์Šคํ…œ์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ธ๋‹ค.
    • ์„œ๋ฒ„์˜ ๋ถ€ํ•˜๋ฅผ ์ค„์ด๊ณ , ํ‘ธ์‹œ ์•Œ๋ฆผ ์‹คํŒจ๊ฐ€ ์„œ๋ฒ„์˜ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋Š”๋‹ค.
  • ์•ˆ์ •์„ฑ๊ณผ ๋‚ด๊ตฌ์„ฑ (Reliability and Durability)
    • SQS๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•œ๋‹ค. ์ผ์‹œ์ ์ธ ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ๋‚˜ ์„œ๋น„์Šค ์ค‘๋‹จ ์‹œ์—๋„ ๋ฉ”์‹œ์ง€ ์†์‹ค ์—†์ด ์žฌ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๋กœ๊น… (Monitoring and Logging)
    • AWS ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ CloudWatch ๋“ฑ์„ ํ†ตํ•ด ์‰ฝ๊ฒŒ ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ณ  ๋กœ๊ทธ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ (Asynchronous Processing)
    • ํ‘ธ์‹œ ์•Œ๋ฆผ ์ „์†ก์„ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•จ์œผ๋กœ์จ ๋ฉ”์ธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์‘๋‹ต ์‹œ๊ฐ„์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ์žฌ์‹œ๋„ ๋งค์ปค๋‹ˆ์ฆ˜ (Retry Mechanism)
    • SQS์˜ ๊ฐ€์‹œ์„ฑ ํƒ€์ž„์•„์›ƒ๊ณผ DLQ๋ฅผ ์ด์šฉํ•ด ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์žฌ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

 

1. IAM ์ƒ์„ฑ

๋จผ์ € SQS์— ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ผ ๊ถŒํ•œ์ด ํ•„์š”ํ•œ๋ฐ, ์ด๋ฅผ IAM์—์„œ ๋งŒ๋“ค์–ด์ค€๋‹ค.

 

์ƒˆ๋กœ์šด ์‚ฌ์šฉ์ž๋ฅผ ์ƒ์„ฑํ•˜๋ฉด์„œ, AmazonSQSFullAccess ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์‚ฌ์šฉ์ž๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

 

์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑ๋๋‹ค๋ฉด, ์—‘์„ธ์Šค ํ‚ค๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

 

 

์—‘์„ธ์Šค ํ‚ค๋Š” ํ…Œ์ŠคํŠธ ์‹œ์—๋งŒ "AWS ์™ธ๋ถ€์—์„œ ์‹คํ–‰๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜" ์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๊ณ ,

 

์‹ค์ œ ๋ฐฐํฌํ•œ ํ™˜๊ฒฝ์ด AWS EC2 ๋‚ด๋ผ๋ฉด "AWS ์ปดํ“จํŒ… ์„œ๋น„์Šค์—์„œ ์‹คํ–‰๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜"์˜ ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด์ฃผ๋Š”๊ฒŒ ์ข‹๋‹ค.

 

 

์—‘์„ธ์Šค ํ‚ค๋ฅผ ์ƒ์„ฑํ•˜๋ฉด 1ํšŒ์„ฑ์œผ๋กœ ์—‘์„ธ์Šค ํ‚ค์™€ ๋น„๋ฐ€ ์—‘์„ธ์Šค ํ‚ค๋ฅผ ๋ณด์—ฌ์ฃผ๋‹ˆ ๊ณ ์ด ๋ชจ์…”๋‘”๋‹ค.

 

 

 

 

2. SQS (Simple Queue Service) ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ

 

์ด์ œ SQS ์—์„œ ๋Œ€๊ธฐ์—ด์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

 

 

ํ์— ๋“ค์–ด๊ฐˆ ๋ฉ”์„ธ์ง€์˜ ์„ฑ๊ฒฉ์— ๋”ฐ๋ผ ๊ตฌ์„ฑ์„ ํ•ด์ฃผ๋ฉด ๋˜๋Š”๋ฐ,

 

๋‚ด ์„œ๋น„์Šค์—์„œ ์‚ฌ์šฉํ•  ๋ฉ”์„ธ์ง€๋Š” 1์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ์˜๋ฏธ ์—†๋Š” ๋ฉ”์„ธ์ง€์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์กด ๊ธฐ๊ฐ„์„ 1์‹œ๊ฐ„์œผ๋กœ ์„ค์ •ํ–ˆ๋‹ค.

 

 

์—ฌ๊ธฐ์„œ ๋Œ€๊ธฐ์—ด์— ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋Š” ์‚ฌ์šฉ์ž์— ๋ฐฉ๊ธˆ ๋งŒ๋“ค์—ˆ๋˜ IAM์˜ Role์„ ๋„ฃ์–ด์ฃผ๋ฉด ๋œ๋‹ค.

 

 

ARN Role์€ IAM ์‚ฌ์šฉ์ž์— ๋“ค์–ด๊ฐ€์„œ ๋ณต์‚ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

 

3. Lambda ํ•จ์ˆ˜ ์ƒ์„ฑ

 

 

๋‹ค์Œ์œผ๋กœ Lambda ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ๋˜๋Š”๋ฐ, ๋Ÿฐํƒ€์ž„ ์–ธ์–ด๋Š” ํŒŒ์ด์ฌ์œผ๋กœ ํ•˜๋Š”๊ฒŒ ๊ฐ€์žฅ ์ข‹๋‹ค.

 

ํŒŒ์ด์ฌ์˜ firebase_admin ๋ชจ๋“ˆ์ด ๊ฐ€์žฅ ๊ฐ€๋ณ๊ณ  ์„ค์ •ํ•˜๊ธฐ ๊ฐ„ํŽธํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

 

 

 

firebase-admin ๋ชจ๋“ˆ ์„ค์น˜

 

๋‹ค์Œ์œผ๋กœ firebase admin ๋ชจ๋“ˆ์„ ํ•จ์ˆ˜์— ์„ค์น˜ํ•ด๋ณด์ž.

 

 

๋จผ์ € python์œผ๋กœ ๋œ ๋นˆ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

 

โ€ผ๏ธ Lambdaํ•จ์ˆ˜์—์„œ ๋ชจ๋“ˆ๋กœ ์ธ์‹ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ตœ์ƒ์œ„ ํด๋”์˜ ์ด๋ฆ„์ด ๊ผญ python์ด์–ด์•ผ ํ•œ๋‹ค!

 

 

 

pip์„ ์ด์šฉํ•˜์—ฌ firebase-admin ์†Œ์Šค๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.

$ pip install firebase-admin -t {๊ฒฝ๋กœ}/python

 

 

 

์†Œ์Šค๊ฐ€ ์„ค์น˜๋˜์—ˆ๋‹ค๋ฉด, ๊ทธ๋Œ€๋กœ ์••์ถ•ํ•ด์ค€๋‹ค.

 

 

AWS Lambda๋กœ ๋Œ์•„์™€  ๊ณ„์ธต์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•ด์ค€๋‹ค.

 

 

 

 

๊ณ„์ธต์—๋Š” .zip ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜์—ฌ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๋ฐฉ๊ธˆ ์••์ถ•ํ–ˆ๋˜ zipํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๊ณ , Lambdaํ•จ์ˆ˜์˜ ํ˜ธํ™˜ ์•„ํ‚คํ…์ฒ˜์™€ ์–ธ์–ด๋ฅผ ์„ ํƒํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

 

 

์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ๋‹ค๋ฉด ๋ฒ„์ „์„ ํ™•์ธํ•œ๋‹ค. (์ฒ˜์Œ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด 1์ผ ๊ฒƒ์ด๋‹ค.)

 

 

์ด์ œ ํ•จ์ˆ˜๋กœ ๋Œ์•„์™€, ์ƒˆ๋กœ์šด ๊ณ„์ธต์„ ์ƒ์„ฑํ•ด์ค€๋‹ค.

 

 

๋ฐฉ๊ธˆ ๋งŒ๋“ค์—ˆ๋˜ ๊ณ„์ธต์„ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

 

์ด๋กœ์จ ํ•ด๋‹น ํ•จ์ˆ˜์— firebase_admin ๋ชจ๋“ˆ์ด ์„ค์น˜๋˜์—ˆ๋‹ค.

 

 

 

 

ํŠธ๋ฆฌ๊ฑฐ ์ถ”๊ฐ€

 

์ด์ œ ํ•ด๋‹น ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์œ„ํ•œ ํŠธ๋ฆฌ๊ฑฐ๋กœ, SQSํ• ๋‹นํ•ด์ฃผ๋ฉด ๋œ๋‹ค.


๊ทธ ์ „์— ๊ถŒํ•œ์„ ์—ฐ๊ฒฐํ•ด์•ผ ํ•˜๋Š”๋ฐ,

 

ํ•ด๋‹น ํ•จ์ˆ˜์˜ ๊ตฌ์„ฑ-๊ถŒํ•œ์— ๋“ค์–ด๊ฐ€์„œ Role ๋กœ ๋“ค์–ด๊ฐ„๋‹ค.

 

 

์ •์ฑ… ์—ฐ๊ฒฐ์„ ์„ ํƒํ•œ๋‹ค.

 

AWSLambdaSQSQueueExecutionRole๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

 

์ •์ฑ…์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค๋ฉด ํ•จ์ˆ˜๋กœ ๋Œ์•„์™€ ํŠธ๋ฆฌ๊ฑฐ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

 

 

์ „์— ๋งŒ๋“ค์—ˆ๋˜ SQS๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

 

ํŠธ๋ฆฌ๊ฑฐ๊ฐ€ ์—ฐ๊ฒฐ๋˜์—ˆ๋‹ค.

 

์ด์ œ ํ•ด๋‹น SQS ํ์— ๋ฉ”์„ธ์ง€๊ฐ€ ๋“ค์–ด์˜ค๋ฉด Lambda ํ•จ์ˆ˜๋กœ ๋ฉ”์„ธ์ง€๊ฐ€ ์ „๋‹ฌ๋  ๊ฒƒ์ด๋‹ค.


ํ•จ์ˆ˜ ์ฝ”๋“œ ์ž‘์„ฑ

์ด์ œ ํ•ด๋‹น ํ•จ์ˆ˜๋กœ ๋ฉ”์„ธ์ง€๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด ์‹คํ–‰๋  ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ๋œ๋‹ค.

 

 

๋จผ์ € ํ•ด๋‹น ํด๋”์— firebase ํ”„๋กœ์ ํŠธ์—์„œ ๋ฐ›์€ ํ‚ค ์ •๋ณด (json)ํŒŒ์ผ์„ ๋„ฃ์–ด๋‘์–ด์•ผ ํ•œ๋‹ค.

 

import firebase_admin as admin
from firebase_admin import credentials
from firebase_admin import messaging

# Firebase ๊ณ„์ • ์ •๋ณด ๋กœ๋“œ
credential = credentials.Certificate('waitherAccountServiceKey.json')
admin.initialize_app(credential)

# ๋ฉ”์ธ ํ•จ์ˆ˜
def lambda_handler(event, context):
    parsed_payload = parse_sqs_message(event)
    return send_notification(parsed_payload)

# ํ‘ธ์‹œ ์•Œ๋ฆผ ์ „์†ก
def send_notification(payload):
    responses = []
    for token in payload['tokens']:
        message = messaging.Message(
            notification=messaging.Notification(
                title=payload['title'],
                body=payload['content']
            ),
            token=token
        )
        response = messaging.send(message)
        responses.append(response)
    return responses

# SQS ๋ฉ”์‹œ์ง€ ํŒŒ์‹ฑ
def parse_sqs_message(sqs_message):
    title = sqs_message['Records'][0]['messageAttributes']['title']['stringValue']
    tokens = sqs_message['Records'][0]['messageAttributes']['tokens']['stringValue']
    tokens = tokens[1:-1].split(", ")
    content = sqs_message['Records'][0]['body']
    return {"title": title, "content": content, "tokens": tokens}

 

๋ฉ”์„ธ์ง€๊ฐ€ ์ „์†ก๋˜๋ฉด lambda_handler ํ•จ์ˆ˜๊ฐ€ ์‹คํ–‰๋˜๋Š”๋ฐ, ๋ฐ›์€ ๋ฉ”์„ธ์ง€(event)๋ฅผ ํŒŒ์‹ฑํ•ด์„œ firebase_admin ๋ชจ๋“ˆ๋กœ ๋ฉ”์„ธ์ง€๋ฅผ ์ „์†กํ•œ๋‹ค.

 

์•ˆ์— ๋‚ด์šฉ ์ค‘ tokens๋ฅผ ์Šฌ๋ผ์ด์‹ฑํ•ด์„œ split ํ•˜๋Š” ๊ณผ์ •์ด ์žˆ๋Š”๋ฐ, ๊ทธ ์ด์œ ๋Š” ๋’ค์—์„œ ์„ค๋ช…ํ•˜๊ฒ ๋‹ค.

 

SQS๋กœ ๋ฐ›๋Š” ์ด๋ฒคํŠธ๋Š” ๋ณดํ†ต ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜•์‹์ด๋‹ค.

 

{
  "Records": [
    {
      "messageId": "{๋ฉ”์„ธ์ง€ ์•„์ด๋””}",
      "receiptHandle": "{SQS ๋ฉ”์„ธ์ง€ ๊ณ ์œ  ์‹๋ณ„์ž}",
      "body": "{๋ฉ”์„ธ์ง€ ๋ฐ”๋””}",
      "attributes": {
        "ApproximateReceiveCount": "10",
        "SentTimestamp": "1724855187392",
        "SenderId": "AIDAYS2NS75TM4GSXUFYB",
        "ApproximateFirstReceiveTimestamp": "1724855189392"
      },
      "messageAttributes": {
        "title": {
          "stringValue": "ํ…Œ์ŠคํŠธ ์ œ๋ชฉ์ž…๋‹ˆ๋‹ค.",
          "binaryValue": null,
          "stringListValues": [],
          "binaryListValues": [],
          "dataType": "String"
        },
        "token": {
          "stringValue": "[token1, token2, token3]",
          "binaryValue": null,
          "stringListValues": [],
          "binaryListValues": [],
          "dataType": "String"
        }
      },
      "md5OfMessageAttributes": "{์†์„ฑ ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ์šฉ MD5 ํ•ด์‹œ๊ฐ’}",
      "md5OfBody": "{๋ฐ”๋”” ๋ฌด๊ฒฐ์„ฑ ๊ฒ€์ฆ์šฉ MD5 ํ•ด์‹œ๊ฐ’}",
      "eventSource": "aws:sqs",
      "eventSourceARN": "{Role ARN}",
      "awsRegion": "ap-northeast-2"
    }
  ]
}

 

ํฐ ํ‹€๋กœ ๋‚˜๋ˆ„๋ฉด ๋ฉ”์„ธ์ง€ ์†์„ฑ(messageAttributes)๊ณผ body๋กœ ๋‚˜๋‰˜๋Š”๋ฐ,

 

๋‚˜๋Š” ๋ฉ”์„ธ์ง€ ์†์„ฑ์— ์‚ฌ์šฉ์ž ํ† ํฐ ๊ฐ’๋“ค๊ณผ ํ‘ธ์‹œ์•Œ๋ฆผ ์ œ๋ชฉ์„ ๋„ฃ์„ ๊ฒƒ์ด๊ณ , body์—๋Š” ํ‘ธ์‹œ์•Œ๋ฆผ ๋‚ด์šฉ์„ ๋„ฃ์„ ๊ฒƒ์ด๋‹ค.

 

ํ•ด๋‹น ๋‚ด์šฉ์„ ํŒŒ์‹ฑํ•˜๋Š”๊ฒŒ parse_sqs_message ํ•จ์ˆ˜์˜ ๋‚ด์šฉ์ด๋‹ค.

 

 

 

4. ์„œ๋น„์Šค ๋กœ์ง ๊ตฌํ˜„

์ด์ œ ํ‘ธ์‹œ์•Œ๋ฆผ์„ ์ „์†กํ•˜๋Š” ์„œ๋ฒ„์ชฝ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž.

 

Depdency(gradle)

implementation platform('software.amazon.awssdk:bom:2.21.20')
implementation 'software.amazon.awssdk:sqs'

 

๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” aws-sdk for java ๋ฒ„์ „ 2๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ์ด๋‹ค.

 

application.yml

cloud:
  aws:
    credentials:
      access-key: [access key]
      secret-key: [secret access key]
    stack:
      auto: false
    sqs:
      queue:
        message-delay-seconds: [message-delay-seconds]
        url: [SQS Queue URL]

 

 

AWSConfig

@Configuration
public class AwsConfig {
    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;
    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    //Credential
    private StaticCredentialsProvider createAwsCredentialsProvider() {
        AwsBasicCredentials basicAWSCredentials = AwsBasicCredentials.create(this.accessKey, this.secretKey);
        return StaticCredentialsProvider.create(basicAWSCredentials);
    }

    //SQS Async Client
    @Bean
    public SqsAsyncClient sqsAsyncClient() {
        return SqsAsyncClient.builder()
                .region(Region.AP_NORTHEAST_2) //์„œ์šธ Region
                .credentialsProvider(createAwsCredentialsProvider())
                .build();
    }
}

 

AWSConfig์—๋Š” SqsAsyncClient๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ด์ค€๋‹ค. 

 

SqsClient๋Š” ๋™๊ธฐ ๋ฐฉ์‹, SqsAsyncClient๋Š” ๋น„๋™๊ธฐ ๋ฐฉ์‹์˜ ํด๋ผ์ด์–ธํŠธ์ด๋‹ค.

 

์ž„์‹œ ์ž๊ฒฉ ์ฆ๋ช… ๊ฐ์ฒด(StaticCredentialProvider)๋Š” ์ฒ˜์Œ IAM์—์„œ ์ƒ์„ฑํ–ˆ๋˜ ์—‘์„ธ์Šค ํ‚ค์˜ access key, secret access key๋กœ๋ถ€ํ„ฐ ์ƒ์„ฑํ•œ๋‹ค.

 

 

AWSSqsProperties

@Getter
@Component
public class AwsSqsProperties {
    @Value("${cloud.aws.sqs.queue.url}")
    private String queueUrl;
    @Value("${cloud.aws.sqs.queue.message-delay-seconds}")
    private Integer messageDelaySecs;
}

 

SQS์— ์—ฐ๊ฒฐํ•  ์„ค์ •๋“ค์„ ๋ฏธ๋ฆฌ ์ปดํฌ๋„ŒํŠธ๋กœ ๋“ฑ๋กํ•ด ๋‘์—ˆ๋‹ค.

 

 

AwsSqsUtils

@Slf4j
@RequiredArgsConstructor
@Component
public class AwsSqsUtils {

    private final SqsAsyncClient sqsAsyncClient;
    private final AwsSqsProperties awsSqsProperties;

    public void sendMessage(SqsMessageDto messageDto) {
        Map<String, MessageAttributeValue> attributes = createAttributes(messageDto.tokens(), messageDto.title());

        SendMessageRequest request = createRequest(messageDto, attributes);

        try {
            CompletableFuture<SendMessageResponse> future = sqsAsyncClient.sendMessage(request);
            future.whenComplete((sendMessageResponse, throwable) -> {
                if (throwable == null) {
                    log.info("[SQS Async Client] ๋ฉ”์„ธ์ง€ ์ „์†ก ์„ฑ๊ณต Status ---> {}", sendMessageResponse.sdkHttpResponse().statusCode());
                    log.info("[SQS Async Client] ๋ฉ”์„ธ์ง€ ์ „์†ก ์„ฑ๊ณต ID ---> {}", sendMessageResponse.messageId());
                } else {
                    log.error("[SQS Async Client] ๋ฉ”์„ธ์ง€ ์ „์†ก ์‹คํŒจ ---> {}", throwable.getMessage());
                }
            });

        } catch (SqsException sqsException) {
            log.error("[SQS] SQS ๋ฉ”์„ธ์ง€ ์ „์†ก ์‹คํŒจ --> {}", sqsException.getMessage());
        }
    }

    private SendMessageRequest createRequest(SqsMessageDto messageDto, Map<String, MessageAttributeValue> attributes) {
        return SendMessageRequest.builder()
                .queueUrl(awsSqsProperties.getQueueUrl())
                .delaySeconds(awsSqsProperties.getMessageDelaySecs())
                .messageAttributes(attributes)
                .messageBody(messageDto.content())
                .build();
    }

    public void sendMessages(List<String> tokens, String title, String content) {
        sendMessage(new SqsMessageDto(tokens, title, content));
    }

    private static Map<String, MessageAttributeValue> createAttributes(List<String> tokens, String title) {
        Map<String, MessageAttributeValue> attributes = new HashMap<>();
        attributes.put("tokens", MessageAttributeValue.builder().stringValue(tokens.toString()).dataType("String").build());
        attributes.put("title", MessageAttributeValue.builder().stringValue(title).dataType("String").build());
        return attributes;
    }

}

 

์ด์ œ ๋ฉ”์„ธ์ง€๋ฅผ ์ „์†กํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.

 

๋น„๋™๊ธฐ ๋ฐฉ์‹์˜ AsyncSqsClient๋ฅผ ์‚ฌ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์—, ์‘๋‹ต์„ CompletableFuture๋กœ ๋ฐ›์•„์•ผํ•œ๋‹ค.

 

catchํ•˜๋Š” SqsException์€ AWS์—์„œ ์ง€์›ํ•˜๋Š” ์ „์†ก ์‹คํŒจ์˜ ์˜ˆ์™ธ๋“ค์ด ๋‹ด๊ฒจ์žˆ๊ณ , throwable์— ๋“ค์–ด์˜ค๋Š” ์˜ˆ์™ธ๋“ค์€ ์ „์†ก ํ›„ ์‹คํŒจ ์˜ˆ์™ธ๋“ค์„ ๋‹ด๊ณ ์žˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ ์—ฌ๋Ÿฌ token์ด ๋‹ด๊ฒจ์žˆ๋Š” ๋ฆฌ์ŠคํŠธ๋ฅผ ๋„ฃ๊ธฐ ์œ„ํ•ด์„œ Attribute builder์— ๋ฆฌ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” stringListValues ํ•จ์ˆ˜๊ฐ€ ์žˆ์ง€๋งŒ

 

StringListValues๋Š” ์•„์ง ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•œ๋‹ค ๐Ÿซ 

 

๋”ฐ๋ผ์„œ ๋ฆฌ์ŠคํŠธ๋ฅผ toString์œผ๋กœ ๋„ฃ๊ณ  ํŒŒ์ด์ฌ์œผ๋กœ ํŒŒ์‹ฑํ•˜๋Š” ๊ณผ์ •์œผ๋กœ ํ•  ์ˆ˜ ๋ฐ–์— ์—†์—ˆ๋‹ค.

 

์ด์ œ ํ•ด๋‹น Util์„ ์‚ฌ์šฉํ•ด์„œ ๋ฉ”์„ธ์ง€๋ฅผ ์ „์†กํ•˜๋ฉด ๋˜๊ฒ ๋‹ค.

 

 

5. ์ถ”๊ฐ€

 

AWS CloudWatch์—์„œ Lambda ํ•จ์ˆ˜์˜ ์‹คํ–‰ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

 

Lambda์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ง์ ‘ ์ƒ์„ฑํ•˜์—ฌ ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 

 

์ถ”๊ฐ€๋กœ, ๋”์šฑ ์•ˆ์ •์„ฑ ์žˆ๋Š” ์„œ๋น„๋ฅผ ์œ„ํ•ด SQS์˜ DLQ(Dead Letter Queue)๋ฅผ ์šด์šฉํ•  ์ˆ˜๋„ ์žˆ๊ฒ ๋‹ค.

 

 

 

6. ์„ฑ๋Šฅ ๊ฐœ์„  ํ…Œ์ŠคํŠธ

 

์œ„ ๊ตฌ์กฐ์˜ ์„ฑ๋Šฅ์€ ๋ˆˆ์— ๋„๊ฒŒ ๊ฐœ์„ ๋œ๋‹ค.

 

ํ† ํฐ์ด ๋ช‡ ๊ฐœ๋ผ๋„ ์š”์ฒญ์ด ํ•œ ๋ฒˆ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ ์ฐจ์ด๋ฅผ ๋ณด๊ธฐ ์œ„ํ•ด ๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ๋ฐ”๊พธ๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค.

SQS + Lambda ๊ธฐ์ค€, ํ† ํฐ 1000๊ฐœ ์ „์†ก

 

๊ธฐ์กด ์Šคํ”„๋ง Firebase-admin ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ, ํ† ํฐ 1000๊ฐœ ์ „์†ก

 

 

 

์‚ฌ์šฉ์ž 1000๋ช…์—๊ฒŒ ํ‘ธ์‹œ์•Œ๋ฆผ์„ ์ „์†กํ•˜๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ์„ ๋•Œ, ์•ฝ 98%(2620ms -> 36ms)๊ฐ€ ํ–ฅ์ƒ๋˜์—ˆ๋‹ค.

 

1000๋ช…์„ ํ…Œ์ŠคํŠธ ํ•ด๋ณด์•˜์ง€๋งŒ, ์‚ฌ์šฉ์ž๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๋งŽ์•„์งˆ์ˆ˜๋ก ์„ฑ๋Šฅ ์ฐจ์ด๋Š” ํ™•์‹คํ•˜๊ฒŒ ๋” ๋ฒŒ์–ด์งˆ ๊ฒƒ์ด๋‹ค.

 

์ด๋กœ์จ ์‚ฌ์šฉ์ž ์ˆ˜์— ๋ฌด๊ด€ํ•œ ํ‘ธ์‹œ์•Œ๋ฆผ ์ „์†ก ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•ด ๋ณด์•˜๋‹ค.

 

 

 

 

 

์ฐธ๊ณ 
https://cabi.oopy.io/1d1010b2-b287-43af-a2c1-bab35cdf1dd8