๋์์ฑ ๋ฌธ์ ๋ฐ๊ฒฌํ ํ ๊ณต๋ถํ๊ฒ ๋ ACID ์ ๋ฆฌ.
๋น์ ์ ํธ๋ํฝ ์ฐจ๋จ ๊ธฐ๋ฅ์ ๋ง๋ค๋ฉด์ Rate Limit์ด๋ Bot ํ์ง ๋ฏธ๋ค์จ์ด๋ฅผ ๋ถ์ด๋ ์์ ์ ๊ตฌํํ๊ณ ์์๋ค.
slowapi๋ฅผ ์ฐ๋ ค๋ค๊ฐ ๋ฌธ์๋ฅผ ๋ณด๋ in-memory ๊ธฐ๋ฐ์ด๋ผ๋ ๊ฑธ ์๊ฒ ๋๋ค. ๋ฉํฐ ์์ปค ํ๊ฒฝ์์๋ ์์ปค๋ง๋ค ์นด์ดํฐ๊ฐ ๋ฐ๋ก
๋์๊ฐ๋๊น Rate Limit์ด ์ฌ์ค์ ๋ฌด์๋ฏธํด์ง๋ค๋ ๊ฒ ๋ํ ๋ฐฐ์ฐ๊ฒ ๋์์ผ๋ฉฐ, Redis์ ์์ฒด ๊ธฐ๋ฅ์ ํตํด
ํด๋น ๋ถ๋ถ์ ํด์ํ ์ ์์์ ๊นจ๋ซ๊ณ Redis ์ ์ฉ ํ์์ฑ์ ๋๊ผ๋ค.
Request
↓
Bot ํ์ง ๋ฏธ๋ค์จ์ด
↓
Rate Limit (Redis ์นด์ดํฐ)
↓
Endpoint
โโโ ํฌ๋ ๋ง ์ฐจ๊ฐ (Redis DECR)
โโโ Job ์์ฑ → PostgreSQL
โโโ SQS ๋ฐํ
๊ทธ๋์ ์ ์ฒด ์ํคํ ์ฒ์ ์์ด Redis DECR๊ณผ ๊ฐ์ ๋ด๋ถ ๊ธฐ๋ฅ์ผ๋ก ์ฒ๋ฆฌํ๋ฉด ๋ฉํฐ ์์ปค ํ๊ฒฝ์์ ๋ฐ์ํ ์ ์๋
race condition์ ํด๊ฒฐํ ์ ์์์ ๋ฐฐ์ ํ์๋ถ๊ป ์ํคํ ์ฒ ๋ฌธ์๋ฅผ ๋๋ ธ๋๋ ๋ต๋ณ์ ํด์ฃผ์ จ๋ค.
"ํฌ๋ ๋ง์ ์ ๊ทผํ๋ ์์ปค๋ API ํ๋. 6๊ฐ ์์ปค๋ AI ๊ธฐ๋ฅ์ฉ์ด๋ผ
ํฌ๋ ๋ง ์๋ชจ ์ดํ์ ์์๋๋ ์ ๋ค์ด๊ณ , Redis๋ ์์ง ์ ํฌํํ ํด๋น ์ ๋ผ์."
์ฌ์ค ์ฒ์์ ๋ค์์ ๋๋ ์ ๋๋ก ์ดํดํ์ง ๋ชปํ๋ค. ๋๋ ๋ฉํฐ์์ปค๋ฅผ ๊ทธ๋ฅ ์์ปค๊ฐ ์ฌ๋ฌ๊ฐ๋ ๋ฉํฐ์์ปค๋ผ ์๊ฐํ๋๋ฐ,
๊ทธ๊ฒ ์๋์๊ณ ํด๋น์ด ์๋๋ค๊ณ ํ์ ๊ฑฐ๊ตฌ๋ ์ ๋๋ก๋ง ์๊ฐํ๋ค.
"DB์ ACID๋ง ์์๋ ๋์์ฑ ๋ฌธ์ ๋ฅผ ์ด๋ ์ ๋ ํด๊ฒฐํ ์ ์์ด์."
์ถ๊ฐ์ ์ผ๋ก ๋ง์ํด์ฃผ์ ์ ๋ค์ ๊ณต๋ถํด๋ณด๊ฒ ๋์๋ค.
ACID๊ฐ ๋ญ๋ฐ

ACID๋ Atomicity์์์ฑ, Consistency์ผ๊ด์ฑ, Isolation๊ฒฉ๋ฆฌ์ฑ/๋ ๋ฆฝ์ฑ, Durability์ง์์ฑ/์์์ฑ์ ์ฝ์ด.
- A:ํธ๋์ญ์ ๊ฐ ๋ช ๋ น์ ์ ๋ถ ์ฑ๊ณตํ๊ฑฐ๋ ์คํจํด์ผํ๋ฉฐ, ํ๋๋ผ๋ ์คํจํ๋ฉด ์ ์ฒด ์์ ์ทจ์
- C:ํธ๋์ญ์ ์ฑ๊ณตํ db๋ ์ ํด์ง ๊ท์น์ ์ ์งํด์ผํจ.
- I:์ฌ๋ฌ ํธ๋์ญ์ ๋์ ์คํ ๊ฐ ์๋ก ์ํฅ์ ์ฃผ์ง ์๋ ๋ ๋ฆฝ์ ๋์์ด ์ด๋ฃจ์ด์ ธ์ผํจ.
- D:์ฑ๊ณต๋ ํธ๋์ญ์ ์ ๊ฒฐ๊ณผ๋ ์์คํ ์ฅ์ ๊ฐ ๋ฐ์ํด๋ ์๊ตฌ์ ์ผ๋ก ๋ฐ์๋์ด์ผ ํ๋ค.
์ฐ๋ฆฌ ์ฝ๋๋ฅผ ๋ฏ์ด๋ณด์๋ค
๊ณต๋ถํ๊ณ ๋์ ์ค์ ์ฝ๋๋ฅผ ๋ค์ ๋ดค๋ค. ์๊ฐํด๋ณด๋ DB ํธ๋์ญ์ ๊ฐ ๋ค์ํ ๋ณ์ ๋ฐ์์ผ๋ก ์ค๊ฐ์ ๊ณผ์ ์ด ์ค๋จ๋ ์๋,
์คํจํ ์๋ ์๋๋ฐ ๊ทธ๋ฐ ์ค๋์คํจํ์ ๋๋ฅผ ๋๋นํ ์ฝ๋๊ฐ ์์๋ค. ๋ฒ๊ทธ ๋ฐ์ ์ ์กฐํํ ์ ์๋ ๋ก๊ฑฐ ๊ตฌํ์
์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ๋ค์ผ๋ฉฐ ์ฐจํ ์์ ํ์์์ง๋ง, ํ๋ก๋์ ๋ ๋ฒจ์ ์ฝ๋๋ ์ฝ๋ ๋ฒ ์ด์ค๊ฐ ์ปค์ง์๋ก ๋๋ฒ๊น ์ ์ํ ์ฅ์น์
๋ณ์์ ๋๋นํ๋ ์ ๋ต์ ์ฌ๋์๊ฒ ์ค๊ณํด๋์์ผ ํจ์ ์ด๋ฒ ๊ธฐํ๋ก ๋ค์ ํ ๋ฒ ๊นจ๋ฌ์๋ค.
์ฝ๋์์ ๋ฐ๊ฒฌํ๋ ACID ์๋ฐ ๋ฌธ์ ๋ค์ด๋ค.
rollback ๋ฌธ์ ์๋ณ
# ๋ฌธ์ ์ํฉ (์ต๋ช
ํ)
def deduct_and_record(db, user):
user.credits -= 1
db.commit() # 1์ฐจ ์ปค๋ฐ
record_event(db, ...) # ์ฌ๊ธฐ์ ํฐ์ง๋ฉด?
db.commit() # 2์ฐจ ์ปค๋ฐ — ํฌ๋ ๋ง๋ง ๋น ์ง ์ฑ๋ก ๋๋จ
ํฌ๋ ๋ง ์ฐจ๊ฐ(deduct_credit)์ด ๋จ๋ ์ปค๋ฐ๋ ๋ค, ์ด๋ฒคํธ ๊ธฐ๋ก(create_event)์ด๋ Job ์์ฑ(create_job)์์ ์์ธ๊ฐ ํฐ์ง๋ฉด
ํฌ๋ ๋ง์ ์ด๋ฏธ ๋น ์ง ์ํ์ธ๋ฐ ๋ถ์ ์์ ์ ์์ฑ๋์ง ์๋๋ค. ์ฌ์ฉ์ ์ ์ฅ์์๋ ํ์๊ฐ ์ค์๋๋ฐ ๊ฒฐ๊ณผ๊ฐ ์๋ ์ํฉ์ด ๋ ๊ฒ์ด์๋ค. ์ค์ ๋ก credit_service.deduct_credit()์ด db.commit()์ ์ง์ ํธ์ถํ๊ณ ์์๊ณ , ์ดํ job_repo.create_job()๊น์ง ๊ฐ๊ธฐ ์ ์
์ด๋์๋ ํฐ์ง๋ฉด ๋กค๋ฐฑ ๋์ ์์ฒด๊ฐ ์์๋ค. ๋๊ณผ ์ง๊ฒฐ๋๋ ํฌ๋ ๋ง ๊ด๋ จ ์ด์์๊ธฐ์ ์ฝ๋์์ ํฐ ๋ฌธ์ ๋ฅผ ๋ฐ๊ฒฌํ๊ฒ ๋ง์๋ค.
# ๊ฐ์ ๋ฐฉํฅ
def deduct_and_record(db, user):
try:
user.credits -= 1
record_event(db, ...)
db.commit() # ํ ๋ฒ๋ง
except:
db.rollback()
raise
deduct_credit()์ flush()๋ง ํ๋๋ก ๋ฐ๊พธ๊ณ , ์ด๋ฒคํธ ์์ฑ → Job ์์ฑ๊น์ง ์ ๋ถ ๊ฐ์ ํธ๋์ญ์ ์์์ ์งํํ ๋ค job_service
๋ ๋ฒจ์์ ํ ๋ฒ๋ง commit(). ์ค๊ฐ์ ์ด๋์ ํฐ์ ธ๋ except → db.rollback() ์ผ๋ก ํฌ๋ ๋ง ์ฐจ๊ฐ ์์ฒด๊ฐ ์ทจ์๋๋ ํ์์ผ๋ก ์์ ํ๋ค.
BQ ์ ์ก์ด fire-and-forget ๋ฌด์ฑ ์ํ ๋ฐฐ๋ฌ..?
์ด๋ฒคํธ๋ฅผ PG์ ์ ์ฅํ ์งํ, ThreadPoolExecutor๋ก BQ ์ ์ก์ ๋น๋๊ธฐ ์คํ๋๊ณ ์์๋ค๊ณ ํ๋ค. BQ ์ ์ก ํจ์๊ฐ executor.submit()์ผ๋ก ๋์ ธ์ง๊ณ , ๋ฉ์ธ ์ค๋ ๋๋ ๋ฐ๋ก 200์ ๋ฆฌํดํ๋ ๊ตฌ์กฐ...!
# ๋ฌธ์ ์ํฉ (์ต๋ช
ํ)
executor.submit(send_to_bq, data)
return {"status": "ok"} # BQ์ ์ ๋ค์ด๊ฐ๋ 200
BQ ์ ์ก์ด ์คํจํด๋ ์ด๋์๋ ๊ธฐ๋ก์ด ๋จ์ง ์๋ ์ํฉ์ด์๋ค..!! ๋คํธ์ํฌ ํ์์์, ์ธ์ฆ ๋ง๋ฃ, BQ ์ฟผํฐ ์ด๊ณผ ์ด๋ค ์ด์ ๋ก ์คํจํ๋ ๋ฐ์ดํฐ๋ ์กฐ์ฉํ ์ฌ๋ผ์ง๋ค. ๋ถ์ ๋์๋ณด๋์์ ์ด์ ์ด๋ฒคํธ๊ฐ ์ ์ ๋ฐ๋ฐ์ ์ ์กํ์ง? ๊ฐ์ ์ํฉ์ด ์๊ฒจ๋ ์์ธ ์ถ์ ์ด ๋ถ๊ฐ๋ฅํ
์ฝ๋๋ก ๊ตฌํ์ด ๋์ด์์๊ณ , ์ด ๋ํ ์ถํ ์ค์ํ ์์ ์ฌํญ์ผ๋ก ๋ถ๊ฑฐ์ง ์ ์๋ ๋ฌธ์ ์๋ค.
## ๊ฐ์ ๋ฐฉํฅ >> outbox
def record_event(db, ...):
event = Event(...)
db.add(event)
db.flush()
outbox = BqOutbox(event_id=event.id) # ๊ฐ์ ํธ๋์ญ์
db.add(outbox)
db.flush()
# commit์ ํธ์ถ์๊ฐ — PG ์ ์ฅ๊ณผ outbox ๊ธฐ๋ก์ด ์์์ ์ผ๋ก ๋ฌถ์
outbox ํจํด์ด๋, ๋ฉ์์ง๋ฅผ ์ธ๋ถ์์คํ (BigQuery)์ ์์ ํ๊ฒ ์ ๋ฌํ๊ธฐ ์ํ ํจํด์ผ๋ก ๋ณด๋ผ ๋ฉ์์ง๋ฅผ ํ ์ด๋ธ์ ์ ์ด๋๊ณ , ๋์ค์ ๊บผ๋ด์ ๋ณด๋ด๋ ํจํด์ด๋ผ๊ณ ํ๋ค. ๊ธฐ์กด ๋ฐฉ์์ด ์ฃผ๋ฌธ์๋ฅผ ์์ฑํด๋๊ณ ๋ฐ๋ก ํ๋ฐฐ๋ฅผ ๋์ง๋ ํ์์ผ๋ก ํ๋ฐฐ๊ฐ ๋ถ์ค๋ผ๋
๋ชจ๋ฅด๋ ํํ์๋ค๋ฉด, ์ฃผ๋ฌธ์ ์์ฑ >> ๋ฐ์ก๋๊ธฐ ๋ชฉ๋ก์ ๊ธฐ๋ก >> ํ๋ฐฐ๊ธฐ์ฌ๊ฐ ๋ชฉ๋กํ์ธ >> ๋ฐ์ก / ์คํจ ์ ๋ชฉ๋ก์
๋จ์์๋ ๊ฒ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์๋๋ก ๋ณด์๋ ํจํด์ด๋ค.
PG(PostgreSQL)์ ์ด๋ฒคํธ๋ฅผ ์ ์ฅํ ๋ bq_outbox ํ ์ด๋ธ์๋ ๊ฐ์ ํธ๋์ญ์ ์ผ๋ก "์ ์ก ๋๊ธฐ" ๋ ์ฝ๋๋ฅผ ๋จ๊ธด๋ค.
์ค์ผ์ค๋ฌ๊ฐ 30์ด ๊ฐ๊ฒฉ์ผ๋ก pending ๋ ์ฝ๋๋ฅผ ์ฝ์ด BQ์ batch insertํ๊ณ , ์ฑ๊ณตํ๋ฉด sent, ์คํจํ๋ฉด failed๋ก ๋งํน.
์คํจํ ๊ฑด์ ์ฌ์๋ ๋์์ผ๋ก ๋จ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ ์ ์ค์ ๋ฐฉ์งํ ์ ์๋ค!! fire-and-forget → at-least-once delivery ๋ณด์ฅ.
์ ๋ฆฌํ๋ฉด์
ํ๋ก๋์ ๋ ๋ฒจ์ ์ฝ๋๋ฅผ ๊ตฌํํด๋ณด๋ฉด์, ๋ชจ๋ฅด๋๊ฒ ์ ๋ง์ ๋ง ๋ง๋ค๋ ๊ฒ์ ํฌํจํ์ฌ ํต์ฌ์ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ๊ฒ์ ๊ทธ์น๋ ๊ฒ์ด ์๋๋ผ
๋ณ์์ ์ด์, ์ถํ ์์ ๊ฐ๋ฅ์ฑ์ ๋๋นํ๊ณ ์ ์ ๋ ๋น๋ํด์ง ์ฝ๋ ๋ฒ ์ด์ค๋ฅผ ์ค์ค๋ก๋ ๊ธฐ๋ฅ๋ณ๋ก ์ฝ๊ฒ ์ฐพ์๊ฐ ์์ ํ๊ณ ๋๋ฒ๊น ํ ์ ์๋ ํ๊ฒฝ์ ๋ง๋๋ ๊ฒ์ ๋ํ ์ค์์ฑ์ ๋ฐฐ์ด๋ค.
์ด๋ก ์ผ๋ก ๋ฐฐ์ ๋ DB ACID ๋ํ ๋ฐฐ์ด์ ์ด ์์์ด๋ ์ด๋ก ์ผ๋ก๋ง ์๊ฐํด ๊ธฐ์ต๋ ๋์ง ์์์ง๋ง ์ค๋ฌด์์ ๊ฐ์ฅ ๊ธฐ์ด๊ฐ ์ค์ํ ๊ฒ์ฒ๋ผ
์ด๋ก ๊ณต๋ถ๋ ์ทจ์ ์ ์ํ CS ์ง์ ๊ณต๋ถ๋ ์ทจ์ ์ ์ํจ์ด ์๋ ๋ชจ๋ ์๋์ด ๊ฐ๋ฐ์๋ก ์ฑ์ฅํ ์ ์๋ ๊ทํ ์ฑ์ฅ์ฌ๋ฃ๊ฐ ๋จ์ ์ค์ค๋ก
์ธ์งํ๊ณ ๊ฐ๋ณ๊ฒ ์ฌ๊ฒจ์ ์๋๊ฒ ๋ค ์๊ฐํ๊ฒ ๋๋ ์ค์ํ ๊ฒฝํ์ ํ ์ ์์๋ค.