Skip to content

hak0622/ticketflow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

172 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽŠ TicketFlow ๐ŸŽŠ

ํ‹ฐ์ผ“ํŒ… ํ™˜๊ฒฝ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ดˆ๊ณผ ์˜ˆ๋งคยท์ค‘๋ณต ๊ฒฐ์ œยท๋ณด์ƒ ๋ˆ„๋ฝยท๋ฐฉ์น˜ ์˜ˆ์•ฝ ๋ฌธ์ œ๋ฅผ ์ง์ ‘ ์žฌํ˜„ํ•˜๊ณ  ๋‹จ๊ณ„์ ์œผ๋กœ ํ•ด๊ฒฐํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.


๐Ÿ“œ ๋ชฉ์ฐจ


๐Ÿ™‹โ€โ™‚๏ธ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

ํ‹ฐ์ผ“ํŒ… ์„œ๋น„์Šค์—์„œ "์ขŒ์„์ด 1๊ฐœ ๋‚จ์•˜๋Š”๋ฐ 2๋ช…์ด ์˜ˆ๋งค๋๋‹ค", "๊ฒฐ์ œ๊ฐ€ ์‹คํŒจํ–ˆ๋Š”๋ฐ ์ขŒ์„์ด ๋Œ์•„์˜ค์ง€ ์•Š์•˜๋‹ค" ๊ฐ™์€ ๋ฌธ์ œ๋Š” ์™œ ์ƒ๊ธฐ๋Š” ๊ฑธ๊นŒ. ์ด ํ”„๋กœ์ ํŠธ๋Š” ๊ทธ ์งˆ๋ฌธ์—์„œ ์ถœ๋ฐœํ–ˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ์—๋Š” SELECT ... FOR UPDATE๋กœ ์ž”์—ฌ์„์„ ์ง๋ ฌํ™”ํ•˜๋ฉด ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ๋ดค์Šต๋‹ˆ๋‹ค. ์ •ํ•ฉ์„ฑ์€ ์ง€์ผœ์กŒ์ง€๋งŒ 500๋ช… ๋™์‹œ ์š”์ฒญ์—์„œ ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„์ด 3.7์ดˆ๊นŒ์ง€ ์˜ฌ๋ผ๊ฐ”์Šต๋‹ˆ๋‹ค. Redis๋กœ ์ขŒ์„ ์ œ์–ด๋ฅผ ์˜ฎ๊ฒผ๋”๋‹ˆ ์ด๋ฒˆ์—” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์œ„์น˜์—์„œ deadlock์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. "๊ฒฐ์ œ ์‹คํŒจ ์งํ›„ ์ฆ‰์‹œ ๋ณด์ƒ"์ด ์ง๊ด€์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋Š”๋ฐ, ์‹คํŒจ ์ฃผ์ž… ์‹คํ—˜์—์„œ ๋ณด์ƒ ํ˜ธ์ถœ ์ž์ฒด๋„ ๊ฐ™์€ ๋น„์œจ๋กœ ์œ ์‹ค๋์Šต๋‹ˆ๋‹ค.

๋งค ๋‹จ๊ณ„๋งˆ๋‹ค "์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค"๋Š” ๊ฐ€์„ค์ด ํ‹€๋ ธ๊ณ , ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ์™€ ์‹คํ—˜ ์ฝ”๋“œ๋กœ ์ง์ ‘ ํ™•์ธํ•œ ๋’ค์—์•ผ ๋‹ค์Œ ๊ตฌ์กฐ๋กœ ๋„˜์–ด๊ฐ”์Šต๋‹ˆ๋‹ค.

์ด ํ”„๋กœ์ ํŠธ์—์„œ ์ค‘์ ์ ์œผ๋กœ ๊ณ ๋ฏผํ•œ ๊ฒƒ์€ ์„ธ ๊ฐ€์ง€์ž…๋‹ˆ๋‹ค.

  • ์ •ํ•ฉ์„ฑ vs ์ฒ˜๋ฆฌ๋Ÿ‰ โ€” Lock ๋ฒ”์œ„๋ฅผ ์–ด๋””๊นŒ์ง€ ์žก์•„์•ผ ์ดˆ๊ณผ ์˜ˆ๋งค๋ฅผ ๋ง‰์œผ๋ฉด์„œ๋„ ์‘๋‹ต ์‹œ๊ฐ„์ด ๊ฒฌ๋”œ ๋งŒํ•œ๊ฐ€
  • ๋ฉฑ๋“ฑ์„ฑ์˜ ๋ฒ”์œ„ โ€” "์ค‘๋ณต ์š”์ฒญ ์ฐจ๋‹จ"๊ณผ "๋™์ผ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜"์€ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ •์ƒ ์žฌ์‹œ๋„์™€ ๋™์‹œ ์ค‘๋ณต ํด๋ฆญ์€ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฐฉ์–ด์„ ์ด ํ•„์š”ํ•˜๋‹ค
  • ๋ณด์ƒ์˜ ์™„๊ฒฐ์„ฑ โ€” ๊ฒฐ์ œ ์‹คํŒจ ์‚ฌ์‹ค์„ DB์— ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ๊ณผ ๋ณด์ƒ ๋Œ€์ƒ์„ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์€ ๋ณ„๊ฐœ์˜ ์ž‘์—…์ด๋‹ค. ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์— ๋ฌถ์ด์ง€ ์•Š์œผ๋ฉด ์•ฑ์ด ์žฌ์‹œ์ž‘ํ•  ๋•Œ ๋ณด์ƒ ๋Œ€์ƒ ์ •๋ณด๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค

๐Ÿ“Š ์˜ˆ์ƒ ํŠธ๋ž˜ํ”ฝ ๋ฐ ์ธํ”„๋ผ ์„ค๊ณ„ ๊ธฐ์ค€

์‹œ๋‚˜๋ฆฌ์˜ค ๊ฐ€์ •: DAU 15,000๋ช…, ์ €๋… 8์‹œ ํ‹ฐ์ผ“ ์˜คํ”ˆ, 5๋ถ„ ์ด๋‚ด์— 10,000๋ช…์ด ๋Œ€๊ธฐ์—ด์— ์ง„์ž…ํ•ฉ๋‹ˆ๋‹ค.

์ด ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ€ํ•˜๋ฅผ ์„ธ ๊ฐ€์ง€๋กœ ๋‚˜๋ˆ ์„œ ์ ‘๊ทผํ–ˆ์Šต๋‹ˆ๋‹ค.

์ˆœ์œ„ ๋ถ€ํ•˜ ์œ ํ˜• ์—”๋“œํฌ์ธํŠธ ๊ณ„์‚ฐ
1์ˆœ์œ„ ๋Œ€๊ธฐ์—ด ์ง„์ž… spike POST /queue 10,000๋ช… / 300์ดˆ โ‰ˆ 33 req/s (๋‹จ์‹œ๊ฐ„ ์ง‘์ค‘)
2์ˆœ์œ„ ๋Œ€๊ธฐ์—ด polling ์ง€์† ๋ถ€ํ•˜ GET /queue/me 10,000๋ช… ร— 1/5s = 2,000 req/s (๊ณ ์ • polling ๊ธฐ์ค€)
3์ˆœ์œ„ ์˜ˆ๋งค ๋™์‹œ์„ฑ POST /booking ์ž…์žฅ๊ถŒ ๋ฐœ๊ธ‰ ํ›„ ๋Œ€๊ธฐ์ž๋“ค์ด ์ง‘์ค‘์ ์œผ๋กœ ์˜ˆ๋งค ์š”์ฒญ (์ •ํ•ฉ์„ฑ ์ค‘์‹ฌ)

polling์ด ์ง„์งœ ๋ณ‘๋ชฉ์ž…๋‹ˆ๋‹ค. ๋Œ€๊ธฐ์—ด ์ง„์ž…์€ 33 req/s์— ๋ถˆ๊ณผํ•˜์ง€๋งŒ, ์ง„์ž… ํ›„ ๋ชจ๋‘๊ฐ€ 5์ดˆ๋งˆ๋‹ค ์ˆœ๋ฒˆ์„ ์กฐํšŒํ•˜๋ฉด ์„œ๋ฒ„๋Š” 2,000 req/s๋ฅผ ๊ณ„์† ๋ฐ›์Šต๋‹ˆ๋‹ค. Adaptive Polling(position > 1,000์ด๋ฉด 10์ดˆ ๊ฐ„๊ฒฉ)์„ ์ ์šฉํ•˜๋ฉด ์ด๋ก ์ƒ 1,100 req/s๋กœ ์ค„์–ด๋“ญ๋‹ˆ๋‹ค.

์ด ์ˆ˜์น˜๋ฅผ ๋ชฉํ‘œ ๊ธฐ์ค€์œผ๋กœ ์žก๊ณ  AWS EC2 2๋Œ€ โ†’ GCP 3๋Œ€ + GCP LB๋กœ ๋‹จ๊ณ„์ ์œผ๋กœ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“น ๋ฐ๋ชจ ์˜์ƒ

default.MOV


๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ

Backend

Java Spring Boot Spring Security Spring Data JPA QueryDSL Gradle

Data / Auth

MySQL Redis H2 JWT OAuth2

Frontend

React Vite Tailwind CSS

Infra / Monitoring / Testing

GCP GCP LB Vercel Cloudinary Docker Compose GitHub Actions Prometheus Grafana JUnit5 k6 ShedLock


๐ŸŒ ๋ฐฐํฌ ๊ตฌ์กฐ

ํ˜„์žฌ ๋ฐฐํฌ ๊ตฌ์กฐ

์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ €
    โ”‚
    โ–ผ
Vercel (React ํ”„๋ก ํŠธ์—”๋“œ)
    โ”‚
    โ–ผ
GCP HTTP(S) Load Balancer
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ–ผ                          โ–ผ                          โ–ผ
Spring Boot app-1          Spring Boot app-2          Spring Boot app-3
(Server1)                  (Server2)                  (Server3)
    โ”‚
    โ”œโ”€โ”€ MySQL
    โ”œโ”€โ”€ Redis
    โ”œโ”€โ”€ Prometheus
    โ””โ”€โ”€ Grafana
  • Server1: Spring Boot app + MySQL + Redis + Prometheus + Grafana
  • Server2/3: Spring Boot app only
  • ๋Œ€๊ธฐ์—ด ์„ฑ๋Šฅ ์ธก์ •์€ ์ด GCP Load Balancer + app 3๋Œ€ ๊ตฌ์„ฑ์„ ๊ธฐ์ค€์œผ๋กœ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

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

์ฒ˜์Œ์—๋Š” AWS EC2 2๋Œ€ + Nginx ๋กœ๋“œ๋ฐธ๋Ÿฐ์„œ๋กœ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋ฆฌํ‹ฐ์–ด์™€ ์ž‘์€ VM ์ŠคํŽ™์—์„œ ์–ด๋””๊นŒ์ง€ ๋ฒ„ํ‹ธ ์ˆ˜ ์žˆ๋Š”์ง€ ๋จผ์ € ํ™•์ธํ–ˆ๊ณ , ์ดํ›„ ๋” ํฐ ๊ทœ๋ชจ์˜ ๋ถ€ํ•˜ ์‹คํ—˜๊ณผ ์Šค์ผ€์ผ์—…/์Šค์ผ€์ผ์•„์›ƒ ๋น„๊ต๋ฅผ ์œ„ํ•ด GCP๋กœ ์ด์ „ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž ๋ธŒ๋ผ์šฐ์ €
    โ”‚
    โ–ผ
Vercel (React ํ”„๋ก ํŠธ์—”๋“œ)
    โ”‚  /api/* ์š”์ฒญ โ†’ Vercel rewrites๋กœ EC2๋กœ ํ”„๋ก์‹œ (CORS ํ•ด๊ฒฐ)
    โ–ผ
Nginx (EC2 #1, ํฌํŠธ 80 โ€” ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ)
    โ”‚  Round-Robin
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ–ผ                          โ–ผ
Spring Boot app-1          Spring Boot app-2
(EC2 #1, Docker)           (EC2 #2, Docker)
    โ”‚                          โ”‚
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
               โ–ผ
          MySQL + Redis
          (EC2 #1, Docker, ๊ณต์œ )

์ด๋ฏธ์ง€(๊ณต์—ฐ ํฌ์Šคํ„ฐ ๋“ฑ)๋Š” Cloudinary CDN์„ ํ†ตํ•ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.
CI/CD๋Š” GitHub Actions โ†’ Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ โ†’ EC2 ๋ฐฐํฌ ์ˆœ์œผ๋กœ ์ž๋™ํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ’ก ํ•ด๊ฒฐํ•œ ๋ฌธ์ œ

๋ฌธ์ œ ์›์ธ ํ•ด๊ฒฐ ๋ฐฉํ–ฅ
์ดˆ๊ณผ ์˜ˆ๋งค ์ž”์—ฌ์„ ํ™•์ธ๊ณผ INSERT ์‚ฌ์ด ํƒ€์ด๋ฐ ์ฐจ์ด Redis DECR ์›์ž ์—ฐ์‚ฐ์œผ๋กœ ์ขŒ์„ ์ ์œ  ์„ ๊ฒฐ์ •
์ค‘๋ณต ๊ฒฐ์ œ ์ •์ƒ ์žฌ์‹œ๋„์™€ ๋™์‹œ ์ค‘๋ณต ์š”์ฒญ์€ ์„œ๋กœ ๋‹ค๋ฅธ ์‹คํŒจ ์ง€์ ์„ ๊ฐ€์ง DB ์‚ฌ์ „ ์กฐํšŒ + Redis SETNX + DB unique key 3๊ณ„์ธต ๋ถ„๋ฆฌ
๋ณด์ƒ ๋ˆ„๋ฝ ๊ฒฐ์ œ ์‹คํŒจ ํ›„ ๋ณด์ƒ ํ˜ธ์ถœ์ด ์œ ์‹ค๋˜๋ฉด ์ขŒ์„์ด ๋ณต๊ตฌ๋˜์ง€ ์•Š์Œ Transactional Outbox๋กœ ๋ณด์ƒ ๋Œ€์ƒ์„ ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์— ๊ธฐ๋ก
๋ฐฉ์น˜ ์˜ˆ์•ฝ ๋ฏธ๊ฒฐ์ œ ์˜ˆ์•ฝ์ด ์ขŒ์„์„ ์žฅ๊ธฐ ์ ์œ  30๋ถ„ ์ดˆ๊ณผ PENDING_PAYMENT ์ž๋™ ์ทจ์†Œ + Redis ์ขŒ์„ ๋ณต์›
๋Œ€๊ธฐ์—ด ์ง„์ž… spike ํ‹ฐ์ผ“ ์˜คํ”ˆ ์ˆœ๊ฐ„ ๋Œ€๋Ÿ‰์˜ queue ๋“ฑ๋ก ์š”์ฒญ์ด ์งง์€ ์‹œ๊ฐ„์— ์ง‘์ค‘ Redis ๊ธฐ๋ฐ˜ enqueue ๊ตฌ์กฐ๋กœ 10,000๋ช… ์ฑ„์šฐ๊ธฐ ์‹œ๋‚˜๋ฆฌ์˜ค๊นŒ์ง€ 0% ์—๋Ÿฌ๋กœ ์ฒ˜๋ฆฌ
๋Œ€๊ธฐ์—ด polling ์ง€์† ๋ถ€ํ•˜ ๋Œ€๊ธฐ ์ธ์›์ด ๋งŽ์•„์งˆ์ˆ˜๋ก /queue/me ์š”์ฒญ ์ˆ˜๊ฐ€ ์„ ํ˜• ์ฆ๊ฐ€ 3๋Œ€ ์Šค์ผ€์ผ์•„์›ƒ๊ณผ Adaptive Polling์œผ๋กœ ํ‰๊ท  ์กฐํšŒ ๋ถ€ํ•˜๋ฅผ ๋‚ฎ์ถ”๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๊ฐœ์„ 

๊ฐ ๋ฌธ์ œ๋ฅผ experiments/ ํŒจํ‚ค์ง€์—์„œ ์ง์ ‘ ์žฌํ˜„ํ•˜๊ณ , ๋กœ๊ทธ์™€ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์›์ธ์„ ์ขํ˜€๊ฐ€๋ฉฐ ๊ตฌ์กฐ๋ฅผ ๋ฐ”๊ฟจ์Šต๋‹ˆ๋‹ค.


๐Ÿ”„ ์˜ˆ๋งค ํ”Œ๋กœ์šฐ

์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜

MVP์—์„œ๋Š” DB row lock์œผ๋กœ ์ขŒ์„์„ ์ œ์–ดํ–ˆ์œผ๋‚˜, ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ์—์„œ hot row ๋ณ‘๋ชฉ์„ ํ™•์ธํ•œ ๋’ค "์‹ค์‹œ๊ฐ„ ์ œ์–ด๋Š” Redis, ์˜์† ์ƒํƒœ ์ €์žฅ์€ DB"๋กœ ์—ญํ• ์„ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ (GCP 3์„œ๋ฒ„ โ€” ํ˜„์žฌ)

์ด์ „ ์•„ํ‚คํ…์ฒ˜ โ€” AWS EC2 2์„œ๋ฒ„ + Nginx

์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ (2์„œ๋ฒ„)

์ดˆ๊ธฐ ์•„ํ‚คํ…์ฒ˜ โ€” ๋‹จ์ผ ์„œ๋ฒ„ (AWS Elastic Beanstalk โ†’ EC2)
์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜ (1์„œ๋ฒ„)

์ดˆ๊ธฐ ๋ฐฐํฌ๋Š” AWS Elastic Beanstalk์œผ๋กœ ์‹œ์ž‘ํ–ˆ์œผ๋‚˜, ๋น„์šฉ ์ ˆ๊ฐ๊ณผ ์ง์ ‘ ์ œ์–ด๋ฅผ ์œ„ํ•ด EC2 + Docker + Nginx ๊ตฌ์„ฑ์œผ๋กœ ์ „ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋งค ํ”Œ๋กœ์šฐ

sequenceDiagram
    actor User
    participant Redis
    participant BookingService
    participant DB

    User->>Redis: โ‘  ๋Œ€๊ธฐ์—ด ๋“ฑ๋ก (ZADD, score=timestamp)

    loop ์ˆœ๋ฒˆ ํด๋ง (GET /queue/me)
        User->>Redis: ZRANK
        Redis-->>User: ํ˜„์žฌ ์ˆœ๋ฒˆ
    end

    Note over Redis: ConcertQueueScheduler (1s ์ฃผ๊ธฐ)<br/>Lua: ZRANGE โ†’ ZREM โ†’ SETEX (์ƒ์œ„ 100๋ช… ์ž…์žฅ๊ถŒ ๋ฐœ๊ธ‰, TTL 600s)<br/>๋Œ€๊ธฐ์—ด์—์„œ ์ œ๊ฑฐ๋˜๋ฉด ZRANK = null โ†’ ์ž…์žฅ ๊ฐ€๋Šฅ

    User->>BookingService: โ‘ก ์˜ˆ๋งค ์š”์ฒญ
    BookingService->>Redis: Lua CLAIM_ADMITTED (EXISTS โ†’ TTL โ†’ DEL, ์›์ž์ )
    BookingService->>Redis: DECR seats:concert:{id}
    Note over BookingService: ์ž…์žฅ๊ถŒ ์—†์œผ๋ฉด ์˜ˆ๋งค ๊ฑฐ๋ถ€
    BookingService->>Redis: GET concert:status:{id} (์บ์‹œ ํžˆํŠธ ์‹œ DB ์ƒ๋žต)
    BookingService->>DB: SELECT Concert (์บ์‹œ ๋ฏธ์Šค ์‹œ์—๋งŒ)
    BookingService->>DB: INSERT Booking (PENDING_PAYMENT)
    Note over BookingService: bookedCount๋Š” Redis remaining ๊ธฐ๋ฐ˜ ํŒŒ์ƒ๊ฐ’์œผ๋กœ ์กฐํšŒ ์‹œ ๊ณ„์‚ฐ
    BookingService-->>User: ์˜ˆ๋งค ์™„๋ฃŒ

    User->>BookingService: โ‘ข ๊ฒฐ์ œ ์š”์ฒญ (idempotencyKey)
    BookingService->>DB: SELECT Payment (๋ฉฑ๋“ฑ์„ฑ ์‚ฌ์ „ ์กฐํšŒ)
    BookingService->>Redis: SETNX payment:idempotency:{key} (TTL 30s)
    BookingService->>DB: INSERT Payment (PENDING) โ†’ Mock PG ์ฒ˜๋ฆฌ
    BookingService->>DB: Payment (COMPLETED) + Booking (CONFIRMED)
    BookingService-->>User: ๊ฒฐ์ œ ์™„๋ฃŒ
    Note over BookingService: finally: Redis ๋ฉฑ๋“ฑ์„ฑ ํ‚ค ์‚ญ์ œ
Loading

๊ฒฐ์ œ ์‹คํŒจ ๋ณด์ƒ ํ”Œ๋กœ์šฐ

๋ณด์ƒ์„ "๊ฒฐ์ œ ์‹คํŒจ ์งํ›„ ์ฆ‰์‹œ ํ˜ธ์ถœ"๋กœ ๋จผ์ € ๊ตฌํ˜„ํ–ˆ์ง€๋งŒ, ์‹คํŒจ ์ฃผ์ž… ์‹คํ—˜์—์„œ ๋ณด์ƒ ํ˜ธ์ถœ ์ž์ฒด๋„ ๊ฐ™์€ ๋น„์œจ๋กœ ์œ ์‹ค๋์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ๋ณด์ƒ ์‚ฌ์‹ค์„ Outbox์— ๋‚จ๊ธฐ๊ณ , ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ์ขŒ์„ ๋ณต๊ตฌ์™€ ์ƒํƒœ ์ „ํ™˜์„ ์ฑ…์ž„์ง‘๋‹ˆ๋‹ค.

sequenceDiagram
    participant PaymentService
    participant DB
    participant Redis
    participant Scheduler

    Note over PaymentService: Mock PG ๊ฒฐ์ œ ์‹คํŒจ
    PaymentService->>DB: Payment.fail() + INSERT Outbox(PENDING)<br/>โ€” ๋™์ผ ํŠธ๋žœ์žญ์…˜ ์›์ž์  ์ปค๋ฐ‹
    PaymentService-->>User: ๊ฒฐ์ œ ์‹คํŒจ ์‘๋‹ต

    Note over Scheduler: PaymentCompensationScheduler (10s ์ฃผ๊ธฐ, ShedLock)
    Scheduler->>DB: SELECT Outbox WHERE status = PENDING

    loop ๊ฑด๋ณ„ REQUIRES_NEW ํŠธ๋žœ์žญ์…˜
        Scheduler->>DB: SELECT Booking
        alt ์ด๋ฏธ CANCELLED (๋ฉฑ๋“ฑ ์ฒ˜๋ฆฌ)
            Scheduler->>DB: Outbox โ†’ PUBLISHED (์ขŒ์„ ๋ณต์› ์—†์Œ)
        else PENDING_PAYMENT
            Scheduler->>DB: Booking โ†’ CANCELLED
            Scheduler->>Redis: restoreSeat(concertId)
            Scheduler->>DB: SOLD_OUT ์ด๋ฉด OPEN ์œผ๋กœ ์ƒํƒœ ๋ณต๊ตฌ
            Scheduler->>DB: Outbox โ†’ PUBLISHED
        end
    end
Loading

๐Ÿ”ฅ ํ•ต์‹ฌ ๊ธฐ์ˆ  ์„ ํƒ ์ด์œ 

Redis Sorted Set + Lua Script โ€” ๋Œ€๊ธฐ์—ด์„ ์™œ Redis๋กœ ์˜ฎ๊ฒผ๊ณ , ์™œ Lua๊ฐ€ ํ•„์š”ํ–ˆ๋‚˜

์ฒ˜์Œ์—๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค

DB ํ…Œ์ด๋ธ” ํ•˜๋‚˜๋กœ ๋Œ€๊ธฐ์—ด์„ ๊ตฌํ˜„ํ•ด๋„ ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ๋ดค์Šต๋‹ˆ๋‹ค. INSERT INTO queue (user_id, created_at) ํ›„ COUNT(*) ๋กœ ์ˆœ๋ฒˆ์„ ๊ณ„์‚ฐํ•˜๋ฉด ๋‹จ์ˆœํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค.

๋ฌธ์ œ๋ฅผ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค

ํ‹ฐ์ผ“ํŒ… ์˜คํ”ˆ ์ˆœ๊ฐ„์—๋Š” ๋“ฑ๋ก๋ณด๋‹ค ์กฐํšŒ๊ฐ€ ํ›จ์”ฌ ๋งŽ์Šต๋‹ˆ๋‹ค. ์ˆ˜๋ฐฑ ๋ช…์ด ์งง์€ ์ฃผ๊ธฐ๋กœ ์ˆœ๋ฒˆ์„ ํด๋งํ•˜๋ฉด COUNT(*) ์ฟผ๋ฆฌ๊ฐ€ ๋ฐ˜๋ณต๋˜๊ณ , ๋Œ€๊ธฐ์—ด ์ฝ๊ธฐ ๋น„์šฉ์ด ๊ทธ๋Œ€๋กœ DB์— ์Œ“์˜€์Šต๋‹ˆ๋‹ค.

๋Œ€์•ˆ์„ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

DB ํ…Œ์ด๋ธ”: ๊ตฌํ˜„์ด ๋‹จ์ˆœํ•˜์ง€๋งŒ, ์ˆœ๋ฒˆ ์กฐํšŒ ์ฟผ๋ฆฌ๊ฐ€ ํด๋ง๋งˆ๋‹ค ๋ฐ˜๋ณต๋ผ ๊ณ ๋™์‹œ์„ฑ์—์„œ DB ๋ถ€ํ•˜๊ฐ€ ์ง‘์ค‘๋ฉ๋‹ˆ๋‹ค.

Redis List: LPUSH/LINDEX๋กœ ์ˆœ๋ฒˆ ์กฐํšŒ๋Š” ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์ค‘๊ฐ„ ์‚ญ์ œ(์ž…์žฅ ์ฒ˜๋ฆฌ ์‹œ LREM)๊ฐ€ O(N)์ž…๋‹ˆ๋‹ค. ๋Œ€๊ธฐ์—ด ๊ทœ๋ชจ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ์ž…์žฅ ์ฒ˜๋ฆฌ ๋น„์šฉ์ด ์„ ํ˜•์œผ๋กœ ๋Š˜์–ด๋‚ฉ๋‹ˆ๋‹ค.

Redis Sorted Set: ZADD O(log N), ZRANK O(log N)์œผ๋กœ ์ˆœ๋ฒˆ ์กฐํšŒ, ZREM O(log N)์œผ๋กœ ์‚ญ์ œ. score๋ฅผ ๋“ฑ๋ก ํƒ€์ž„์Šคํƒฌํ”„๋กœ ์„ค์ •ํ•˜๋ฉด ์„ ์ฐฉ์ˆœ ์ˆœ์„œ๊ฐ€ ์ž๋™์œผ๋กœ ์œ ์ง€๋˜๊ณ , ๋ชจ๋“  ์ฃผ์š” ์—ฐ์‚ฐ์ด O(log N)์ž…๋‹ˆ๋‹ค.

Sorted Set๋„ ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. List๋ณด๋‹ค ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋” ์”๋‹ˆ๋‹ค. ์ˆ˜๋ฐฑ๋งŒ ๋ช… ๊ทœ๋ชจ๋ผ๋ฉด skip list ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ฌด์‹œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ํ‹ฐ์ผ“ํŒ… ๋Œ€๊ธฐ์—ด์€ ์ด๋ฒคํŠธ ์˜คํ”ˆ ์งํ›„ ์ˆ˜๋ฐฑ~์ˆ˜์ฒœ ๋ช… ์ˆ˜์ค€์ด๊ณ , ์ž…์žฅ ์ฒ˜๋ฆฌ ํ›„ ๋น ๋ฅด๊ฒŒ ์ค„์–ด๋“œ๋Š” ํŠน์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ทœ๋ชจ์—์„œ๋Š” ์กฐํšŒ ์„ฑ๋Šฅ ์ด์ ์ด ๋ฉ”๋ชจ๋ฆฌ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ์ƒ์‡„ํ•ฉ๋‹ˆ๋‹ค.

Sorted Set๋งŒ์œผ๋กœ๋Š” ๋ถ€์กฑํ–ˆ์Šต๋‹ˆ๋‹ค

Sorted Set์„ ์„ ํƒํ•œ ๋’ค ์ƒˆ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค. ์ž…์žฅ๊ถŒ ๋ฐœ๊ธ‰์ด ZRANGE โ†’ ZREM โ†’ SETEX ์„ธ ๋‹จ๊ณ„์ธ๋ฐ, ์ด ์‚ฌ์ด์— ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๋‘ ๋ฒˆ ์‹คํ–‰๋˜๊ฑฐ๋‚˜ ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ฃฝ์œผ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ์š”.

  • ZREM์€ ๋๋Š”๋ฐ SETEX ์ „์— ํ”„๋กœ์„ธ์Šค๊ฐ€ ์ฃฝ์œผ๋ฉด โ†’ ๋Œ€๊ธฐ์—ด์—์„œ ๋น ์กŒ์ง€๋งŒ ์ž…์žฅ๊ถŒ์ด ์—†๋Š” ์‚ฌ์šฉ์ž ๋ฐœ์ƒ
  • ์Šค์ผ€์ค„๋Ÿฌ๊ฐ€ ๊ฑฐ์˜ ๋™์‹œ์— ๋‘ ๋ฒˆ ์‹คํ–‰๋˜๋ฉด โ†’ ๊ฐ™์€ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ž…์žฅ๊ถŒ ์ค‘๋ณต ๋ฐœ๊ธ‰

์›์ž์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์„ธ ๊ฐ€์ง€ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค.

Java ์ˆœ์ฐจ ํ˜ธ์ถœ: ๊ตฌํ˜„์ด ๊ฐ€์žฅ ๋‹จ์ˆœํ•˜์ง€๋งŒ, ์„ธ ๋ช…๋ น ์‚ฌ์ด์— ๋‹ค๋ฅธ ์š”์ฒญ์ด ๋ผ์–ด๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์›์ž์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.

Redis MULTI/EXEC with WATCH: WATCH๋กœ ๊ฐ์‹œํ•˜๋˜ ํ‚ค๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด EXEC๊ฐ€ ๋ฌดํšจํ™”๋˜๊ณ  ์žฌ์‹œ๋„๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋™์‹œ ์š”์ฒญ์ด ๋งŽ์€ ๊ตฌ๊ฐ„์—์„œ WATCH ์‹คํŒจ๊ฐ€ ์ž์ฃผ ๋ฐœ์ƒํ•˜๋ฉด ์žฌ์‹œ๋„ ๋ฃจํ”„๊ฐ€ ํ•„์š”ํ•ด์ง€๊ณ , "๋‹จ์ˆœํ•œ ์›์ž ์—ฐ์‚ฐ"์ด "์žฌ์‹œ๋„๋ฅผ ์ œ์–ดํ•˜๋Š” ๋ณต์žกํ•œ ์ฝ”๋“œ"๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

Lua Script: Redis ์„œ๋ฒ„ ๋‚ด๋ถ€์—์„œ ๋‹จ์ผ ์Šค๋ ˆ๋“œ๋กœ ์‹คํ–‰๋˜์–ด ์žฌ์‹œ๋„ ์—†์ด ์›์ž์„ฑ์ด ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค. ์Šคํฌ๋ฆฝํŠธ ์˜ค๋ฅ˜ ์‹œ ๋””๋ฒ„๊น…์ด ์–ด๋ ต๊ณ , ๋ณต์žกํ•œ ์Šคํฌ๋ฆฝํŠธ๋Š” ์„œ๋ฒ„ ์‘๋‹ต์„ ๋ธ”๋กœํ‚นํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์„ธ ์ค„์งœ๋ฆฌ ์—ฐ์‚ฐ์ด๋ผ๋ฉด ์ด ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๊ฐ€ ๋‚ฉ๋“๋ฉ๋‹ˆ๋‹ค.

-- POP_AND_GRANT: ๋Œ€๊ธฐ์—ด ์ƒ์œ„ N๋ช…์„ ๊บผ๋‚ด ์ž…์žฅ๊ถŒ ๋ฐœ๊ธ‰ (์›์ž์ )
local users = redis.call('ZRANGE', queueKey, 0, count - 1)
if (#users == 0) then return users end
redis.call('ZREM', queueKey, unpack(users))
for i = 1, #users do
  redis.call('SETEX', admittedPrefix .. users[i], ttl, '1')
end
return users

์ž…์žฅ๊ถŒ ์†Œ๋น„(EXISTS โ†’ TTL โ†’ DEL)๋„ ๊ฐ™์€ ์ด์œ ๋กœ Lua Script๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐ˜ํ™˜๊ฐ’์œผ๋กœ ๋‚จ์€ TTL์„ ๋ฐ›์•„, DB ์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ ์ž…์žฅ๊ถŒ์„ ์›๋ž˜ ์ž”์—ฌ TTL๋กœ ๋ณต์›ํ•˜๋Š” ๋ฐ๋„ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

-- CLAIM_ADMITTED: ์ž…์žฅ๊ถŒ ํ™•์ธ + ์›์ž์  ์†Œ๋น„, ๋‚จ์€ TTL ๋ฐ˜ํ™˜
if redis.call('EXISTS', key) == 0 then return -1 end
local ttl = redis.call('TTL', key)
redis.call('DEL', key)
return ttl

๊ฒฐ๊ณผ

๋Œ€๊ธฐ์—ด ์ˆœ๋ฒˆ ์กฐํšŒ๊ฐ€ DB polling์ด ์•„๋‹Œ Redis ZRANK ๋‹จ์ผ ๋ช…๋ น์œผ๋กœ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ์ž…์žฅ๊ถŒ ๋ฐœ๊ธ‰๊ณผ ์†Œ๋น„๋Š” Lua Script๋กœ ์›์ž์„ฑ์„ ๋ณด์žฅํ•ด ์ค‘๋ณต ๋ฐœ๊ธ‰๊ณผ TOCTOU ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.

DB Lock โ€” ๋‚™๊ด€์  ๋ฝ๊ณผ ๋น„๊ด€์  ๋ฝ ์ค‘ ์–ด๋А ์ชฝ์ด ํ‹ฐ์ผ“ํŒ…์— ๋งž๋Š”๊ฐ€

์ฒ˜์Œ์—๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค

Optimistic Lock๊ณผ Pessimistic Lock ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฅด๋ฉด ํ•ด๊ฒฐ๋  ๊ฑฐ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ถฉ๋Œ์ด ๋งŽ์œผ๋ฉด ๋น„๊ด€์  ๋ฝ, ๋“œ๋ฌผ๋ฉด ๋‚™๊ด€์  ๋ฝ์ด๋ผ๋Š” ํ†ต๋…์—์„œ ์‹œ์ž‘ํ–ˆ๊ณ , "๋‚™๊ด€์  ๋ฝ์ด ์ฒ˜๋ฆฌ๋Ÿ‰์ด ๋†’๋‹ค"๋Š” ์„ค๋ช…์„ ๋ณด๊ณ  ์ฒ˜์Œ์—” ๋‚™๊ด€์  ๋ฝ์„ ๋จผ์ € ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‚™๊ด€์  ๋ฝ์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค

๋‚™๊ด€์  ๋ฝ์€ ์ถฉ๋Œ์ด ๋“œ๋ฌธ ํ™˜๊ฒฝ์—์„œ ๋น„๊ด€์  ๋ฝ๋ณด๋‹ค ์ฒ˜๋ฆฌ๋Ÿ‰์ด ๋†’์Šต๋‹ˆ๋‹ค. DB ์ž์›์„ ์ปค๋ฐ‹ ์‹œ์ ์—๋งŒ ์žก๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด ์ด์ ์€ "์ถฉ๋Œ์ด ๋“œ๋ฌธ ํ™˜๊ฒฝ"์ด๋ผ๋Š” ์ „์ œ์—์„œ๋งŒ ์„ฑ๋ฆฝํ•ฉ๋‹ˆ๋‹ค.

ํ‹ฐ์ผ“ํŒ… ์˜คํ”ˆ ์งํ›„๋Š” "๊ฑฐ์˜ ๋ชจ๋“  ์š”์ฒญ์ด ๊ฐ™์€ ์ขŒ์„ row๋ฅผ ์ฝ๊ณ  UPDATEํ•˜๋Š” ๊ตฌ๊ฐ„"์ž…๋‹ˆ๋‹ค. ์ถฉ๋Œ์€ ์˜ˆ์™ธ๊ฐ€ ์•„๋‹ˆ๋ผ ์ •์ƒ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.

[๋‚™๊ด€์  ๋ฝ ์‹œ๋‚˜๋ฆฌ์˜ค]
50๊ฐœ ์Šค๋ ˆ๋“œ ๋™์‹œ ์š”์ฒญ โ†’ 1๊ฐœ๋งŒ ์ปค๋ฐ‹ ์„ฑ๊ณต โ†’ 49๊ฐœ OptimisticLockException
โ†’ ์žฌ์‹œ๋„ โ†’ ๋˜ ์ถฉ๋Œ โ†’ ์žฌ์‹œ๋„ ๋ฌดํ•œ ๋ฐ˜๋ณต
โ†’ ์‘๋‹ต ์ง€์—ฐ ๋ˆ„์ , DB ์ปค๋„ฅ์…˜ ์ ์œ  ์ฆ๊ฐ€

์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ œํ•œํ•ด๋„, ์‹คํŒจํ•œ ์š”์ฒญ๋“ค์ด ์—๋Ÿฌ๋กœ ๋๋‚˜๋Š” ๊ฑด ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ๋‚™๊ด€์  ๋ฝ์˜ ์žฅ์ ์€ ์ด ๊ตฌ๊ฐ„์—์„œ ๋ฐœํ˜„๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋น„๊ด€์  ๋ฝ์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

๋น„๊ด€์  ๋ฝ์€ ๋ฝ์„ ์žก์€ ํŠธ๋žœ์žญ์…˜์ด ์™„๋ฃŒํ•  ๋•Œ๊นŒ์ง€ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์ด ๋Œ€๊ธฐํ•ฉ๋‹ˆ๋‹ค. ์žฌ์‹œ๋„ ์—†์ด ์ˆœ์ฐจ ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค. ํฌ๋ฆฌํ‹ฐ์ปฌ ์„น์…˜์ด ๊ธธ๋ฉด ๋Œ€๊ธฐ ์‹œ๊ฐ„์ด ๋ˆ„์ ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ์ž”์—ฌ์„ ์ฐจ๊ฐ์€ ๋‹จ์ˆœ UPDATE ์—ฐ์‚ฐ์ž…๋‹ˆ๋‹ค. ๋ฝ ์œ ์ง€ ์‹œ๊ฐ„์ด ์งง์•„ ๋Œ€๊ธฐ ๋น„์šฉ์ด ํฌ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋”ํ•ด์„œ, Redis ๋Œ€๊ธฐ์—ด์ด ์ด๋ฏธ ์ง„์ž… ์ธ์›์„ 50๋ช… ๋‹จ์œ„๋กœ ๋‚˜๋ˆ  ์ž…์žฅ์‹œํ‚ค๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ ๋™์‹œ์— ์ขŒ์„์„ ์ฐจ๊ฐํ•˜๋Š” ์ธ์›๋„ ์ œํ•œ๋ฉ๋‹ˆ๋‹ค. ๋ฝ ๊ฒฝํ•ฉ ์ž์ฒด๊ฐ€ ํฌ์ง€ ์•Š๋‹ค๋Š” ์ ๋„ ๋น„๊ด€์  ๋ฝ์„ ์„ ํƒํ•˜๋Š” ๋ฐ ์œ ๋ฆฌํ•˜๊ฒŒ ์ž‘์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ

์ดˆ๊ณผ ์˜ˆ๋งค๋ฅผ ๋ง‰์•˜์Šต๋‹ˆ๋‹ค. ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ์—์„œ "์ •ํ•ฉ์„ฑ์€ ํ™•๋ณด๋์ง€๋งŒ ๊ณ ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ๋Ÿ‰์€ ๋ถ€์กฑํ•œ ๊ตฌ์กฐ"๋ผ๋Š” ๋‹ค์Œ ๋ฌธ์ œ๋ฅผ ํ™•์ธํ–ˆ๊ณ , ์ดํ›„ Redis ์ขŒ์„ ์ฐจ๊ฐ์œผ๋กœ ๊ฐœ์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

Redis ์ขŒ์„ ์ฐจ๊ฐ + ํŒŒ์ƒ bookedCount โ€” deadlock ์›์ธ์ด ์˜ˆ์ƒ ๋ฐ–์— ์žˆ์—ˆ๋‹ค

์ฒ˜์Œ์—๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค

DB ๋ฝ ๊ตฌ์กฐ์—์„œ ๋ฒ—์–ด๋‚˜๋ฉด ์‘๋‹ต ์‹œ๊ฐ„์ด ์ข‹์•„์งˆ ๊ฒƒ ๊ฐ™์•„, ์ขŒ์„ ์ œ์–ด๋ฅผ Redis decrementSeat๋กœ ์˜ฎ๊ฒผ์Šต๋‹ˆ๋‹ค. ์ด๋•Œ๋„ ์˜ˆ์•ฝ ์„ฑ๊ณต ์ˆ˜๋ฅผ ๋ฐ”๋กœ ํ™•์ธํ•˜๊ณ  ์‹ถ์–ด์„œ concert.booked_count = booked_count + 1 ์—…๋ฐ์ดํŠธ๋Š” ์œ ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์œ„์น˜์—์„œ deadlock์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค

JMeter ๋™์‹œ ์˜ˆ์•ฝ ํ…Œ์ŠคํŠธ์—์„œ ์ผ๋ถ€ ์š”์ฒญ์ด 500์œผ๋กœ ์‹คํŒจํ–ˆ๊ณ , ๋กœ๊ทธ์— ์•„๋ž˜ ์˜ˆ์™ธ๊ฐ€ ๋‚จ์•˜์Šต๋‹ˆ๋‹ค.

MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

Redis๋กœ ์ขŒ์„ ์ œ์–ด๋ฅผ ์˜ฎ๊ฒผ๋Š”๋ฐ ์™œ DB deadlock์ด ์ƒ๊ธธ๊นŒ์š”. BookingService.book() ํ๋ฆ„์„ ๋”ฐ๋ผ๊ฐ€๋ดค์Šต๋‹ˆ๋‹ค. booking INSERT ์งํ›„ incrementBookedCount()๋ฅผ ํ˜ธ์ถœํ•ด concert.booked_count๋ฅผ ์ฆ‰์‹œ UPDATEํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ขŒ์„ ์ ์œ ๋Š” Redis๊ฐ€ ์ฒ˜๋ฆฌํ–ˆ์ง€๋งŒ, ๋ชจ๋“  ์„ฑ๊ณต ์š”์ฒญ์ด ์—ฌ์ „ํžˆ ๊ฐ™์€ concert ํ–‰์„ UPDATEํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. booking INSERT๊ฐ€ ์žก๋Š” ์ธ๋ฑ์Šค/์™ธ๋ž˜ํ‚ค ๋ฝ๊ณผ concert UPDATE ๋ฝ์ด ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ์„ž์ด๋ฉด์„œ deadlock์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

deadlock ์ œ๊ฑฐ๋ฅผ ์œ„ํ•œ ์„ ํƒ์ง€๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

๋ฐฉ๋ฒ• 1: concert UPDATE๋ฅผ ๋ณ„๋„ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ถ„๋ฆฌ @Transactional(REQUIRES_NEW)๋กœ booked_count๋งŒ ๋”ฐ๋กœ UPDATEํ•˜๋ฉด ๋‘ ๋ฝ์ด ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์—์„œ ์ถฉ๋Œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. deadlock์€ ํ•ด์†Œ๋˜์ง€๋งŒ ๋ชจ๋“  ์š”์ฒญ์ด ๊ฐ™์€ concert ํ–‰์„ UPDATEํ•˜๋Š” hot row ๋ฌธ์ œ๋Š” ๊ทธ๋Œ€๋กœ ๋‚จ์Šต๋‹ˆ๋‹ค. ์‘๋‹ต ์‹œ๊ฐ„ ๋ณ‘๋ชฉ์ด ํ˜•ํƒœ๋งŒ ๋ฐ”๋€๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 2: booked_count๋ฅผ ๋น„๋™๊ธฐ๋กœ ๊ฐฑ์‹  @Async๋‚˜ ์ด๋ฒคํŠธ๋กœ booked_count ๊ฐฑ์‹ ์„ ๋’ค๋กœ ๋ฏธ๋ฃจ๋ฉด ๋™๊ธฐ ๋ณ‘๋ชฉ์„ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ฐฑ์‹ ์ด ์ง€์—ฐ๋˜๋Š” ๊ตฌ๊ฐ„์— ์กฐํšŒํ•˜๋ฉด staleํ•œ ๊ฐ’์ด ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค. ์ •ํ•ฉ์„ฑ๋ณด๋‹ค ์กฐํšŒ ์„ฑ๋Šฅ์„ ์šฐ์„ ํ•˜๋Š” ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 3: bookedCount๋ฅผ ํŒŒ์ƒ๊ฐ’์œผ๋กœ ์ „ํ™˜ booked_count UPDATE๋ฅผ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ , ์กฐํšŒ ์‹œ totalSeats - Redis remainingSeats๋กœ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰์‹œ ๊ฐฑ์‹ ํ•  ํ•„์š”๊ฐ€ ์—†์–ด hot row ์ž์ฒด๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. Redis๊ฐ€ ๋‚ด๋ ค๊ฐ€๋ฉด ์‹ค์‹œ๊ฐ„ ์ž”์—ฌ์„ ๊ฐ’์„ ๊ตฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ fallback์œผ๋กœ DB์— ์ €์žฅ๋œ ๋งˆ์ง€๋ง‰ bookedCount๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ๊ฒฝ๊ณ  ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ์™„ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค. Redis๋ฅผ ์‹ค์‹œ๊ฐ„ ์ œ์–ด ๋„๊ตฌ๋กœ ์“ฐ๋Š” ์ด์ƒ Redis ๊ฐ€์šฉ์„ฑ์— ๋Œ€ํ•œ ์˜์กด์ด ์ƒ๊ธฐ๋Š” ๊ฑด ํ”ผํ•  ์ˆ˜ ์—†๊ณ , fallback์„ ๋ช…์‹œ์ ์œผ๋กœ ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์ด ๋” ๋‚˜์€ ์ ‘๊ทผ์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 1์€ hot row๋ฅผ ๋‚จ๊ฒจ๋‘๊ณ , ๋ฐฉ๋ฒ• 2๋Š” stale ์กฐํšŒ๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ๋ฒ• 3์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ

concert ํ–‰์„ ๋งค ์š”์ฒญ๋งˆ๋‹ค ๊ฐฑ์‹ ํ•˜๋˜ hot row update๊ฐ€ ์‚ฌ๋ผ์กŒ๊ณ , ๋™์ผ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ deadlock ์—†์ด ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

3๊ณ„์ธต ๋ฉฑ๋“ฑ์„ฑ โ€” "unique key ํ•˜๋‚˜๋ฉด ๋˜์ง€ ์•Š๋‚˜"์—์„œ ์‹œ์ž‘ํ•œ ๊ณ ๋ฏผ

์ฒ˜์Œ์—๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค

DB์— uk_payment_idempotency_key unique constraint ํ•˜๋‚˜๋ฅผ ๋‘๋ฉด ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ค‘๋ณต ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด INSERT๊ฐ€ ์‹คํŒจํ•˜๊ณ  ํ˜ธ์ถœ์ž๊ฐ€ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋๋‹ค๋Š” ๊ฑธ ์•Œ ์ˆ˜ ์žˆ์œผ๋‹ˆ๊นŒ์š”.

experiments/e3์—์„œ ์„ธ ์ „๋žต์„ ๊ฐ๊ฐ ์‹คํ—˜ํ•˜๋ฉด์„œ ์ด ์ƒ๊ฐ์ด ํ‹€๋ ธ๋‹ค๋Š” ๊ฑธ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

์„ธ ์ „๋žต์ด ์„œ๋กœ ๋‹ค๋ฅธ ์ƒํ™ฉ์—์„œ ๋ฌด๋„ˆ์กŒ์Šต๋‹ˆ๋‹ค

์ „๋žต A โ€” DB unique key๋งŒ ์‚ฌ์šฉ

์ค‘๋ณต ์š”์ฒญ์ด ๊ฑฐ์˜ ๋™์‹œ์— ๋“ค์–ด์˜ค๋ฉด ๋‘ ์š”์ฒญ ๋ชจ๋‘ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์ดˆ๋ฐ˜์„ ํ†ต๊ณผํ•ฉ๋‹ˆ๋‹ค. INSERT ์‹œ์ ์—์•ผ ์ถฉ๋Œ์ด ๋“œ๋Ÿฌ๋‚˜๋Š”๋ฐ, ๊ทธ ์ „์— ์ด๋ฏธ ๊ฒฐ์ œ ๊ด€๋ จ ๋กœ์ง์ด ์‹คํ–‰๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ์ถฉ๋Œ์„ ์•ž๋‹จ์—์„œ ์ฐจ๋‹จํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ ์„ฑ๊ณตํ•œ ๊ฒฐ์ œ๋ฅผ ์‚ฌ์šฉ์ž๊ฐ€ ์žฌ์‹œ๋„ํ•˜๋Š” ๊ฒฝ์šฐ, unique constraint ์˜ˆ์™ธ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ์—์„œ๋Š” "๊ฒฐ์ œ ์‹คํŒจ"์ฒ˜๋Ÿผ ๋ณด์ž…๋‹ˆ๋‹ค. ๋ฉฑ๋“ฑ์„ฑ์˜ ๋ณธ๋ž˜ ์˜๋ฏธ๋Š” "๊ฐ™์€ ์š”์ฒญ์ด ๋“ค์–ด์™”์„ ๋•Œ ๊ธฐ์กด ๊ฒฐ๊ณผ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒƒ"์ธ๋ฐ, unique key๋งŒ์œผ๋กœ๋Š” ๊ทธ ๋ฐ˜ํ™˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

์ „๋žต B โ€” SELECT EXISTS โ†’ INSERT

์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์žฌ์‹œ๋„๋Š” ๋น ๋ฅด๊ฒŒ ๊ฑธ๋Ÿฌ๋‚ผ ์ˆ˜ ์žˆ๊ณ , ๊ธฐ์กด ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ธฐ๋„ ์‰ฝ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ SELECT์™€ INSERT ์‚ฌ์ด์— ๋™์‹œ ์š”์ฒญ์ด ๋ผ์–ด๋“ค๋ฉด ๋‘˜ ๋‹ค SELECT์—์„œ "์—†์Œ"์„ ๋ณด๊ณ  INSERT๋กœ ์ง„์ž…ํ•ฉ๋‹ˆ๋‹ค. ๋™์‹œ ์ค‘๋ณต ํด๋ฆญ์— ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.

์ „๋žต C โ€” Redis SETNX๋งŒ ์‚ฌ์šฉ

๋™์‹œ ์š”์ฒญ ์ฐจ๋‹จ์—๋Š” ํšจ๊ณผ์ ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Redis๊ฐ€ ์žฅ์•  ์ƒํƒœ๋ฉด ๋ฐฉ์–ด์„ ์ด ํ†ต์งธ๋กœ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

์„ธ ๊ณ„์ธต์ด ์„œ๋กœ ๋‹ค๋ฅธ ์‹คํŒจ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋‹ด๋‹นํ•˜๋„๋ก ์กฐํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค

์‹คํ—˜์„ ํ†ตํ•ด ๊ฐ ์ „๋žต์ด ๋‹ด๋‹นํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด ๋‹ค๋ฅด๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ์„ธ ๊ณ„์ธต์„ ์กฐํ•ฉํ•˜๋ฉด ๊ฐ๊ฐ์˜ ์ทจ์•ฝ์ ์„ ์„œ๋กœ ๋ณด์™„ํ•ฉ๋‹ˆ๋‹ค.

๊ณ„์ธต ์—ญํ•  ๋Œ€์‘ ์ƒํ™ฉ
DB ์‚ฌ์ „ ์กฐํšŒ ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์š”์ฒญ์ด๋ฉด ๊ธฐ์กด ๊ฒฐ๊ณผ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ ์ •์ƒ ์žฌ์‹œ๋„
Redis SETNX (TTL 30s) ๋™์ผ ํ‚ค์˜ ๋™์‹œ ์š”์ฒญ์„ ์•ž๋‹จ์—์„œ ์ฐจ๋‹จ ๋™์‹œ ์ค‘๋ณต ํด๋ฆญ
DB unique key Redis ์žฅ์•  ์‹œ ์ตœํ›„ ๋ฐฉ์–ด์„  Redis ์žฅ์• 

3๊ณ„์ธต์€ ๊ตฌํ˜„ ๋ณต์žก๋„๋ฅผ ๋†’์ž…๋‹ˆ๋‹ค. ๊ณ„์ธต์ด ๋งŽ์„์ˆ˜๋ก ๋ฒ„๊ทธ๊ฐ€ ์ˆจ์„ ์ž๋ฆฌ๋„ ๋งŽ์•„์ง‘๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐ ๊ณ„์ธต์ด ์„œ๋กœ ๋‹ค๋ฅธ ์‹คํŒจ ๋ชจ๋“œ๋ฅผ ๋‹ด๋‹นํ•˜๊ณ , ํ•˜๋‚˜๊ฐ€ ๋™์ž‘ํ•˜์ง€ ์•Š์•„๋„ ๋‚˜๋จธ์ง€๊ฐ€ ๋ณด์™„ํ•˜๋Š” ๊ตฌ์กฐ์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฒฐ์ œ๋ผ๋Š” ๋„๋ฉ”์ธ์—์„œ๋Š” ์ด ๋ณต์žก๋„๊ฐ€ ๋‚ฉ๋“๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ

PaymentServiceFailRateTest โ€” ๋™์ผ idempotencyKey๋กœ ์žฌ์š”์ฒญ ์‹œ ๊ธฐ์กด ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ˜ํ™˜๋จ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. Grafana ๋Œ€์‹œ๋ณด๋“œ์—์„œ MOCK/CACHED ๋ถ„ํฌ๋กœ ์ค‘๋ณต ๊ฒฐ์ œ๊ฐ€ ์‹ค์ œ๋กœ ์ฐจ๋‹จ๋˜๊ณ  ๋™์ผ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ˜ํ™˜๋์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

Transactional Outbox โ€” Kafka๊ฐ€ ์•„๋‹Œ Outbox๋ฅผ ์„ ํƒํ•œ ์ด์œ 

์ฒ˜์Œ์—๋Š” ์ด๋ ‡๊ฒŒ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค

๊ฒฐ์ œ ์‹คํŒจ ์‹œ ์ฆ‰์‹œ ๋ณด์ƒ ๋กœ์ง์„ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹(Fire-and-forget)์œผ๋กœ ๋จผ์ € ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. paymentService.fail() ์ดํ›„ ๋ฐ”๋กœ bookingService.cancel()์„ ํ˜ธ์ถœํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค. ์ง๊ด€์ ์ด๊ณ  ๋‹จ์ˆœํ•ฉ๋‹ˆ๋‹ค.

์‹คํŒจ ์ฃผ์ž… ์‹คํ—˜์—์„œ ๋ฌธ์ œ๋ฅผ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค

experiments/e4์—์„œ N๋ฒˆ์งธ ํ˜ธ์ถœ๋งˆ๋‹ค ์˜ˆ์™ธ๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์‹คํŒจ์œจ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ผ์ • ๋น„์œจ์˜ ๊ฒฐ์ œ ์‹คํŒจ๋ฅผ ์ฃผ์ž…ํ•˜์ž, ๋ณด์ƒ ํ˜ธ์ถœ ์ž์ฒด๋„ ๊ฐ™์€ ๋น„์œจ๋กœ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ์ œ๋Š” ์‹คํŒจํ–ˆ๋Š”๋ฐ ์ขŒ์„ ๋ณต์›์€ ์•ˆ ๋œ ์ผ€์ด์Šค๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทผ๋ณธ ์›์ธ์€ "๊ฒฐ์ œ ์‹คํŒจ"์™€ "๋ณด์ƒ ๋Œ€์ƒ ๊ธฐ๋ก"์ด ๋ณ„๊ฐœ์˜ ์ž‘์—…์ด๋ผ๋Š” ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๊ฒฐ์ œ ์‹คํŒจ๋Š” DB์— ๋‚จ๋Š”๋ฐ, ๋ณด์ƒ ๋Œ€์ƒ์€ ๋ฉ”๋ชจ๋ฆฌ์™€ ๋น„๋™๊ธฐ ํ˜ธ์ถœ ํ๋ฆ„ ์•ˆ์—๋งŒ ์žˆ์Šต๋‹ˆ๋‹ค. ์•ฑ์ด ์žฌ์‹œ์ž‘๋˜๋ฉด ๋ณด์ƒ ๋Œ€์ƒ ์ •๋ณด๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

๋Œ€์•ˆ์„ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

Fire-and-forget: ๋‹จ์ˆœํ•˜์ง€๋งŒ ๋ณด์ƒ ๋Œ€์ƒ์ด ๋ฉ”๋ชจ๋ฆฌ์—๋งŒ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์•ฑ ์žฌ์‹œ์ž‘, ๋„คํŠธ์›Œํฌ ์ˆœ๋‹จ, ์˜ˆ์™ธ ๋ฐœ์ƒ โ€” ๋ณด์ƒ ํ˜ธ์ถœ์ด ๋Š๊ธธ ์ˆ˜ ์žˆ๋Š” ์ง€์ ์ด ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค.

@Async + ์žฌ์‹œ๋„: Fire-and-forget๋ณด๋‹ค๋Š” ๋‚ซ์ง€๋งŒ ์žฌ์‹œ์ž‘ ์‹œ ๋Œ€๊ธฐ ์ค‘์ธ ์ž‘์—…์ด ์‚ฌ๋ผ์ง€๋Š” ๊ฑด ๋™์ผํ•ฉ๋‹ˆ๋‹ค. ์˜์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.

Kafka / ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค: ๋ฐœํ–‰-๊ตฌ๋… ๊ตฌ์กฐ๋กœ ๋ณด์ƒ ์ฒ˜๋ฆฌ๋ฅผ ์™„์ „ํžˆ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๊ณ , ์žฌ์ฒ˜๋ฆฌยท์ˆœ์„œ ๋ณด์žฅยทํ™•์žฅ์„ฑ ๋ฉด์—์„œ ๊ฐ€์žฅ ๊ฐ•๋ ฅํ•œ ์„ ํƒ์ง€์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Kafka ํด๋Ÿฌ์Šคํ„ฐ ์šด์˜, ํ† ํ”ฝ ๊ด€๋ฆฌ, Consumer ์„ค๊ณ„๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค. ์ง€๊ธˆ ํ•ด๊ฒฐํ•˜๋ ค๋Š” ๋ฌธ์ œ๋Š” "๊ฒฐ์ œ ์‹คํŒจ ํ›„ ๋ณด์ƒ์ด ์œ ์‹ค๋˜์ง€ ์•Š๊ฒŒ"์ž…๋‹ˆ๋‹ค. ์ด ๋ฒ”์œ„์—์„œ๋Š” ์ธํ”„๋ผ ๋ณต์žก๋„๊ฐ€ ๋„ˆ๋ฌด ๋†’์Šต๋‹ˆ๋‹ค.

Transactional Outbox: "๊ฒฐ์ œ ์‹คํŒจ"์™€ "๋ณด์ƒํ•ด์•ผ ํ•œ๋‹ค๋Š” ์‚ฌ์‹ค"์„ ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. ๊ฒฐ์ œ ์‹คํŒจ๊ฐ€ DB์— ๋‚จ์•„์žˆ์œผ๋ฉด ๋ณด์ƒ ๋Œ€์ƒ Outbox๋„ ๋ฐ˜๋“œ์‹œ ๋‚จ์•„์žˆ์Šต๋‹ˆ๋‹ค. ๋‹จ์ ์€ DB ํด๋ง ์˜ค๋ฒ„ํ—ค๋“œ(10์ดˆ๋งˆ๋‹ค)๊ฐ€ ์ƒ๊ธฐ๊ณ , ๋ณด์ƒ์ด ์ฆ‰์‹œ๊ฐ€ ์•„๋‹Œ ์ตœ๋Œ€ 10์ดˆ ์ง€์—ฐ๋œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ด ์ง€์—ฐ์ด ํ—ˆ์šฉ๋˜๋Š” ๋ฒ”์œ„์ด๊ณ , ๋‚˜์ค‘์— Kafka๋กœ ์ „ํ™˜ํ•˜๋”๋ผ๋„ Outbox ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Fire-and-forget:
  ๊ฒฐ์ œ ์‹คํŒจ (DB์— ๊ธฐ๋ก) โ†’ ๋ณด์ƒ ํ˜ธ์ถœ โ†’ ์•ฑ ์ข…๋ฃŒ โ†’ ์ด๋ฒคํŠธ ์œ ์‹ค
  ์žฌ์‹œ์ž‘ ํ›„: ๊ฒฐ์ œ ์‹คํŒจ ๊ธฐ๋ก์€ ์žˆ์ง€๋งŒ, ๋ณด์ƒํ•ด์•ผ ํ•œ๋‹ค๋Š” ์ •๋ณด๊ฐ€ ์—†์Œ

Outbox ํŒจํ„ด:
  [๊ฐ™์€ ํŠธ๋žœ์žญ์…˜] Payment.fail() + INSERT Outbox(PENDING)
  ์•ฑ ์žฌ์‹œ์ž‘ ํ›„์—๋„ Outbox๊ฐ€ ๋‚จ์•„ Scheduler๊ฐ€ ์žฌ์ฒ˜๋ฆฌ

๊ฒฐ๊ณผ

๋™์ผํ•œ ์‹คํŒจ์œจ์„ ์ฃผ์ž…ํ–ˆ์„ ๋•Œ, Outbox ๋ฐฉ์‹์€ ์Šค์ผ€์ค„๋Ÿฌ ์žฌ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ์ตœ์ข… 100% ๋ณด์ƒ ์„ฑ๊ณต์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ฐ Outbox๋Š” REQUIRES_NEW ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋…๋ฆฝ ์ฒ˜๋ฆฌ๋˜๋ฉฐ, ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„ ํ›„ FAILED๋กœ ์ „ํ™˜๋˜์–ด Grafana ์•Œ๋ฆผ์„ ๋ฐœ์†กํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“ˆ ์„ฑ๋Šฅ ์ธก์ • ๊ฒฐ๊ณผ

๋™์‹œ์„ฑ ์ œ์–ด ๊ตฌ์กฐ๋ฅผ ๋จผ์ € ์ •๋ฆฌํ•˜๊ณ , ๊ทธ ๋‹ค์Œ ์„œ๋ฒ„๋ฅผ ๋Š˜๋ฆฌ๋Š” ์ˆœ์„œ๋กœ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ๊ณ„ ํ™˜๊ฒฝ ํ•ต์‹ฌ ๊ฒฐ๊ณผ
Phase 1 ๋กœ์ปฌ ๋‹จ์ผ ์„œ๋ฒ„ DB Lock โ†’ Redis ๊ฐœ์„ . ์‘๋‹ต์‹œ๊ฐ„ 62% โ†“, deadlock ์ œ๊ฑฐ
Phase 2 AWS EC2 2๋Œ€ + Nginx ์ˆ˜ํ‰ ํ™•์žฅ์œผ๋กœ ์‘๋‹ต์‹œ๊ฐ„ 9๋ฐฐ ๊ฐœ์„  (13.8s โ†’ 1.5s)
Phase 3 GCP e2-medium 2~3๋Œ€ + GCP LB ๋Œ€๊ธฐ์—ด ์ง„์ž… 10,000๋ช… 0% ์—๋Ÿฌ. ์˜ˆ๋งค ๋™์‹œ์„ฑ Redis ์บ์‹œ + pool ํŠœ๋‹์œผ๋กœ 74.1 TPS. 1,000 req/s polling ์•ˆ์ •ํ™”. Adaptive Polling์œผ๋กœ 2,000 โ†’ 1,100 req/s
Phase 1 โ€” ๋กœ์ปฌ ๋‹จ์ผ ์„œ๋ฒ„: DB Lock โ†’ Redis ๊ตฌ์กฐ ๊ฐœ์„  ๊ณผ์ •

1. ๊ฐœ์„  ์ „: DB Lock ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ

์ฒ˜์Œ ๊ตฌ์กฐ์—์„œ๋Š” ์ดˆ๊ณผ ์˜ˆ๋งค๋ฅผ ๋ง‰๋Š” ๋ฐ ์ง‘์ค‘ํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ ์ •ํ•ฉ์„ฑ์€ ์ž˜ ์ง€์ผœ์กŒ์ง€๋งŒ, ์ขŒ์„ ์ˆ˜์™€ ๋™์‹œ ์š”์ฒญ ์ˆ˜๊ฐ€ ๋Š˜์ˆ˜๋ก ์ง๋ ฌํ™” ๋น„์šฉ์ด ๋น ๋ฅด๊ฒŒ ์ปค์กŒ์Šต๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ p95 ์ฒ˜๋ฆฌ๋Ÿ‰ ๊ฒฐ๊ณผ ํ•ด์„
100์„ / 100๋ช… 162ms 257ms 334.4/sec ๋‚ฎ์€ ์ง€์—ฐ ์‹œ๊ฐ„์œผ๋กœ ์•ˆ์ • ์ฒ˜๋ฆฌ
100์„ / 300๋ช… 1310ms 1609ms 133.9/sec ์‹คํŒจ์œจ 66.67%, ์ขŒ์„ ์ดˆ๊ณผ ์š”์ฒญ์€ ์ •์ƒ์ ์œผ๋กœ ๋งˆ๊ฐ ์ฒ˜๋ฆฌ
300์„ / 300๋ช… 1146ms 1941ms 143.8/sec ์ „๋ถ€ ์„ฑ๊ณต, ๋‹ค๋งŒ ์‘๋‹ต ์ง€์—ฐ์ด ์ปค์ง
500์„ / 500๋ช… 3778ms 6822ms 69.8/sec ์ „๋ถ€ ์„ฑ๊ณต, ์ •ํ•ฉ์„ฑ์€ ์œ ์ง€๋์ง€๋งŒ ์ง๋ ฌ ์ฒ˜๋ฆฌ ํ•œ๊ณ„ ํ™•์ธ

2. ์ปค๋„ฅ์…˜ ํ’€ ์ฆ๊ฐ€ ์‹คํ—˜ (Hikari pool 10 โ†’ 50) โ€” ๋” ๋А๋ ค์กŒ๋‹ค

DB Lock ๊ตฌ์กฐ์—์„œ ์‘๋‹ต ์‹œ๊ฐ„์ด ๊ธธ์–ด์ง€๋Š” ๊ฑธ ๋ณด๊ณ , ์ฒ˜์Œ ๋“  ์ƒ๊ฐ์€ "์ปค๋„ฅ์…˜ ํ’€์ด ๋ถ€์กฑํ•ด์„œ ์š”์ฒญ์ด ๋Œ€๊ธฐํ•˜๋Š” ๊ฑด ์•„๋‹๊นŒ?"์˜€์Šต๋‹ˆ๋‹ค. Hikari pool ์‚ฌ์ด์ฆˆ๋ฅผ 10์—์„œ 50์œผ๋กœ ๋Š˜๋ฆฌ๋ฉด ๋” ๋งŽ์€ ์š”์ฒญ์ด ๋™์‹œ์— ์ฒ˜๋ฆฌ๋  ๊ฒƒ์ด๋ผ๊ณ  ์˜ˆ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ๋Š” ์˜ˆ์ƒ๊ณผ ๋ฐ˜๋Œ€์˜€์Šต๋‹ˆ๋‹ค.

์ง€ํ‘œ pool=10 pool=50
ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ 3778ms 5656ms
p95 6822ms 9650ms
TPS 69.8/sec 49.8/sec
์—๋Ÿฌ์œจ 0% 0%

์ปค๋„ฅ์…˜ ํ’€์„ 5๋ฐฐ ๋Š˜๋ ธ๋”๋‹ˆ ์‘๋‹ต ์‹œ๊ฐ„์ด ์•ฝ 50% ๋” ๋Š˜์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค.

์™œ ๋” ๋‚˜๋น ์กŒ๋Š”์ง€ InnoDB lock ํ†ต๊ณ„๋กœ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ „ํ›„๋กœ MySQL InnoDB row lock ๋ˆ„์  ์ง€ํ‘œ๋ฅผ ์ˆ˜์ง‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ง€ํ‘œ pool=10 ์ดํ›„ pool=50 ์ดํ›„ ์ฆ๊ฐ€๋Ÿ‰
Innodb_row_lock_waits 5,586 6,085 +499
Innodb_row_lock_time (ms) 201,222 594,108 +392,886ms
ํ‰๊ท  ๋ฝ ๋Œ€๊ธฐ ์‹œ๊ฐ„ โ€” โ€” 392,886 / 499 โ‰ˆ 787ms

pool=50 ์กฐ๊ฑด์—์„œ row lock ํ•œ ๋ฒˆ ๋Œ€๊ธฐ์— ํ‰๊ท  787ms๊ฐ€ ์†Œ์š”๋์Šต๋‹ˆ๋‹ค.

์›์ธ ํ•ด์„: ์ปค๋„ฅ์…˜ ํ’€์„ ๋Š˜๋ฆฌ๋ฉด ๋” ๋งŽ์€ ํŠธ๋žœ์žญ์…˜์ด ๋™์‹œ์— DB์— ์ง„์ž…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ด ๊ตฌ์กฐ์—์„œ๋Š” ๋ชจ๋“  ์„ฑ๊ณต ์š”์ฒญ์ด ๊ฐ™์€ concert ํ–‰์˜ Pessimistic Lock์„ ์žก์œผ๋ ค ํ•ฉ๋‹ˆ๋‹ค. ๋™์‹œ์— ์ง„์ž…ํ•˜๋Š” ํŠธ๋žœ์žญ์…˜์ด ๋งŽ์•„์งˆ์ˆ˜๋ก ๊ฐ™์€ row๋ฅผ ๋‘๊ณ  ๊ฒฝํ•ฉํ•˜๋Š” ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚˜๊ณ , lock ๋Œ€๊ธฐ ์ค„์ด ๊ธธ์–ด์ง‘๋‹ˆ๋‹ค. ์ปค๋„ฅ์…˜ ํ’€ ๋ถ€์กฑ์ด ๋ณ‘๋ชฉ์ด ์•„๋‹ˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋” ๋งŽ์€ ์š”์ฒญ์„ ๋™์‹œ์— DB๋กœ ๋ฐ€์–ด ๋„ฃ์„์ˆ˜๋ก lock ๊ฒฝํ•ฉ์ด ์‹ฌํ•ด์ง€๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.

์ด ์‹คํ—˜์„ ํ†ตํ•ด "์„œ๋ฒ„ ์ž์› ์ฆ์„ค๋ณด๋‹ค ๊ตฌ์กฐ ๋ณ€๊ฒฝ์ด ๋จผ์ €"๋ผ๋Š” ํŒ๋‹จ์˜ ๊ทผ๊ฑฐ๋ฅผ ์ง์ ‘ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

3. 1์ฐจ ๊ฐœ์„  ํ›„: Redis ์ขŒ์„ ์ฐจ๊ฐ ๋„์ž…

์ขŒ์„ ์ œ์–ด๋ฅผ Redis decrementSeat๋กœ ์˜ฎ๊ธฐ๋ฉด ๋ณ‘๋ ฌ์„ฑ์ด ์ข‹์•„์งˆ ๊ฑฐ๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค. ์‹ค์ œ๋กœ DB row lock ๋ณ‘๋ชฉ์€ ์ค„์—ˆ์ง€๋งŒ, ์˜ˆ์•ฝ ์„ฑ๊ณต ์งํ›„ concert.booked_count๋ฅผ ์ฆ‰์‹œ UPDATEํ•˜๋˜ ๋กœ์ง์ด ์ƒˆ๋กœ์šด ๋ฌธ์ œ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction

booking INSERT ์ดํ›„ ๊ฐ™์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ concert.booked_count๋ฅผ ๊ฐฑ์‹ ํ•˜๋ฉด์„œ ๋™์ผ ๊ณต์—ฐ ํ–‰์ด hot row๊ฐ€ ๋๊ณ , ์ผ๋ถ€ ์š”์ฒญ์ด 500์œผ๋กœ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

4. ์ตœ์ข… ๊ฐœ์„ : bookedCount ํŒŒ์ƒ๊ฐ’ ์ „ํ™˜

  • ์ขŒ์„ ์ •ํ•ฉ์„ฑ: Redis decrementSeat / restoreSeat
  • DB ์—ญํ• : booking INSERT, ์ƒํƒœ ์ €์žฅ, ๋ณด์ƒ ์ด๋ ฅ ์ €์žฅ
  • bookedCount: totalSeats - remainingSeat๋กœ ์กฐํšŒ ์‹œ ๊ณ„์‚ฐ

์ดํ›„ ๊ฐ™์€ 500์„ / 500๋ช… ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋‹ค์‹œ ์ธก์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ p95 ์ฒ˜๋ฆฌ๋Ÿ‰ ์—๋Ÿฌ์œจ
500์„ / 500๋ช… 1445ms 2518ms 186.6/sec 0%

5. ๊ฐœ์„  ์ „ vs ๊ฐœ์„  ํ›„

์ง€ํ‘œ ๊ฐœ์„  ์ „ ๊ฐœ์„  ํ›„ ๊ฐœ์„ ์œจ
Avg ์‘๋‹ต ์‹œ๊ฐ„ 3778ms 1445ms ์•ฝ 62% ๊ฐ์†Œ
p95 6822ms 2518ms ์•ฝ 63% ๊ฐ์†Œ
TPS 69.8/sec 186.6/sec ์•ฝ 167% ์ฆ๊ฐ€
Error ์ผ๋ถ€ 500 + deadlock 0% ์•ˆ์ •์„ฑ ํ™•๋ณด

6. JMeter & Grafana ๊ฒฐ๊ณผ

๊ฐœ์„  ์ „ JMeter ๊ฒฐ๊ณผ

Jmeter before

๊ฐœ์„  ์ „์—๋Š” ์ขŒ์„ ์ •ํ•ฉ์„ฑ์€ ์œ ์ง€๋์ง€๋งŒ, ์š”์ฒญ์ด ๋ชฐ๋ฆด์ˆ˜๋ก DB row lock ๋Œ€๊ธฐ๋กœ ์‘๋‹ต ์‹œ๊ฐ„์ด ๊ธ‰๊ฒฉํžˆ ์ฆ๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์„  ํ›„ JMeter ๊ฒฐ๊ณผ

Jmeter after

bookedCount๋ฅผ ํŒŒ์ƒ๊ฐ’์œผ๋กœ ์ „ํ™˜ํ•œ ๋’ค์—๋Š” deadlock ์—†์ด ๋™์ผ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ฒ˜๋ฆฌํ–ˆ๊ณ , ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„๊ณผ ์ฒ˜๋ฆฌ๋Ÿ‰์ด ํ•จ๊ป˜ ๊ฐœ์„ ๋์Šต๋‹ˆ๋‹ค.

Grafana ๋Œ€์‹œ๋ณด๋“œ

Grafana Dashboard

์˜ˆ์•ฝ ๊ฒฐ๊ณผ ๋ถ„ํฌ์™€ ๋ณด์ƒ ์ฒ˜๋ฆฌ ์ƒํƒœ๋ฅผ ํ†ตํ•ด, ๋‹จ์ˆœํžˆ ๋นจ๋ผ์กŒ๋Š”์ง€๋งŒ ๋ณธ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์‹คํŒจ ์œ ํ˜•๊ณผ ์ •ํ•ฉ์„ฑ๊นŒ์ง€ ํ•จ๊ป˜ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ถ„ํฌ ํ•ญ๋ชฉ ์˜๋ฏธ
BOOKED ์˜ˆ๋งค ์„ฑ๊ณต
ALREADY_BOOKED ์ค‘๋ณต ์˜ˆ๋งค ์‹œ๋„ โ†’ ์ฐจ๋‹จ๋จ
SOLD_OUT ์ž”์—ฌ์„ ์—†์Œ
MOCK/COMPLETED ๊ฒฐ์ œ ์„ฑ๊ณต
MOCK/CACHED ๋ฉฑ๋“ฑ์„ฑ ํ‚ค๋กœ ์žฌ๋ฐ˜ํ™˜ (์ค‘๋ณต ๊ฒฐ์ œ ์ฐจ๋‹จ)
Phase 2 โ€” EC2 ์‹ค์„œ๋ฒ„: ์ˆ˜ํ‰ ํ™•์žฅ ํšจ๊ณผ ๊ฒ€์ฆ

๊ตฌ์กฐ ๊ฐœ์„  ์ดํ›„ ์‹ค์ œ EC2 ํ™˜๊ฒฝ์—์„œ ๋‹จ์ผ ์„œ๋ฒ„์™€ 2์„œ๋ฒ„ ๊ตฌ์„ฑ์„ ๋™์ผ ์กฐ๊ฑด(300๋ช… ๋™์‹œ ์š”์ฒญ)์œผ๋กœ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ

  • EC2 #1: Nginx(๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ) + app + MySQL + Redis (t3.micro)
  • EC2 #2: app only, EC2 #1์˜ MySQLยทRedis ๊ณต์œ  (t3.micro)
  • JMeter: SyncTimer 300๋ช… ๋™์‹œ ์ถœ๋ฐœ, 300์„ ์ฝ˜์„œํŠธ

EC2 1์„œ๋ฒ„ ๊ฒฐ๊ณผ

JMeter EC2 1์„œ๋ฒ„

EC2 2์„œ๋ฒ„ ๊ฒฐ๊ณผ (Nginx ๋กœ๋“œ ๋ฐธ๋Ÿฐ์‹ฑ)

JMeter EC2 2์„œ๋ฒ„

EC2 1์„œ๋ฒ„ vs 2์„œ๋ฒ„ ๋น„๊ต

๊ตฌ์„ฑ ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ p95 TPS ์—๋Ÿฌ์œจ
EC2 1์„œ๋ฒ„ 13,822ms 14,903ms 20/sec 0%
EC2 2์„œ๋ฒ„ (Nginx LB) 1,525ms 2,341ms 110.7/sec 0%
๊ฐœ์„ ์œจ 9๋ฐฐ ๋น ๋ฆ„ 6.4๋ฐฐ ๋น ๋ฆ„ 5.5๋ฐฐ ํ–ฅ์ƒ ์œ ์ง€

t3.micro ๋‹จ์ผ ์„œ๋ฒ„๋Š” 300๋ช… ๋™์‹œ ์š”์ฒญ์—์„œ CPU ํฌ๋ ˆ๋”ง ์†Œ์ง„๊ณผ JVM GC ์••๋ฐ•์ด ๊ฒน์ณ 13์ดˆ๋Œ€ ์‘๋‹ต์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๋ฅผ 1๋Œ€ ์ถ”๊ฐ€ํ•ด Nginx๊ฐ€ ์š”์ฒญ์„ ์ ˆ๋ฐ˜์”ฉ ๋ถ„์‚ฐํ•˜์ž ๊ฐ ์„œ๋ฒ„์˜ ๋ถ€ํ•˜๊ฐ€ ์ค„์–ด ์‘๋‹ต ์‹œ๊ฐ„์ด 9๋ฐฐ ๊ฐœ์„ ๋์Šต๋‹ˆ๋‹ค.

์—๋Ÿฌ์œจ์€ ๋‘ ๊ตฌ์„ฑ ๋ชจ๋‘ 0%์ž…๋‹ˆ๋‹ค. Redis ์›์ž์  ์ขŒ์„ ์ฐจ๊ฐ์ด 2์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ๋„ ์ค‘๋ณต ์˜ˆ๋งค๋ฅผ ์™„์ „ํžˆ ์ฐจ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

๋‹จ, MySQL์€ ์—ฌ์ „ํžˆ EC2 #1 ๋‹จ์ผ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ณต์œ ํ•ฉ๋‹ˆ๋‹ค. ์š”์ฒญ์ด ๋” ๋Š˜์–ด๋‚˜๋ฉด DB๊ฐ€ ๋‹ค์Œ ๋ณ‘๋ชฉ์ด ๋  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.

Phase 3 โ€” GCP ํ™˜๊ฒฝ: ์„ค์ • ์กฐ์ •, ์Šค์ผ€์ผ์•„์›ƒ, polling ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ํƒ์ƒ‰

AWS EC2 2๋Œ€์—์„œ๋Š” VM ์ŠคํŽ™ ํ•œ๊ณ„๋กœ ๋” ํฐ ์‹คํ—˜์ด ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค. GCP e2-medium์œผ๋กœ ์ด์ „ํ•ด ์„œ๋ฒ„ ์ˆ˜์™€ polling ์ •์ฑ…์„ ๋‹จ๊ณ„์ ์œผ๋กœ ๋ฐ”๊พธ๋ฉฐ ์ธก์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

1. ๋Œ€๊ธฐ์—ด ์ง„์ž… spike โ€” POST /queue

k6๋กœ 10,000๋ช…์„ ramp-up ๋ฐฉ์‹์œผ๋กœ ๋Œ€๊ธฐ์—ด์— ์ฑ„์› ์Šต๋‹ˆ๋‹ค.

์‹œ๋‚˜๋ฆฌ์˜ค ๋ชฉํ‘œ ์œ ์ž… ์‹ค์ œ TPS ์ด ์š”์ฒญ ์ˆ˜ ํ‰๊ท  ์‘๋‹ต์‹œ๊ฐ„ p95 ์—๋Ÿฌ์œจ
1,000๋ช… / 30์ดˆ ์•ฝ 33 TPS 33.02 991 8.29ms 11.21ms 0.00%
3,000๋ช… / 30์ดˆ ์•ฝ 100 TPS 99.99 3000 6.93ms 9.43ms 0.00%
6,000๋ช… / 60์ดˆ ์•ฝ 100 TPS 100.00 6001 6.17ms 7.84ms 0.00%
10,000๋ช… / 300์ดˆ ์•ฝ 33 TPS 33.00 9900 6.24ms 7.39ms 0.00%

ramping ๋ฐฉ์‹ ํŠน์„ฑ์ƒ ์ด ์š”์ฒญ ์ˆ˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค์ƒ ๋ชฉํ‘œ ์ธ์›๊ณผ ์ •ํ™•ํžˆ ์ผ์น˜ํ•˜์ง€ ์•Š๊ณ , 9,900 ~ 10,000 ๋ฒ”์œ„๋กœ ๊ด€์ธก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋Œ€๊ธฐ์—ด ์ง„์ž… ์ž์ฒด๋Š” Redis Sorted Set ๊ธฐ๋ฐ˜ enqueue ๊ตฌ์กฐ์—์„œ ์ถฉ๋ถ„ํžˆ ์ฒ˜๋ฆฌ๋์Šต๋‹ˆ๋‹ค. ๋ณ‘๋ชฉ์€ ์ง„์ž…์ด ์•„๋‹Œ ์ง„์ž… ํ›„ ์ง€์†๋˜๋Š” polling ์ชฝ์ž„์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

2. GCP 2๋Œ€ ํ™˜๊ฒฝ์—์„œ ์—ฐ๊ฒฐ/๋ฉ”๋ชจ๋ฆฌ ์„ค์ •์„ ๋จผ์ € ์ •๋ฆฌํ–ˆ๋‹ค

GCP๋กœ ์ด์ „ํ•œ ๋’ค์—๋Š” ์„œ๋ฒ„ ์ˆ˜๋ฅผ ๋ฐ”๋กœ ๋Š˜๋ฆฌ๊ธฐ๋ณด๋‹ค, polling ๋ถ€ํ•˜์—์„œ ์–ด๋–ค ์„ค์ •์ด ์‹ค์ œ๋กœ ์˜ํ–ฅ์„ ์ฃผ๋Š”์ง€ ๋จผ์ € ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

  • threads.max=100
  • max-connections=2000
  • accept-count=500
  • app ์ปจํ…Œ์ด๋„ˆ mem_limit=1g
  • JAVA_OPTS=-Xms256m -Xmx800m
  • Prometheus / Grafana๋ฅผ ๋ถ™์—ฌ ์ธ์Šคํ„ด์Šค๋ณ„ TPS, p95๋ฅผ ๊ด€์ฐฐ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ์ •๋ฆฌ

์ด ๋‹จ๊ณ„์—์„œ ํ™•์ธํ•œ ๊ฑด "VM์„ ํ‚ค์šฐ๋Š” ๊ฒƒ"๋ณด๋‹ค, Tomcat ์—ฐ๊ฒฐ ์ˆ˜ยท๋Œ€๊ธฐ์—ดยทJVM ๋ฉ”๋ชจ๋ฆฌยท๊ด€์ธก ๊ฐ€๋Šฅ์„ฑ์„ ๋จผ์ € ๋งž์ถฐ์•ผ ๊ฒฐ๊ณผ๋ฅผ ํ•ด์„ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

3. app 2๋Œ€ โ†’ 3๋Œ€ ์Šค์ผ€์ผ์•„์›ƒ

๊ตฌ์„ฑ ๋ถ€ํ•˜ ์‹คํŒจ์œจ p95 ๊ฒฐ๊ณผ
app 2๋Œ€ 500 req/s 0% 209.87ms ์„ฑ๊ณต
app 2๋Œ€ 1000 req/s 41.77% 4.5s ์‹คํŒจ
app 3๋Œ€ 500 req/s 0% 19.2ms ์„ฑ๊ณต
app 3๋Œ€ 1000 req/s 0% 91.86ms ์„ฑ๊ณต
app 3๋Œ€ 1500 req/s 5.61% ~ 21.91% 1.06s ~ 1.2s ์‹คํŒจ

k6 app 3๋Œ€ / 1000 req/s ์„ฑ๊ณต

k6 app 3๋Œ€ / 1500 req/s ์‹คํŒจ

3๋Œ€๋กœ ๋Š˜๋ฆฐ ๋’ค 1,000 req/s๋Š” ์•ˆ์ •ํ™”๋์ง€๋งŒ 1,500 req/s๋Š” ์—ฌ์ „ํžˆ ์‹คํŒจ. ํ˜„์žฌ ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ๋Š” 1,000 ~ 1,500 req/s ์‚ฌ์ด์ž…๋‹ˆ๋‹ค.

4. Adaptive Polling ์ ์šฉ

๊ณ ์ • 5์ดˆ polling ๊ธฐ์ค€์œผ๋กœ 10,000๋ช…์ด ๋งŒ๋“ค์–ด๋‚ด๋Š” 2,000 req/s๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•ด ์„œ๋ฒ„๊ฐ€ nextPollMs๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

  • position <= 1,000 โ†’ 5,000ms (์•ž์ˆœ๋ฒˆ: ๋น ๋ฅธ ์กฐํšŒ)
  • position > 1,000 โ†’ 10,000ms (๋’ท์ˆœ๋ฒˆ: ๋А๋ฆฐ ์กฐํšŒ)
  • ๊ธฐ๋Œ€ ๋ถ€ํ•˜: 1,000 ร— 1/5s + 9,000 ร— 1/10s = 1,100 req/s (45% ๊ฐ์†Œ)

์ •์ฑ… ์ž์ฒด๋Š” ์˜๋„๋Œ€๋กœ ๋™์ž‘ํ–ˆ์ง€๋งŒ, 10,000 VU closed-model๋กœ ๊ทธ๋Œ€๋กœ ๊ฒ€์ฆํ•˜๋ ค ํ•˜์ž ๋ถ€ํ•˜๋ฐœ์ƒ๊ธฐ์˜ thundering herd ๋ฌธ์ œ๊ฐ€ ๋จผ์ € ๋“œ๋Ÿฌ๋‚ฌ์Šต๋‹ˆ๋‹ค. "์„œ๋ฒ„ ํ•œ๊ณ„ ํ…Œ์ŠคํŠธ(constant-arrival-rate)"์™€ "์šด์˜ ์ •์ฑ… ํšจ๊ณผ ๊ฒ€์ฆ(VU ๊ธฐ๋ฐ˜)"์€ ๋ถ„๋ฆฌํ•ด์„œ ๋ด์•ผ ํ•œ๋‹ค๋Š” ์ ์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

5. ์˜ˆ๋งค ๋™์‹œ์„ฑ โ€” Redis ์บ์‹œ + HikariCP ํŠœ๋‹

polling ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ๋ฅผ ํ™•์ธํ•œ ๋’ค์—๋Š”, ๋ณ„๊ฐœ ์ถ•์œผ๋กœ booking API ์ž์ฒด๊ฐ€ ๋™์‹œ์— ๋ชฐ๋ ค๋„ ์ •ํ•ฉ์„ฑ๊ณผ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ์œ ์ง€ํ•˜๋Š”์ง€๋ฅผ ๋‹ค์‹œ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค.

๋Œ€๊ธฐ์—ด ํ…Œ์ŠคํŠธ์™€ ๋ณ„๊ฐœ๋กœ, GCP ์ด์ „ ํ›„ ์˜ˆ๋งค ์ž์ฒด์˜ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. JMeter SyncTimer๋กœ 300๋ช…์ด ๋™์‹œ์— ์ถœ๋ฐœํ•˜๋Š” ์กฐ๊ฑด์—์„œ 300์„ ์ฝ˜์„œํŠธ๋ฅผ ์˜ˆ๋งคํ•˜๋Š” ์‹œ๋‚˜๋ฆฌ์˜ค์ž…๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ: GCP e2-medium 2๋Œ€, JMeter SyncTimer 300๋ช… ๋™์‹œ ์ถœ๋ฐœ, 300์„ ์ฝ˜์„œํŠธ

์ ์šฉํ•œ ์ตœ์ ํ™” ๋ชฉ๋ก

๋ณ€๊ฒฝ ํ•ญ๋ชฉ ๋ณ€๊ฒฝ ๋‚ด์šฉ ์ด์œ 
HikariCP pool-size 50 โ†’ 20 pool=50์—์„œ hot-row contention ์‹ฌํ™” ํ™•์ธ (Phase 1 ์‹คํ—˜ ๋™์ผ ์›์ธ)
Redis ์ƒํƒœ ์บ์‹œ ์—†์Œ โ†’ concert:status:{id} TTL 30s ์˜ˆ๋งค๋งˆ๋‹ค SELECT Concert DB ์กฐํšŒ ์ œ๊ฑฐ
getReferenceById() findById() โ†’ ํ”„๋ก์‹œ ์ฐธ์กฐ ์บ์‹œ ํžˆํŠธ ์‹œ DB ์กฐํšŒ ์—†์ด FK ์—ฐ๊ด€๋งŒ ์„ค์ •
์บ์‹œ ๋ฌดํšจํ™” ์—†์Œ โ†’ ์ฝ˜์„œํŠธ ์‚ญ์ œยท์ˆ˜์ • ์‹œ ์ž๋™ ์ œ๊ฑฐ ์บ์‹œ TTL ๋งŒ๋ฃŒ ์ „ ์ •ํ•ฉ์„ฑ ๋ณด์žฅ

์ธก์ • ๊ฒฐ๊ณผ ๋น„๊ต

๊ตฌ์„ฑ TPS ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ ์ค‘์•™๊ฐ’ p90 p95 ์—๋Ÿฌ์œจ
๋ฐฐ์น˜ ํŠœ๋‹๋งŒ ์ ์šฉ 47.7/sec 2,775ms 2,088ms 5,761ms 6,049ms ์žˆ์Œ
๋‚™๊ด€์  ๋ฝ (pool=10) 59.8/sec 2,786ms 2,632ms 4,429ms 4,723ms 0%
HikariCP pool=50 61.3/sec 3,576ms 3,468ms 4,633ms 4,807ms 0%
ํ˜„์žฌ ์ฝ”๋“œ ๋ณ€๊ฒฝ ํ›„ (pool=20 + Redis ์ƒํƒœ ์บ์‹œ) 74.1/sec 2,993ms 2,974ms 3,808ms 3,945ms 0%

Redis ์ขŒ์„ ์ฐจ๊ฐ์€ ์ด๋ฏธ ์›์ž์ ์ด๋ผ ๋™์‹œ์„ฑ ์˜ค๋ฅ˜ ์—†์ด 0% ์—๋Ÿฌ๋ฅผ ์œ ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค. pool ๊ณผ๋‹ค ์„ค์ •์ด hot-row contention์„ ํ‚ค์šด๋‹ค๋Š” Phase 1์˜ ๊ฒฐ๋ก ์ด GCP ํ™˜๊ฒฝ์—์„œ๋„ ๋™์ผํ•˜๊ฒŒ ์žฌํ™•์ธ๋์Šต๋‹ˆ๋‹ค.


๐Ÿšจ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

์ž…์žฅ๊ถŒ ์†Œ๋น„ ์ค‘ TTL ๋งŒ๋ฃŒ ํƒ€์ด๋ฐ ์ถฉ๋Œ โ€” EXISTS์™€ DEL ์‚ฌ์ด์˜ ํ‹ˆ

๋ฌธ์ œ ์ƒํ™ฉ

์ดˆ๊ธฐ ๊ตฌํ˜„์—์„œ ์ž…์žฅ๊ถŒ ์†Œ๋น„๋ฅผ EXISTS ํ™•์ธ ํ›„ DEL๋กœ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฐฉ์‹์—์„œ ์ž…์žฅ๊ถŒ์ด ์—†๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์˜ˆ๋งค๋ฅผ ํ†ต๊ณผํ•˜๋Š” ์ผ€์ด์Šค๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์›์ธ ํƒ์ƒ‰

EXISTS์™€ DEL์€ ๋ณ„๊ฐœ์˜ Redis ๋ช…๋ น์ž…๋‹ˆ๋‹ค. Java์—์„œ ์ˆœ์ฐจ ํ˜ธ์ถœํ•˜๋ฉด ๋‘ ๋ช…๋ น ์‚ฌ์ด์— ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

EXISTS โ†’ true
  ... ์ด ์‚ฌ์ด์— TTL ๋งŒ๋ฃŒ ...
DEL โ†’ "์„ฑ๊ณต" (์—†๋Š” ํ‚ค๋ฅผ DELํ•ด๋„ Redis๋Š” ์—๋Ÿฌ๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š์Œ)
โ†’ ์˜ˆ๋งค ์ง„ํ–‰  โ† ์ž…์žฅ๊ถŒ์ด ์—†๋Š” ์ƒํƒœ์—์„œ ํ†ต๊ณผ

DEL์˜ ๋ฐ˜ํ™˜๊ฐ’์ด 0(์‚ญ์ œํ•  ํ‚ค ์—†์Œ)์ด์–ด๋„ ์ฝ”๋“œ์—์„œ ์ด๋ฅผ ํ™•์ธํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

์„ ํƒ์ง€๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

๋ฐฉ๋ฒ• 1: DEL ๋ฐ˜ํ™˜๊ฐ’ ํ™•์ธ DEL์ด 0์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ์ด๋ฏธ ๋งŒ๋ฃŒ๋œ ๊ฒƒ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. EXISTS + DEL ๋‘ ๋ช…๋ น์ด ๋”ฐ๋กœ ์‹คํ–‰๋˜๋Š” ๊ฑด ๋™์ผํ•ด์„œ EXISTS์™€ DEL ์‚ฌ์ด์— ๋งŒ๋ฃŒ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด EXISTS๋Š” true๋ฅผ ๋ฐ˜ํ™˜ํ–ˆ์ง€๋งŒ DEL ๊ฒฐ๊ณผ๋Š” 0์ด ๋ฉ๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ๋Š” ์žก์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ DEL ์ดํ›„ ๋งŒ๋ฃŒ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ์žก์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทผ๋ณธ์ ์œผ๋กœ ๋‘ ๋ช…๋ น ์‚ฌ์ด์˜ ํƒ€์ด๋ฐ ์˜์กด์„ฑ์ด ๋‚จ์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 2: SET NX EX ๋ฎคํ…์Šค ๋ฐฉ์‹ ์•„์˜ˆ ์ž…์žฅ๊ถŒ ์†Œ๋น„๋ฅผ ๋ณ„๋„ ํ‚ค ์ ์œ  ๋ฐฉ์‹์œผ๋กœ ๊ต์ฒดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ตฌ์กฐ๊ฐ€ ๋‹ฌ๋ผ์ ธ ๊ธฐ์กด TTL์„ ๋ณต์›ํ•˜๋Š” ๋กœ์ง์„ ๋งŒ๋“ค๊ธฐ ์–ด๋ ต๊ณ , ์„ค๊ณ„ ๋ณต์žก๋„๊ฐ€ ์˜ฌ๋ผ๊ฐ‘๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 3: Lua Script EXISTS โ†’ TTL โ†’ DEL์„ Redis ์„œ๋ฒ„์—์„œ ์›์ž์ ์œผ๋กœ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ์„ธ ๋ช…๋ น์ด ๋Š๊ธฐ์ง€ ์•Š๊ณ  ๋‹จ์œ„ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— TOCTOU๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€๋กœ TTL ๋ฐ˜ํ™˜๊ฐ’์„ ํ™œ์šฉํ•ด DB ์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ ์ž…์žฅ๊ถŒ์„ ์›๋ž˜ ์ž”์—ฌ TTL๋กœ ๋ณต์›ํ•˜๋Š” ๊ธฐ๋Šฅ๋„ ์–ป์—ˆ์Šต๋‹ˆ๋‹ค.

Lua Script์˜ ๋‹จ์ (๋””๋ฒ„๊น… ๋‚œ์ด๋„)์€ ์žˆ์ง€๋งŒ, ์„ธ ์ค„์งœ๋ฆฌ ์Šคํฌ๋ฆฝํŠธ์ด๊ณ  ๋™์ž‘์ด ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค. ๋ฐฉ๋ฒ• 1์€ ํƒ€์ด๋ฐ ์˜์กด์„ฑ์„ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜์ง€ ๋ชปํ•˜๊ณ , ๋ฐฉ๋ฒ• 2๋Š” ๊ธฐ์กด ๊ตฌ์กฐ๋ฅผ ๋„ˆ๋ฌด ํฌ๊ฒŒ ๋ฐ”๊ฟ‰๋‹ˆ๋‹ค.

-- CLAIM_ADMITTED: EXISTS โ†’ TTL โ†’ DEL ์›์ž์  ์ฒ˜๋ฆฌ, ์—†์œผ๋ฉด -1 ๋ฐ˜ํ™˜
if redis.call('EXISTS', key) == 0 then return -1 end
local ttl = redis.call('TTL', key)
redis.call('DEL', key)
return ttl

๊ฒฐ๊ณผ

์ž…์žฅ๊ถŒ์ด ์—†๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ์ƒํƒœ์—์„œ ์˜ˆ๋งค ์‹œ๋„๊ฐ€ ์ •ํ™•ํžˆ ์ฐจ๋‹จ๋ฉ๋‹ˆ๋‹ค. DB ์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ ๋ฐ˜ํ™˜๋ฐ›์€ TTL๋กœ ์ž…์žฅ๊ถŒ์„ ๋ณต์›ํ•ด ์žฌ์‹œ๋„๋ฅผ ํ—ˆ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋์Šต๋‹ˆ๋‹ค.

Outbox ์ƒํƒœ๊ฐ€ ์ฒ˜๋ฆฌ ํ›„์—๋„ PENDING์œผ๋กœ ๋‚จ์•˜๋‹ค โ€” detached ์—”ํ‹ฐํ‹ฐ ํ•จ์ •

๋ฌธ์ œ ์ƒํ™ฉ

๋ณด์ƒ ์ฒ˜๋ฆฌ ๋กœ์ง์ด ์‹คํ–‰๋๋Š”๋ฐ๋„ PaymentCompensationOutbox ์ƒํƒœ๊ฐ€ PUBLISHED๋‚˜ FAILED๋กœ ๋ฐ”๋€Œ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ฐ™์€ Outbox๊ฐ€ ์Šค์ผ€์ค„๋Ÿฌ์— ์˜ํ•ด ๋ฐ˜๋ณต ์กฐํšŒ๋˜์–ด ๋ณด์ƒ ์ฒ˜๋ฆฌ๊ฐ€ ์ค‘๋ณต ์‹œ๋„๋์Šต๋‹ˆ๋‹ค.

์›์ธ ํƒ์ƒ‰

๋กœ๊ทธ์—๋Š” outbox.markPublished() ํ˜ธ์ถœ์ด ๋ถ„๋ช…ํžˆ ์ฐํ˜”์Šต๋‹ˆ๋‹ค. ์˜ˆ์™ธ๋„ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ DB๋ฅผ ์ง์ ‘ ์กฐํšŒํ•ด๋ณด๋ฉด ์ƒํƒœ๊ฐ€ ๊ทธ๋Œ€๋กœ์ž…๋‹ˆ๋‹ค.

JPA dirty checking์ด ๋™์ž‘ํ•˜๋ ค๋ฉด ์—”ํ‹ฐํ‹ฐ๊ฐ€ ํ˜„์žฌ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์—์„œ ๊ด€๋ฆฌ๋˜๋Š” managed ์ƒํƒœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์Šค์ผ€์ค„๋Ÿฌ ๋ฉ”์„œ๋“œ์—๋Š” @Transactional์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค. findByStatus()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด Spring Data JPA๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ์งง์€ ํŠธ๋žœ์žญ์…˜์„ ์—ด๊ณ  ๋ฐ”๋กœ ๋‹ซ์Šต๋‹ˆ๋‹ค. ์ด ์ˆœ๊ฐ„ ๋ฐ˜ํ™˜๋œ ์—”ํ‹ฐํ‹ฐ๋Š” detached ์ƒํƒœ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

์ด detached ์—”ํ‹ฐํ‹ฐ๋ฅผ REQUIRES_NEW ํŠธ๋žœ์žญ์…˜์„ ๊ฐ€์ง„ Processor๋กœ ๊ทธ๋Œ€๋กœ ๋„˜๊ธฐ๋ฉด, ์ƒˆ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋Š” ์ด ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์•„๋ฌด๋ฆฌ markPublished()๋ฅผ ํ˜ธ์ถœํ•ด๋„ ์ž๋ฐ” ๊ฐ์ฒด์˜ ํ•„๋“œ๊ฐ’๋งŒ ๋ฐ”๋€” ๋ฟ, DB UPDATE๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์„ ํƒ์ง€๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

๋ฐฉ๋ฒ• 1: entityManager.merge(outbox) detached ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์žฌ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. ๋™์ž‘ํ•˜์ง€๋งŒ, merge๋Š” ์ „๋‹ฌ๋œ ์—”ํ‹ฐํ‹ฐ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ DB์— ๊ทธ๋Œ€๋กœ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค. ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์กฐํšŒ๋œ ์‹œ์  ์ดํ›„๋กœ ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€ ๊ฒŒ ์žˆ๋‹ค๋ฉด stale ๋ฐ์ดํ„ฐ๋กœ ๋ฎ์–ด์“ธ ์œ„ํ—˜์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 2: ์Šค์ผ€์ค„๋Ÿฌ์— @Transactional ์ถ”๊ฐ€ ์ „์ฒด Outbox ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๋ฌถ์œผ๋ฉด ์—”ํ‹ฐํ‹ฐ๊ฐ€ managed ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ Outbox A ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด Outbox B๊นŒ์ง€ ํ•จ๊ป˜ ๋กค๋ฐฑ๋ฉ๋‹ˆ๋‹ค. Outbox๋ณ„ ๋…๋ฆฝ ์ฒ˜๋ฆฌ๋ผ๋Š” ํŒจํ„ด์˜ ์˜๋ฏธ๊ฐ€ ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 3: ID๋งŒ ์ „๋‹ฌํ•˜๊ณ  Processor ์•ˆ์—์„œ ์žฌ์กฐํšŒ REQUIRES_NEW ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์—์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒˆ๋กœ ์กฐํšŒํ•˜๋ฉด ํ•ญ์ƒ managed ์ƒํƒœ๊ฐ€ ๋ณด์žฅ๋ฉ๋‹ˆ๋‹ค. ํ•ญ์ƒ ์ตœ์‹  DB ์ƒํƒœ๋ฅผ ์ฝ์–ด์˜ค๊ธฐ ๋•Œ๋ฌธ์— stale ๋ฐ์ดํ„ฐ ๋ฎ์–ด์“ฐ๊ธฐ ์œ„ํ—˜๋„ ์—†์Šต๋‹ˆ๋‹ค.

// Before: detached ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌ
outboxProcessor.process(outbox);

// After: ID๋งŒ ์ „๋‹ฌ โ†’ REQUIRES_NEW ํŠธ๋žœ์žญ์…˜ ๋‚ด๋ถ€์—์„œ ์žฌ์กฐํšŒ
outboxProcessor.process(outbox.getId());

๊ฒฐ๊ณผ

Outbox ์ƒํƒœ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ PUBLISHED/FAILED๋กœ ๋ฐ˜์˜๋์Šต๋‹ˆ๋‹ค. @Transactional์„ ๋ถ™์ด๋Š” ๊ฒƒ๋งŒํผ์ด๋‚˜, ์–ด๋–ค ํŠธ๋žœ์žญ์…˜์—์„œ ๋กœ๋“œ๋œ ์—”ํ‹ฐํ‹ฐ์ธ์ง€๋ฅผ ์‹ ๊ฒฝ ์จ์•ผ ํ•œ๋‹ค๋Š” ๊ฑธ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค.

ํŠน์ • Outbox ์‹คํŒจ๊ฐ€ ๋‹ค๋ฅธ Outbox ์ฒ˜๋ฆฌ์— ์˜ํ–ฅ์„ ์คฌ๋‹ค โ€” Spring AOP self-invocation

๋ฌธ์ œ ์ƒํ™ฉ

PENDING ์ƒํƒœ์˜ Outbox๊ฐ€ ์—ฌ๋Ÿฌ ๊ฑด์ผ ๋•Œ, ํŠน์ • ๊ฑด์—์„œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์•ž์„œ ์ฒ˜๋ฆฌ๋œ Booking/Concert ์ƒํƒœ ๋ณ€๊ฒฝ์ด ์˜๋„์น˜ ์•Š๊ฒŒ ์ปค๋ฐ‹๋˜๊ฑฐ๋‚˜ ๋กค๋ฐฑ๋์Šต๋‹ˆ๋‹ค. Outbox A์˜ ์‹คํŒจ๊ฐ€ Outbox B ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ์— ์˜ํ–ฅ์„ ์คฌ์Šต๋‹ˆ๋‹ค.

์›์ธ ํƒ์ƒ‰

์ดˆ๊ธฐ ์Šค์ผ€์ค„๋Ÿฌ๋Š” ์—ฌ๋Ÿฌ Outbox๋ฅผ ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ์ˆœ์ฐจ ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. ์ค‘๊ฐ„์— ์˜ˆ์™ธ๊ฐ€ ๋‚˜๋ฉด ์ „์ฒด๊ฐ€ ๋กค๋ฐฑ๋ฉ๋‹ˆ๋‹ค. Outbox๋ณ„ ๋…๋ฆฝ์„ฑ์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฐ™์€ ํด๋ž˜์Šค ์•ˆ์— @Transactional(REQUIRES_NEW) ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด ๊ฐ Outbox๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋จผ์ € ์‹œ๋„ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ Spring AOP๋Š” ๊ฐ™์€ ํด๋ž˜์Šค ๋‚ด๋ถ€์˜ ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ(self-invocation)์„ ํ”„๋ก์‹œ๋กœ ๊ฐ์‹ธ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. REQUIRES_NEW๋ฅผ ๋ถ™์—ฌ๋„ ์‹ค์ œ๋กœ๋Š” ์ ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์„ ํƒ์ง€๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

๋ฐฉ๋ฒ• 1: ๊ฐ™์€ ํด๋ž˜์Šค ๋‚ด @Transactional(REQUIRES_NEW) ์•ž์„œ ์„ค๋ช…ํ•œ ๋Œ€๋กœ Spring AOP self-invocation ๋ฌธ์ œ๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 2: ApplicationContext.getBean()์œผ๋กœ ์ž๊ธฐ ์ž์‹  ์ฃผ์ž… Self-invocation์„ ์šฐํšŒํ•˜๋Š” ๋ฐฉ๋ฒ•์ด์ง€๋งŒ, ๋นˆ์ด ์ž๊ธฐ ์ž์‹ ์„ ApplicationContext์—์„œ ๊บผ๋‚ด๋Š” ๊ฑด Spring์˜ ์˜์กด์„ฑ ์ฃผ์ž… ์›์น™์— ์–ด๊ธ‹๋‚ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ ์˜๋„๋ฅผ ์ฝ๊ธฐ ์–ด๋ ต๊ฒŒ ๋งŒ๋“ค๊ณ , ์œ ์ง€๋ณด์ˆ˜ ์‹œ ํ˜ผ๋ž€์„ ์ค๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 3: TransactionTemplate ์ง์ ‘ ์‚ฌ์šฉ ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑํ•˜๊ฒŒ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋™์ž‘ํ•˜์ง€๋งŒ, ์„ ์–ธ์  @Transactional๋ณด๋‹ค ์ฝ”๋“œ๊ฐ€ ์žฅํ™ฉํ•ด์ง€๊ณ  ๋งค๋ฒˆ try-catch ํŒจํ„ด์„ ๋ฐ˜๋ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 4: ๋ณ„๋„ @Component Bean์œผ๋กœ ๋ถ„๋ฆฌ PaymentCompensationOutboxProcessor๋ฅผ ๋ณ„๋„ ๋นˆ์œผ๋กœ ๋งŒ๋“ค๋ฉด ์Šค์ผ€์ค„๋Ÿฌ์—์„œ ์™ธ๋ถ€ ๋นˆ์„ ํ˜ธ์ถœํ•˜๋Š” ๊ตฌ์กฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค. Spring AOP ํ”„๋ก์‹œ๊ฐ€ ์ •์ƒ ์ ์šฉ๋˜๊ณ , ์ฝ”๋“œ ๊ตฌ์กฐ๋„ "์Šค์ผ€์ค„๋Ÿฌ๋Š” ๋ชฉ๋ก ์กฐํšŒ์™€ ์œ„์ž„, Processor๋Š” ์ฒ˜๋ฆฌ ์ฑ…์ž„"์œผ๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ถ„๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void process(Long outboxId) { ... }

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void markRetry(Long outboxId, Exception cause) { ... }

๊ฒฐ๊ณผ

Outbox A ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ Outbox B ์ฒ˜๋ฆฌ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. Outbox ํŒจํ„ด์€ ํ…Œ์ด๋ธ” ๊ตฌ์กฐ๋งŒํผ์ด๋‚˜ ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ์„ค๊ณ„๊ฐ€ ์ค‘์š”ํ•˜๋‹ค๋Š” ๊ฑธ ์‹ค๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

Toss API ์žฅ์• ๊ฐ€ ๋‚ด๋ถ€ ์„œ๋น„์Šค๋กœ ์ „ํŒŒ๋๋‹ค โ€” ํƒ€์ž„์•„์›ƒ๊ณผ Circuit Breaker

๋ฌธ์ œ ์ƒํ™ฉ

์™ธ๋ถ€ PG API ์‘๋‹ต์ด ์ง€์—ฐ๋˜๋ฉด ๊ฒฐ์ œ ์„œ๋น„์Šค ์ „์ฒด๊ฐ€ ๋ฉˆ์ถ”๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์˜€์Šต๋‹ˆ๋‹ค. ์ดˆ๊ธฐ ๊ตฌ์กฐ์—์„œ๋Š” PaymentService ์•ˆ์—์„œ ์ง์ ‘ Toss API HTTP ํ˜ธ์ถœ์„ ํ–ˆ๊ณ , ์‘๋‹ต์ด ์˜ค์ง€ ์•Š์œผ๋ฉด ์Šค๋ ˆ๋“œ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋ธ”๋กœํ‚น๋์Šต๋‹ˆ๋‹ค.

์›์ธ ํƒ์ƒ‰

์™ธ๋ถ€ API ํ˜ธ์ถœ ํƒ€์ž„์•„์›ƒ ์„ค์ •์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค. ํƒ€์ž„์•„์›ƒ์ด ์—†์œผ๋ฉด ์‘๋‹ต์„ ๋ฐ›์„ ๋•Œ๊นŒ์ง€ ๋ฌดํ•œ ๋Œ€๊ธฐํ•ฉ๋‹ˆ๋‹ค. ๋™์‹œ์— ์—ฌ๋Ÿฌ ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ์Šค๋ ˆ๋“œ ํ’€์ด ์ˆœ์‹๊ฐ„์— ๊ณ ๊ฐˆ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์™ธ๋ถ€ ์‹œ์Šคํ…œ์˜ ์žฅ์• ๊ฐ€ ๋‚ด ์„œ๋น„์Šค ์ „์ฒด๋กœ ์ „ํŒŒ๋˜๋Š” ๊ตฌ์กฐ์˜€์Šต๋‹ˆ๋‹ค.

์„ ํƒ์ง€๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

๋ฐฉ๋ฒ• 1: ํƒ€์ž„์•„์›ƒ๋งŒ ์„ค์ • ์Šค๋ ˆ๋“œ ์ ์œ  ์‹œ๊ฐ„์„ ์ œํ•œํ•  ์ˆ˜ ์žˆ์–ด "ํ•œ ์š”์ฒญ์ด ๋ฌดํ•œ ๋ธ”๋กœํ‚น"ํ•˜๋Š” ๋ฌธ์ œ๋Š” ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์™ธ๋ถ€ API๊ฐ€ ์™„์ „ํžˆ ์žฅ์•  ์ƒํƒœ์ธ ๊ฒฝ์šฐ, ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ๋•Œ๋งˆ๋‹ค ๋งค๋ฒˆ ํƒ€์ž„์•„์›ƒ์„ ๊ธฐ๋‹ค๋ฆฐ ๋’ค ์‹คํŒจํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์žฅ์•  ์ƒํƒœ์ธ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์— ๊ณ„์† ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋‚ญ๋น„๊ฐ€ ์žˆ๊ณ , ์„œ๋น„์Šค ์‘๋‹ต ์†๋„๋„ ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„๋งŒํผ ๋А๋ ค์ง‘๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 2: Bulkhead (์Šค๋ ˆ๋“œ ํ’€ ๊ฒฉ๋ฆฌ) ์™ธ๋ถ€ API ํ˜ธ์ถœ ์ „์šฉ ์Šค๋ ˆ๋“œ ํ’€์„ ๋ถ„๋ฆฌํ•˜๋ฉด ์™ธ๋ถ€ ์žฅ์• ๊ฐ€ ๋‚ด๋ถ€ ์„œ๋น„์Šค ์Šค๋ ˆ๋“œ ํ’€์„ ๊ณ ๊ฐˆ์‹œํ‚ค์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ๊ฐ•ํ•œ ๊ฒฉ๋ฆฌ ์ˆ˜๋‹จ์ด์ง€๋งŒ Resilience4j Bulkhead ์„ค์ •๊ณผ ์ „์šฉ ์Šค๋ ˆ๋“œ ํ’€ ๊ด€๋ฆฌ๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 3: ํƒ€์ž„์•„์›ƒ + Circuit Breaker ํƒ€์ž„์•„์›ƒ์œผ๋กœ ๊ฐœ๋ณ„ ์š”์ฒญ ๋ธ”๋กœํ‚น์„ ์ œํ•œํ•˜๊ณ , ์—ฐ์† ์‹คํŒจ๊ฐ€ ์ž„๊ณ„์น˜๋ฅผ ๋„˜์œผ๋ฉด Circuit์„ Openํ•ด ์™ธ๋ถ€ API ํ˜ธ์ถœ ์ž์ฒด๋ฅผ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. ์žฅ์•  ์ƒํƒœ์ธ ์™ธ๋ถ€ ์‹œ์Šคํ…œ์— ๊ณ„์† ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋‚ญ๋น„๋ฅผ ์ค„์ด๊ณ , ๋น ๋ฅธ ์‹คํŒจ(fail-fast)๋กœ ์‘๋‹ต ์†๋„๋„ ์œ ์ง€๋ฉ๋‹ˆ๋‹ค.

๋ณ„๋„ fallback์„ ๋งŒ๋“ค์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. Circuit Breaker ์˜ˆ์™ธ๋‚˜ timeout ์˜ˆ์™ธ๋„ ๊ธฐ์กด catch ๋ธ”๋ก์˜ payment.fail() + Outbox ์ €์žฅ ๊ฒฝ๋กœ๋กœ ํ•ฉ๋ฅ˜ํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค. ์žฅ์•  ๋ณดํ˜ธ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋ฉด์„œ ๊ธฐ์กด ๋ณด์ƒ ํ๋ฆ„์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ํƒ€์ž„์•„์›ƒ: connect 3s, read 3s. ์Šค๋ ˆ๋“œ ์ ์œ  ์‹œ๊ฐ„์„ ๋ช…์‹œ์ ์œผ๋กœ ์ œํ•œ
  • Circuit Breaker: ์—ฐ์† ์‹คํŒจ ์ž„๊ณ„์น˜ ์ดˆ๊ณผ ์‹œ ํšŒ๋กœ ์˜คํ”ˆ, ์ดํ›„ ์š”์ฒญ์€ ์ฆ‰์‹œ ์‹คํŒจ ๋ฐ˜ํ™˜

๊ฒฐ๊ณผ

์™ธ๋ถ€ PG ์ง€์—ฐ ์‹œ ํƒ€์ž„์•„์›ƒ์ด ๋น ๋ฅด๊ฒŒ ๋ฐœ์ƒํ•˜๊ณ , ์—ฐ์† ์‹คํŒจ ์‹œ Circuit Open์œผ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค. ์™ธ๋ถ€ API ์—ฐ๋™์—์„œ "์„ฑ๊ณต ๊ฒฝ๋กœ"๋งŒํผ์ด๋‚˜ "์žฅ์• ๋ฅผ ๋‚ด ์„œ๋น„์Šค ์•ˆ์œผ๋กœ ์–ผ๋งˆ๋‚˜ ๋œ ๊ฐ€์ ธ์˜ค๊ฒŒ ์„ค๊ณ„ํ•˜๋А๋ƒ"๊ฐ€ ์ค‘์š”ํ•˜๋‹ค๋Š” ๊ฑธ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ ํ›„ ๊ณต์—ฐ ๋ชฉ๋ก ์กฐํšŒ ์‹œ CORS ์—๋Ÿฌ โ€” ์„ค์ •์ด ์•„๋‹Œ ๊ตฌ์กฐ ๋ฌธ์ œ์˜€๋‹ค

๋ฌธ์ œ ์ƒํ™ฉ

๋ฐฐํฌ ํ›„ ๋กœ๊ทธ์ธ ์ „์—๋Š” ๊ณต์—ฐ ๋ชฉ๋ก์ด ์ •์ƒ ์กฐํšŒ๋์ง€๋งŒ, ๋กœ๊ทธ์ธ ํ›„์—๋Š” ๋ชฉ๋ก์ด ๋ณด์ด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ € Network ํƒญ์—๋Š” CORS error๊ฐ€ ํ‘œ์‹œ๋๊ณ  Provisional headers are shown ๋ฉ”์‹œ์ง€๊ฐ€ ํ•จ๊ป˜ ๋‚˜ํƒ€๋‚ฌ์Šต๋‹ˆ๋‹ค.

์›์ธ ํƒ์ƒ‰

์ฒ˜์Œ์—๋Š” Spring Security๋‚˜ Spring CORS ์„ค์ •์ด ๋น ์ง„ ๊ฒƒ์œผ๋กœ ๋ดค์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋กœ๊ทธ์ธ ์ „ํ›„ ์š”์ฒญ ํ๋ฆ„์„ ๋น„๊ตํ•ด๋ณด๋‹ˆ ์ฐจ์ด๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • ๋กœ๊ทธ์ธ ์ „: ๋‹จ์ˆœ GET ์š”์ฒญ (Simple Request) โ†’ Preflight ์—†์Œ
  • ๋กœ๊ทธ์ธ ํ›„: Authorization ํ—ค๋” ํฌํ•จ โ†’ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ Preflight(OPTIONS) ์š”์ฒญ์„ ๋จผ์ € ๋ฐœ์†ก

Spring Security๊ฐ€ OPTIONS ์š”์ฒญ์„ ์ธ์ฆ ์—†์ด ํ—ˆ์šฉํ•˜์ง€ ์•Š์•„ Preflight๊ฐ€ 401๋กœ ์ฐจ๋‹จ๋์Šต๋‹ˆ๋‹ค. ์•„๋ž˜ ์„ค์ •์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()

Preflight๋Š” ํ†ต๊ณผํ–ˆ์ง€๋งŒ ์—ฌ์ „ํžˆ CORS ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. Network ํƒญ์—์„œ ์‹ค์ œ ์š”์ฒญ URL์„ ํ™•์ธํ•ด๋ณด๋‹ˆ https://acornpost.cloud/api/concerts๋กœ, ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ EC2๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. Nginx์™€ Spring Boot์˜ ํ—ˆ์šฉ Origin ์„ค์ •์ด ์™„์ „ํžˆ ์ผ์น˜ํ•˜์ง€ ์•Š์•„ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ฐจ๋‹จ๋˜๋Š” ๊ตฌ์กฐ์˜€์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ตญ "์–ด๋–ค Origin์„ ํ—ˆ์šฉํ• ์ง€"๋ฅผ ๋งž์ถ”๋Š” ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ, ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ๋‹ค๋ฅธ ์ถœ์ฒ˜ ์„œ๋ฒ„๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•œ๋‹ค๋Š” ๊ตฌ์กฐ ์ž์ฒด๊ฐ€ ๋ฌธ์ œ์˜€์Šต๋‹ˆ๋‹ค.

์„ ํƒ์ง€๋ฅผ ๋น„๊ตํ–ˆ์Šต๋‹ˆ๋‹ค

๋ฐฉ๋ฒ• 1: Nginx + Spring Boot CORS ์„ค์ •์„ ์ •๋ฐ€ํ•˜๊ฒŒ ๋งž์ถ”๊ธฐ

Nginx์—์„œ Access-Control-Allow-Origin, Allow-Headers๋ฅผ ์„ค์ •ํ•˜๊ณ , Spring Boot์—์„œ๋„ ๋™์ผํ•œ Origin์„ ํ—ˆ์šฉํ•˜๋ฉด ํ•ด๊ฒฐ๋ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋‘ ๊ณ„์ธต์˜ ์„ค์ •์ด ์กฐ๊ธˆ์ด๋ผ๋„ ์–ด๊ธ‹๋‚˜๋ฉด ๋‹ค์‹œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํ™˜๊ฒฝ(Vercel ๋„๋ฉ”์ธ, ๋กœ์ปฌ, EC2)์— ๋”ฐ๋ผ ํ—ˆ์šฉ Origin์ด ๋‹ฌ๋ผ์ง€๊ณ , ๋กœ๊ทธ์ธ ํ›„ Authorization ํ—ค๋”๊ฐ€ ๋ถ™์œผ๋ฉด Preflight๊นŒ์ง€ ์ถ”๊ฐ€๋กœ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์„ค์ •์— ์˜์กดํ•˜๋Š” ๊ตฌ์กฐ๋ผ ์•ˆ์ •์„ฑ์ด ๋‚ฎ์•˜์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ• 2: Vercel rewrite๋กœ ํ”„๋ก์‹œ ๊ตฌ์„ฑ (์„ ํƒ)

Vercel์˜ rewrites๋ฅผ ์ด์šฉํ•ด /api/* ์š”์ฒญ์„ ์„œ๋ฒ„์‚ฌ์ด๋“œ์—์„œ EC2๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ € ์ž…์žฅ์—์„œ๋Š” Vercel ๋„๋ฉ”์ธ ์•ˆ์—์„œ ์š”์ฒญ์ด ์™„๊ฒฐ๋˜์–ด CORS ์ž์ฒด๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์„ค์ •์ด vercel.json ํ•œ ๊ณณ์—๋งŒ ์กด์žฌํ•˜๊ณ , Nginx์™€ Spring Boot์˜ CORS ์„ค์ • ์˜์กด์„ฑ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

{
  "rewrites": [
    {
      "source": "/api/:path*",
      "destination": "https://acornpost.cloud/api/:path*"
    }
  ]
}

ํ”„๋ก ํŠธ์—”๋“œ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋„ EC2 ์ง์ ‘ ์ฃผ์†Œ์—์„œ ์ƒ๋Œ€ ๊ฒฝ๋กœ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

VITE_API_BASE_URL=/api

CORS๋ฅผ "์„ค์ •์œผ๋กœ ํ•ด๊ฒฐ"ํ•˜๋Š” ๋Œ€์‹ , ์• ์ดˆ์— ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š” ๊ตฌ์กฐ๋กœ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด ๋” ์•ˆ์ •์ ์ด๋ผ๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ

์š”์ฒญ ํ๋ฆ„์ด ๋ธŒ๋ผ์šฐ์ € โ†’ Vercel(/api) โ†’ Vercel ์„œ๋ฒ„ โ†’ EC2๋กœ ๋ฐ”๋€Œ์–ด, ๋ธŒ๋ผ์šฐ์ € ๊ธฐ์ค€ ๋™์ผ ์ถœ์ฒ˜ ์š”์ฒญ์ด ๋ฉ๋‹ˆ๋‹ค. CORS ์—๋Ÿฌ ์—†์ด ๋กœ๊ทธ์ธ ์ „ํ›„ ๋ชจ๋‘ ์ •์ƒ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค. CORS ๋ฌธ์ œ๋Š” ํ—ˆ์šฉ ์„ค์ •์„ ๋งž์ถ”๋Š” ๋ฌธ์ œ๊ฐ€ ์•„๋‹ˆ๋ผ ์š”์ฒญ ๊ตฌ์กฐ ์„ค๊ณ„์˜ ๋ฌธ์ œ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Preflight๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๋งฅ๋ฝ(ํ—ค๋” ํฌํ•จ ์—ฌ๋ถ€)์„ ๋จผ์ € ํŒŒ์•…ํ•˜๋ฉด ์›์ธ์„ ๋น ๋ฅด๊ฒŒ ์ขํž ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿš€ ํ›„์† ๊ฐœ์„  ๋ฐฉํ–ฅ

  • ํ”ผํฌ ์‹œ๊ฐ„ ๋Œ€์‘์šฉ ์Šค์ผ€์ค„๋“œ ์Šค์ผ€์ผ๋ง / ์˜คํ† ์Šค์ผ€์ผ๋ง: ํ‰์ƒ์‹œ์—๋Š” ์ ์€ ์„œ๋ฒ„ ์ˆ˜๋กœ ์šด์˜ํ•˜๊ณ , ํ‹ฐ์ผ“ ์˜คํ”ˆ ์‹œ๊ฐ„๋Œ€์—๋Š” app ์ธ์Šคํ„ด์Šค๋ฅผ ํ™•์žฅํ•˜๋Š” ์ „๋žต ๊ฒ€ํ† 
  • Polling ์ •์ฑ… ์ถ”๊ฐ€ ์ตœ์ ํ™”: ํ˜„์žฌ Adaptive Polling์„ ๋” ์„ธ๋ถ„ํ™”ํ•˜๊ฑฐ๋‚˜, jitter์™€ ๊ตฌ๊ฐ„ ์ •์ฑ…์„ ์กฐ์ •ํ•ด ํ‰๊ท  ์กฐํšŒ ๋ถ€ํ•˜๋ฅผ ์ถ”๊ฐ€๋กœ ๋‚ฎ์ถ”๋Š” ๋ฐฉํ–ฅ ๊ฒ€ํ† 

โšก๏ธ DB ์ธ๋ฑ์Šค ์ตœ์ ํ™”

์Šค์ผ€์ค„๋Ÿฌ ์ฟผ๋ฆฌ๊ฐ€ 10๋งŒ ๊ฑด ๊ธฐ์ค€ Full Table Scan โ€” ๋ณตํ•ฉ ์ธ๋ฑ์Šค๋กœ ์•ฝ 22๋ฐฐ ๊ฐœ์„ 

๋ฌธ์ œ ์˜์‹

BookingExpiryScheduler๋Š” 60์ดˆ๋งˆ๋‹ค ์•„๋ž˜ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

SELECT * FROM booking
WHERE status = 'PENDING_PAYMENT'
  AND created_at < NOW() - INTERVAL 30 MINUTE
ORDER BY created_at ASC
LIMIT 1000;

์ฒ˜์Œ์—๋Š” "์Šค์ผ€์ค„๋Ÿฌ ์ฟผ๋ฆฌ๋‹ˆ๊นŒ ์‘๋‹ต ์‹œ๊ฐ„์ด ์ข€ ๊ธธ์–ด๋„ ๊ดœ์ฐฎ๊ฒ ์ง€"๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์Œ“์ผ์ˆ˜๋ก ์ด ์ฟผ๋ฆฌ๊ฐ€ ๋ถ€๋‹ด์ด ๋œ๋‹ค๋Š” ๊ฑธ ํ™•์ธํ•˜๊ณ  ์‹ถ์—ˆ์Šต๋‹ˆ๋‹ค. 10๋งŒ ๊ฑด ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ๊ณ  EXPLAIN์„ ์‹คํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

Before โ€” EXPLAIN

EXPLAIN Before

type=ALL, key=NULL โ€” ์ธ๋ฑ์Šค๋ฅผ ์ „ํ˜€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  10๋งŒ ๊ฑด ์ „์ฒด๋ฅผ ์ฝ์€ ๋’ค ์กฐ๊ฑด ํ•„ํ„ฐ๋ง์„ ํ•ฉ๋‹ˆ๋‹ค.

Before โ€” ์‹คํ–‰ ์‹œ๊ฐ„ (5ํšŒ ํ‰๊ท )

Query Time Before

ํ‰๊ท  45~50ms.

์ธ๋ฑ์Šค ์„ค๊ณ„ ๊ณ ๋ฏผ

status ๋‹จ๋… ์ธ๋ฑ์Šค์™€ (status, created_at) ๋ณตํ•ฉ ์ธ๋ฑ์Šค ๋‘ ๊ฐ€์ง€๋ฅผ ๊ณ ๋ คํ–ˆ์Šต๋‹ˆ๋‹ค.

status ๋‹จ๋… ์ธ๋ฑ์Šค๋Š” PENDING_PAYMENT ๊ฑด๋งŒ ๊ณจ๋ผ๋‚ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ๊ทธ ์ดํ›„ created_at ๋ฒ”์œ„ ํ•„ํ„ฐ๋Š” ์—ฌ์ „ํžˆ ๋ณ„๋„ ํ•„ํ„ฐ๋ง์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ณตํ•ฉ ์ธ๋ฑ์Šค (status, created_at)๋Š” status๋กœ ๋จผ์ € ๋ฒ”์œ„๋ฅผ ์ขํžŒ ๋’ค created_at ์ˆœ์„œ๋กœ ๋ฐ”๋กœ ์ฝ์–ด๋‚˜๊ฐˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ฟผ๋ฆฌ์—์„œ status = ์กฐ๊ฑด์ด ๋“ฑ์น˜ ๋น„๊ต์ด๊ณ  created_at <๊ฐ€ ๋ฒ”์œ„ ์กฐ๊ฑด์ด๋ผ ๋ณตํ•ฉ ์ธ๋ฑ์Šค์˜ ์ปฌ๋Ÿผ ์ˆœ์„œ๊ฐ€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. status๋ฅผ ์•ž์— ๋‘๋ฉด created_at ๋ฒ”์œ„ ์Šค์บ”์„ ์ธ๋ฑ์Šค ์•ˆ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CREATE INDEX idx_booking_status_created_at ON booking(status, created_at);

After โ€” EXPLAIN

EXPLAIN After

type=range, key=idx_booking_test_status_created_at, Using index condition โ€” Index Range Scan์œผ๋กœ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค.

After โ€” ์‹คํ–‰ ์‹œ๊ฐ„ (5ํšŒ ํ‰๊ท )

Query Time After

ํ‰๊ท  1.7~2.2ms.

๊ฒฐ๊ณผ ์š”์•ฝ

ํ•ญ๋ชฉ Before After
์‹คํ–‰ ๊ณ„ํš type=ALL (Full Table Scan) type=range (Index Range Scan)
์‚ฌ์šฉ ์ธ๋ฑ์Šค NULL idx_booking_status_created_at
ํ‰๊ท  ์‹คํ–‰ ์‹œ๊ฐ„ ~45ms ~2ms

๋งŒ๋ฃŒ ์กฐํšŒ ์ฟผ๋ฆฌ(created_at < X)๋Š” ์กฐ๊ฑด ๋ฒ”์œ„๊ฐ€ ๋„“์–ด ์ธ๋ฑ์Šค ํ›„์—๋„ ์ฝ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋А ์ •๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
์–‘์ชฝ ๋ฒ”์œ„๋กœ ์ œํ•œ๋œ ์ฟผ๋ฆฌ์—์„œ๋Š” ์•ฝ 22๋ฐฐ ๊ฐœ์„ ๋„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ“‚ ์ฐธ์กฐ ๋ฌธ์„œ

  • docs/architecture.md โ€” ํŒจํ‚ค์ง€ ๊ตฌ์กฐ, Redis ํ‚ค ๋ช…์„ธ, Lua Script, Scheduler ์ƒ์„ธ
  • docs/flow.md โ€” ์˜ˆ๋งค/๋ณด์ƒ/๋งŒ๋ฃŒ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ, ์ƒํƒœ ์ „์ด
  • docs/api.md โ€” ์ „์ฒด REST ์—”๋“œํฌ์ธํŠธ ๋ชฉ๋ก
  • docs/testing.md โ€” ํ…Œ์ŠคํŠธ ์ „๋žต, ํด๋ž˜์Šค๋ณ„ ์„ค๋ช…, ์‹คํ–‰ ํ™˜๊ฒฝ

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors