ํฌ์ŠคํŠธ

๐Ÿ”Œโšก Circuit Breaker + ์บ์‹œ ๊ธฐ๋ฐ˜ ํ—ฌ์Šค์ฒดํฌ: ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•ˆ์ •์„ฑ์˜ ์™„๋ฒฝํ•œ ์กฐํ•ฉ

๐Ÿ“– ๋ฐฐ๊ฒฝ: ์™œ ์ด ์กฐํ•ฉ์ด ํ•„์š”ํ–ˆ๋‚˜?

Bible Card SaaS๋Š” 8๊ฐœ์˜ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋กœ ๊ตฌ์„ฑ๋œ ๋ณต์žกํ•œ ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. ๊ฐ ์„œ๋น„์Šค๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ์šด์˜๋˜๋ฉด์„œ๋„ ์„œ๋กœ ํ†ต์‹ ํ•ด์•ผ ํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ, ๋‹จ์ผ ์„œ๋น„์Šค์˜ ์žฅ์• ๊ฐ€ ์ „์ฒด ์‹œ์Šคํ…œ์„ ๋งˆ๋น„์‹œํ‚ค๋Š” ์—ฐ์‡„ ์žฅ์• (Cascade Failure)๊ฐ€ ๊ฐ€์žฅ ํฐ ์œ„ํ—˜ ์š”์†Œ์˜€์Šต๋‹ˆ๋‹ค.

์ดˆ๊ธฐ ๋ฌธ์ œ์ ๋“ค

  • JWT ๊ฒ€์ฆ ์‹คํŒจ ์‹œ 5์ดˆ ๋Œ€๊ธฐ โ†’ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์•…ํ™”
  • ์ด๋ฏธ์ง€ ์ƒ์„ฑ ์„œ๋น„์Šค ์žฅ์•  ์‹œ ์ „์ฒด ์‹œ์Šคํ…œ ๋‹ค์šด
  • ํ—ฌ์Šค์ฒดํฌ๊ฐ€ ๋ณ‘๋ชฉ์ด ๋˜์–ด ๋ชจ๋‹ˆํ„ฐ๋ง ์ž์ฒด๊ฐ€ ์‹œ์Šคํ…œ ๋ถ€ํ•˜ ์ฆ๊ฐ€
  • ๊ตฌ๊ธ€ ๋กœ๊ทธ์ธ์—์„œ โ€œCannot serialize non-str key Noneโ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ

๐Ÿ›ก๏ธ Circuit Breaker: ์ „๊ธฐ ์ฐจ๋‹จ๊ธฐ์—์„œ ๋ฐฐ์šด ์ง€ํ˜œ

ํ•ต์‹ฌ ์•„์ด๋””์–ด

์‹ค์ œ ์ „๊ธฐ ์ฐจ๋‹จ๊ธฐ๊ฐ€ ๊ณผ๋ถ€ํ•˜ ์‹œ ์ „๊ธฐ๋ฅผ ์ฐจ๋‹จํ•ด์„œ ํ™”์žฌ๋ฅผ ๋ฐฉ์ง€ํ•˜๋“ฏ์ด, ์†Œํ”„ํŠธ์›จ์–ด์—์„œ๋„ ์„œ๋น„์Šค ์žฅ์•  ์‹œ ์š”์ฒญ์„ ์ฐจ๋‹จํ•ด์„œ ์—ฐ์‡„ ์žฅ์• ๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

3๊ฐ€์ง€ ์ƒํƒœ๋กœ ๋™์ž‘

๐ŸŸข CLOSED (์ •์ƒ) โ†’ ๋ชจ๋“  ์š”์ฒญ ํ—ˆ์šฉ ๐Ÿ”ด OPEN (์ฐจ๋‹จ) โ†’ ๋ชจ๋“  ์š”์ฒญ ์ฆ‰์‹œ ๊ฑฐ๋ถ€ ๐ŸŸก HALF_OPEN (ํ…Œ์ŠคํŠธ) โ†’ ์ผ๋ถ€ ์š”์ฒญ์œผ๋กœ ๋ณต๊ตฌ ํ™•์ธ

์‹ค์ œ ๊ตฌํ˜„ ์˜ˆ์‹œ

JWT ๊ฒ€์ฆ์„ Circuit Breaker๋กœ ๋ณดํ˜ธ

user_info = await with_circuit_breaker( name=โ€œjwt_verificationโ€, func=jwt_verification, config=CircuitConfig( failure_threshold=5, # 5๋ฒˆ ์‹คํŒจ์‹œ ์ฐจ๋‹จ timeout_duration=30.0, # 30์ดˆ ํ›„ ์žฌ์‹œ๋„ call_timeout=2.0 # 2์ดˆ ํƒ€์ž„์•„์›ƒ ) )

๋†€๋ผ์šด ํšจ๊ณผ

  • Before: JWT ๊ฒ€์ฆ ์‹คํŒจ ์‹œ 5์ดˆ ๋Œ€๊ธฐ
  • After: ์žฅ์•  ๊ฐ์ง€ ์‹œ ์ฆ‰์‹œ 503 ์‘๋‹ต (5ms)
  • ๊ฒฐ๊ณผ: ์‚ฌ์šฉ์ž๋Š” ๋น ๋ฅธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›๊ณ , ์„œ๋ฒ„๋Š” ๋ฆฌ์†Œ์Šค ๋ณดํ˜ธ

๐Ÿฅ ํ—ฌ์Šค์ฒดํฌ์˜ ํ•จ์ •: ์˜์‚ฌ๊ฐ€ ํ™˜์ž๋ฅผ ๋” ์•„ํ”„๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค

๊ธฐ์กด ํ—ฌ์Šค์ฒดํฌ์˜ ๋ฌธ์ œ

โŒ ์œ„ํ—˜ํ•œ ํ—ฌ์Šค์ฒดํฌ

@router.get(โ€œ/healthโ€) async def bad_health_check():

Prometheus๊ฐ€ 15์ดˆ๋งˆ๋‹ค ์ด๊ฑธ ํ˜ธ์ถœํ•˜๋ฉดโ€ฆ

db_status = await db.execute(โ€œSELECT 1โ€) # DB ๋ถ€ํ•˜! paddle_status = await paddle_api.get_products() # ์™ธ๋ถ€ API ๋ถ€ํ•˜! redis_status = await redis.ping() # Redis ๋ถ€ํ•˜!

return {โ€œstatusโ€: โ€œokโ€ if all([db_status, paddle_status, redis_status]) else โ€œfailโ€}

๋ฌธ์ œ์ : ํ—ฌ์Šค์ฒดํฌ ์ž์ฒด๊ฐ€ ์‹œ์Šคํ…œ์— ๋ถ€ํ•˜๋ฅผ ์ฃผ์–ด โ€œ๊ฑด๊ฐ•์„ ํ™•์ธํ•˜๋ ค๋‹ค๊ฐ€ ๋ณ‘์„ ๋งŒ๋“œ๋Š”โ€ ์ƒํ™ฉ ๋ฐœ์ƒ

์บ์‹œ ๊ธฐ๋ฐ˜ ํ•ด๊ฒฐ์ฑ…

โœ… ์•ˆ์ „ํ•œ ์บ์‹œ ๊ธฐ๋ฐ˜ ํ—ฌ์Šค์ฒดํฌ

class HealthChecker: def init(self): self.health_cache = {} self.last_check_time = {} self.check_interval = 30 # 30์ดˆ๋งˆ๋‹ค๋งŒ ์‹ค์ œ ์ฒดํฌ

async def get_component_health(self, component: str, check_func): now = time.time()

์บ์‹œ๊ฐ€ ์œ ํšจํ•˜๋ฉด ์ฆ‰์‹œ ๋ฐ˜ํ™˜

if (now - self.last_check_time.get(component, 0)) self.check_interval: return self.health_cache.get(component)

์‹ค์ œ ์ฒดํฌ๋Š” 30์ดˆ๋งˆ๋‹ค๋งŒ

try: result = await asyncio.wait_for(check_func(), timeout=2.0) self.health_cache[component] = {โ€œstatusโ€: โ€œhealthyโ€, โ€œlast_checkโ€: now} except Exception as e: self.health_cache[component] = {โ€œstatusโ€: โ€œunhealthyโ€, โ€œerrorโ€: str(e)}

self.last_check_time[component] = now return self.health_cache[component]

์„ฑ๋Šฅ ๊ฐœ์„  ๊ฒฐ๊ณผ

  • ์‘๋‹ต์‹œ๊ฐ„: 2000ms โ†’ 200ms (10๋ฐฐ ํ–ฅ์ƒ)
  • ์‹œ์Šคํ…œ ๋ถ€ํ•˜: 95% ๊ฐ์†Œ
  • ๋ชจ๋‹ˆํ„ฐ๋ง ์•ˆ์ „์„ฑ: Prometheus๊ฐ€ ์•„๋ฌด๋ฆฌ ์ž์ฃผ ํ˜ธ์ถœํ•ด๋„ ์•ˆ์ „

๐Ÿ”„ Circuit Breaker + ์บ์‹œ ํ—ฌ์Šค์ฒดํฌ์˜ ์‹œ๋„ˆ์ง€

์™„๋ฒฝํ•œ ์กฐํ•ฉ์ธ ์ด์œ 

  1. Circuit Breaker: ์žฅ์•  ๋ฐœ์ƒ ์‹œ ๋น ๋ฅธ ์ฐจ๋‹จ
  2. ์บ์‹œ ํ—ฌ์Šค์ฒดํฌ: ์žฅ์•  ๊ฐ์ง€๋ฅผ ์œ„ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ์‹œ์Šคํ…œ์— ๋ถ€ํ•˜ ์—†์Œ
  3. ์ƒํ˜ธ ๋ณด์™„: ์„œ๋กœ์˜ ์•ฝ์ ์„ ๋ณด์™„ํ•˜๋Š” ์™„๋ฒฝํ•œ ํŒŒํŠธ๋„ˆ์‹ญ

์‹ค์ œ ์žฅ์•  ์‹œ๋‚˜๋ฆฌ์˜ค

09:00 - ๋ชจ๋“  ์„œ๋น„์Šค ์ •์ƒ ๐ŸŸข โ”œโ”€โ”€ Circuit Breaker: CLOSED โ”œโ”€โ”€ ํ—ฌ์Šค์ฒดํฌ: ์บ์‹œ๋œ ๊ฒฐ๊ณผ๋กœ ๋น ๋ฅธ ์‘๋‹ต โ””โ”€โ”€ ์‚ฌ์šฉ์ž: ์ •์ƒ ์„œ๋น„์Šค ์ด์šฉ

09:05 - Auth Service ์žฅ์•  ๋ฐœ์ƒ ๐Ÿ’ฅ โ”œโ”€โ”€ Circuit Breaker: 3๋ฒˆ ์‹คํŒจ ํ›„ OPEN ์ „ํ™˜ โ”œโ”€โ”€ ํ—ฌ์Šค์ฒดํฌ: ์บ์‹œ๋กœ ์žฅ์•  ์ƒํƒœ ๋น ๋ฅด๊ฒŒ ์ „ํŒŒ โ””โ”€โ”€ ์‚ฌ์šฉ์ž: ์ฆ‰์‹œ โ€œ์„œ๋น„์Šค ๋ถˆ๊ฐ€โ€ ๋ฉ”์‹œ์ง€ (๋Œ€๊ธฐ ์‹œ๊ฐ„ ์—†์Œ)

09:35 - 30์ดˆ ํ›„ ์ž๋™ ๋ณต๊ตฌ ์‹œ๋„ ๐Ÿ”„ โ”œโ”€โ”€ Circuit Breaker: HALF_OPEN์œผ๋กœ ์ „ํ™˜ โ”œโ”€โ”€ ํ—ฌ์Šค์ฒดํฌ: ์‹ค์ œ ์ฒดํฌ๋กœ ๋ณต๊ตฌ ํ™•์ธ โ””โ”€โ”€ ๋ณต๊ตฌ ์„ฑ๊ณต ์‹œ CLOSED๋กœ ์ „ํ™˜

09:36 - ์ •์ƒ ์„œ๋น„์Šค ์žฌ๊ฐœ โœ…

๐Ÿ“Š ์‹ค์ธก ์„ฑ๋Šฅ ๋ฐ์ดํ„ฐ

Circuit Breaker ํšจ๊ณผ

Before Circuit Breaker

$ time curl http://localhost:8001/api/auth/me

5.234s (JWT ๊ฒ€์ฆ ์‹คํŒจ ์‹œ)

After Circuit Breaker

$ time curl http://localhost:8001/api/auth/me

0.005s (์ฆ‰์‹œ 503 ์‘๋‹ต)

์บ์‹œ ํ—ฌ์Šค์ฒดํฌ ํšจ๊ณผ

1์ฐจ ํ˜ธ์ถœ (์‹ค์ œ ์ฒดํฌ)

$ time curl http://localhost:8001/api/gateway/health/full

2.5s - ๋ชจ๋“  ์„œ๋น„์Šค ์‹ค์ œ ํ˜ธ์ถœ

2์ฐจ ํ˜ธ์ถœ (์บ์‹œ ์‚ฌ์šฉ)

$ time curl http://localhost:8001/api/gateway/health/full

0.2s - ์บ์‹œ๋œ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜

Circuit Breaker ํ†ต๊ณ„

{ โ€œjwt_verificationโ€: { โ€œstateโ€: โ€œclosedโ€, โ€œtotal_callsโ€: 1000, โ€œtotal_failuresโ€: 12, โ€œfailure_rateโ€: 0.012, โ€œcurrent_failure_countโ€: 0 } }

๐ŸŽฏ Bible Card SaaS์—์„œ์˜ ์‹ค์ œ ์ ์šฉ

๋ณดํ˜ธ ๋Œ€์ƒ

  1. JWT ๊ฒ€์ฆ: ์ธ์ฆ ์„œ๋น„์Šค ์žฅ์•  ์‹œ์—๋„ ๋น ๋ฅธ ์‘๋‹ต
  2. ์„œ๋น„์Šค ๊ฐ„ ํ˜ธ์ถœ: Gateway โ†’ Auth, Payment, BibleCards ๋“ฑ
  3. ์™ธ๋ถ€ API: Paddle ๊ฒฐ์ œ, OpenAI ์ด๋ฏธ์ง€ ์ƒ์„ฑ
  4. ํ—ฌ์Šค์ฒดํฌ: ๋ชจ๋‹ˆํ„ฐ๋ง์ด ์‹œ์Šคํ…œ ๋ถ€ํ•˜ ์—†์ด ๋™์ž‘

์‹ค์ œ ์ฝ”๋“œ ์œ„์น˜

backend_gateway/ โ”œโ”€โ”€ services/circuit_breaker.py # Circuit Breaker ๊ตฌํ˜„ โ”œโ”€โ”€ services/unified_proxy.py # JWT ๊ฒ€์ฆ ๋ณดํ˜ธ โ”œโ”€โ”€ services/proxy.py # ์„œ๋น„์Šค ํ˜ธ์ถœ ๋ณดํ˜ธ โ”œโ”€โ”€ services/health_check.py # ์บ์‹œ ๊ธฐ๋ฐ˜ ํ—ฌ์Šค์ฒดํฌ โ””โ”€โ”€ api/gateway.py # ํ†ต๊ณ„ API ์ œ๊ณต

๐Ÿ† ๊ฒฐ๋ก : ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•ˆ์ •์„ฑ์˜ ์ƒˆ๋กœ์šด ํ‘œ์ค€

์ด ์กฐํ•ฉ์ด ํŠน๋ณ„ํ•œ ์ด์œ 

  1. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜: ์žฅ์•  ์‹œ์—๋„ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ
  2. ์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ: ์—ฐ์‡„ ์žฅ์•  ์™„์ „ ์ฐจ๋‹จ
  3. ์šด์˜ ํšจ์œจ์„ฑ: ์ž๋™ ๋ณต๊ตฌ๋กœ ์ˆ˜๋™ ๊ฐœ์ž… ์ตœ์†Œํ™”
  4. ๋ชจ๋‹ˆํ„ฐ๋ง ์•ˆ์ „์„ฑ: ๊ฑด๊ฐ• ํ™•์ธ์ด ๊ฑด๊ฐ•์„ ํ•ด์น˜์ง€ ์•Š์Œ

ํ™•์žฅ ๊ฐ€๋Šฅ์„ฑ

  • Retry Pattern: Circuit Breaker์™€ ์กฐํ•ฉ์œผ๋กœ ๋” ๊ฐ•๋ ฅํ•œ ๋ณต์›๋ ฅ
  • Bulkhead Pattern: ๋ฆฌ์†Œ์Šค ๊ฒฉ๋ฆฌ๋กœ ์žฅ์•  ์˜ํ–ฅ ๋ฒ”์œ„ ์ตœ์†Œํ™”
  • Cache-Aside Pattern: ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ์„ฑ๋Šฅ ๊ทน๋Œ€ํ™”

์ตœ์ข… ๋ฉ”์‹œ์ง€

โ€œCircuit Breaker + ์บ์‹œ ํ—ฌ์Šค์ฒดํฌ๋Š” ๋‹จ์ˆœํ•œ ํŒจํ„ด ์กฐํ•ฉ์ด ์•„๋‹ˆ๋ผ, ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์‹œ๋Œ€์˜ ์ƒ์กด ์ „๋žต์ž…๋‹ˆ๋‹ค.โ€

ํ˜„๋Œ€์˜ ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์—์„œ ์žฅ์• ๋Š” ์–ธ์ œ๋“  ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ๊ฒƒ์€ ์žฅ์• ๋ฅผ ์˜ˆ๋ฐฉํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์‹œ์Šคํ…œ์ด ์–ด๋–ป๊ฒŒ ๋ฐ˜์‘ํ•˜๋А๋ƒ์ž…๋‹ˆ๋‹ค.

Circuit Breaker๋Š” ๋น ๋ฅธ ์‹คํŒจ๋ฅผ, ์บ์‹œ ๊ธฐ๋ฐ˜ ํ—ฌ์Šค์ฒดํฌ๋Š” ์•ˆ์ „ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋‘˜์˜ ์กฐํ•ฉ์€ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๊ฐ€ ์„œ๋กœ๋ฅผ ๋ณดํ˜ธํ•˜๋ฉด์„œ๋„ ์ „์ฒด ์‹œ์Šคํ…œ์˜ ๊ฐ€์šฉ์„ฑ์„ ์ตœ๋Œ€ํ™”ํ•˜๋Š” ์™„๋ฒฝํ•œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค.


โ€œ์ข‹์€ ์•„ํ‚คํ…์ฒ˜๋Š” ์žฅ์• ๋ฅผ ์ˆจ๊ธฐ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์žฅ์• ์™€ ํ•จ๊ป˜ ์ถค์ถ”๋Š” ๊ฒƒ์ด๋‹ค.โ€ ๐Ÿ•บ๐Ÿ’ƒ


ใ…Žใ…Žใ…Žใ…Žใ…Ž ์ถค์ถฐ๋ผ ๋‚˜์˜ ์—์ด์ „ํŠธ์•ผ~

์ด ๊ธฐ์‚ฌ๋Š” ์ €์ž‘๊ถŒ์ž์˜ CC BY 4.0 ๋ผ์ด์„ผ์Šค๋ฅผ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

ยฉ SouthGlory. ์ผ๋ถ€ ๊ถŒ๋ฆฌ ๋ณด์œ 

Powered by Jekyll with Chirpy theme