// 이 파일이 하는 일: "대용량 트래픽 설계 — 밀려오는 요청 앞에서 무너지지 않기" 코스 —
// 용량 산정(봉투 뒷면 계산)에서 출발해 로드밸런싱·CDN·다층 캐시·레이트리밋·백프레셔·
// 장애 격리·종합 설계 연습까지 8개 섹션으로 안내하는 정적 학습 페이지.
// 프레이밍은 "특급 과정 — 대규모 시스템을 설계하는 시야". 실무 수준 개발자가
// 트래픽이 10배가 되어도 버티는 구조를 스스로 그릴 수 있게 만드는 것이 목표다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

import { Link } from 'react-router-dom';

// ── 예제 코드 상수들 ──

const CODE_CAPACITY = `# 봉투 뒷면 계산(back-of-envelope) — 정확한 숫자보다 '자릿수'를 맞추는 연습
DAU 100만 명, 1인당 하루 평균 요청 50회라고 하자.

  하루 총 요청  = 1,000,000 x 50        = 50,000,000 요청/일
  평균 RPS      = 50,000,000 / 86,400초  ≈ 580 RPS
  피크 RPS      = 평균 x 2~5 (서비스 특성에 따라)
                = 약 1,200 ~ 3,000 RPS

여기에 한 요청의 평균 응답 크기가 20KB라면:
  피크 대역폭   ≈ 3,000 x 20KB ≈ 60MB/s  → 480Mbps

결론: "서버 몇 대가 필요한가?"에 답하기 전에
이 네 줄 계산부터 한다. 자릿수가 틀리면 설계 전체가 틀린다.`;

const CODE_P99 = `# 왜 평균이 아니라 p99인가 — 같은 시스템의 두 얼굴
평균 응답시간   45ms   ← "빠르네요!"
p50 (중앙값)    30ms
p95            120ms
p99            900ms   ← 100명 중 1명은 거의 1초를 기다린다

하루 50회 요청하는 사용자가 p99 지연을 만날 확률:
  1 - (0.99 ^ 50) ≈ 39%
→ "1%의 느린 요청"은 사실상 세 명 중 한 명이 매일 겪는 경험이다.

그리고 페이지 하나가 API 10개를 부르면?
  전부 p99 이내로 끝날 확률 = 0.99 ^ 10 ≈ 90%
→ 열 명 중 한 명의 '화면'은 가장 느린 API 하나에 발목 잡힌다.
평균은 마케팅 숫자, p99가 사용자 경험이다.`;

const CODE_LB_ALGO = `알고리즘            동작 방식                    어울리는 상황
──────────────────────────────────────────────────────────────
라운드로빈          1번→2번→3번→1번… 순서대로     요청 비용이 비슷하고
(round robin)       공평하게 돌린다               서버 사양이 같을 때

최소 연결           지금 연결 수가 가장 적은      요청마다 처리 시간이
(least conn)        서버로 보낸다                 들쭉날쭉할 때 (긴 쿼리 등)

컨시스턴트 해싱     키(유저ID 등)를 해시해 같은   캐시 지역성이 중요할 때 —
(consistent hash)   키는 늘 같은 서버로           서버 추가/제거 시에도
                                                  이동하는 키가 1/N뿐

핵심 질문: "서버 하나가 죽거나 늘어날 때, 트래픽이 어떻게 재배치되는가?"
컨시스턴트 해싱이 나온 이유가 바로 이 질문 때문이다.`;

const CODE_HEALTHCHECK = `# nginx 업스트림 예시 — 죽은 서버로 트래픽을 보내지 않기
upstream api_servers {
  least_conn;                          # 최소 연결 알고리즘
  server 10.0.1.11:8080 max_fails=3 fail_timeout=10s;
  server 10.0.1.12:8080 max_fails=3 fail_timeout=10s;
  server 10.0.1.13:8080 max_fails=3 fail_timeout=10s;
  # 10초 안에 3번 실패하면 → 10초간 풀에서 자동 제외(passive health check)
}

# 능동(active) 헬스체크는 별도 엔드포인트를 주기적으로 찌른다
# GET /actuator/health → 200이면 살아 있음, 아니면 제외
# 주의: 헬스체크가 DB까지 찌르면, DB가 느려질 때 '멀쩡한 앱 서버'까지
# 전부 제외되어 장애가 증폭된다. 얕은 체크와 깊은 체크를 구분할 것.`;

const CODE_CACHE_CONTROL = `# 정적 자산 — "1년 캐시 + 파일명에 버전 해시"가 정석
GET /assets/app.3f9a2c.js
Cache-Control: public, max-age=31536000, immutable
# 내용이 바뀌면 해시도 바뀌어 다른 URL이 된다 → 무효화 걱정 자체가 없음

# HTML — 늘 최신 확인이 필요하니 재검증 조건부
GET /index.html
Cache-Control: no-cache            # "캐시하되, 쓰기 전에 서버에 물어봐"
ETag: "a1b2c3"
# 다음 요청: If-None-Match: "a1b2c3" → 안 바뀌었으면 304 (본문 전송 생략)

# 준(準)동적 콘텐츠 — 엣지에서 낡은 걸 먼저 주고 뒤에서 갱신
GET /api/content/articles/popular
Cache-Control: s-maxage=60, stale-while-revalidate=300
# 60초는 신선, 이후 5분간은 '낡은 응답을 즉시' 주면서 백그라운드로 재검증
# 사용자는 항상 캐시 속도를 경험하고, 원본 부하는 재검증 1회로 합쳐진다`;

const CODE_CACHE_LAYERS = `요청이 통과하는 캐시의 관문 — 앞에서 끝날수록 싸고 빠르다

  브라우저 캐시      (0ms, 네트워크 자체가 없음)
      ↓ miss
  CDN / 엣지         (~10ms, 원본 서버 부하 0)
      ↓ miss
  애플리케이션 캐시   (~1ms, Valkey/Redis — DB 부하 0)
      ↓ miss
  DB 버퍼 캐시       (디스크 대신 메모리에서 읽기)
      ↓ miss
  디스크             (여기까지 오면 제일 비싸다)

설계 목표: 트래픽의 대부분을 위쪽 두 층에서 끝내서,
가장 비싸고 늘리기 어려운 자원(DB)에는 '진짜 필요한 요청'만 도달시키기.`;

const CODE_CACHE_PATTERNS = `// cache-aside — 가장 흔한 패턴 (읽기 위주 서비스의 기본기)
String cached = valkey.get("market:quote:" + symbol);
if (cached != null) return cached;                  // ① 캐시 hit → 즉시 반환
String fresh = db.findQuote(symbol);                // ② miss → 원본 조회
valkey.setex("market:quote:" + symbol, 5, fresh);   // ③ TTL 5초로 캐시에 적재
return fresh;

// write-through: 쓸 때 캐시와 DB를 '함께' 갱신 — 캐시가 늘 최신, 쓰기 지연 증가
// write-behind:  캐시에 먼저 쓰고 DB엔 나중에 몰아서 — 빠르지만 유실 위험
// 선택 기준: 읽기:쓰기 비율. 읽기 100:1이면 cache-aside로 충분한 경우가 대부분.`;

const CODE_STAMPEDE = `// 캐시 스탬피드 — 인기 키의 TTL이 만료되는 '그 1초'에 벌어지는 일
// 초당 3,000개 요청이 전부 miss → 3,000개가 동시에 DB로 돌진 → DB 사망

// 대책 ① 잠금(단 한 명만 재계산): 첫 miss가 락을 잡고 갱신, 나머지는 잠깐 대기
boolean locked = valkey.setnx("lock:quote:" + symbol, "1", 3);
if (locked) {
  String fresh = db.findQuote(symbol);       // 나만 DB에 간다
  valkey.setex("market:quote:" + symbol, 5, fresh);
} else {
  sleepBriefly();                             // 남은 요청은 잠깐 기다렸다
  return valkey.get("market:quote:" + symbol); // 갱신된 캐시를 읽는다
}

// 대책 ② 확률적 조기 갱신: TTL이 임박할수록 높은 확률로 '미리' 재계산
//   만료를 다 같이 기다리지 않으니 만료의 절벽 자체가 사라진다
// 대책 ③ 만료 시각 분산: TTL에 랜덤 지터(5초 ± 1초)를 더해 동시 만료를 흩기`;

const CODE_RATELIMIT = `토큰 버킷 vs 슬라이딩 윈도우 — 무엇을 허용하고 무엇을 막나

토큰 버킷 (token bucket)
  양동이에 초당 r개씩 토큰이 채워지고(최대 b개), 요청 1건 = 토큰 1개 소모
  → 평소엔 초당 r로 제한하되, 모아 둔 토큰만큼 '순간 폭발(burst)'은 허용
  → API처럼 짧은 몰림이 자연스러운 트래픽에 어울린다

슬라이딩 윈도우 (sliding window)
  "최근 60초 동안 N회까지" — 시간 창을 미끄러뜨리며 정확하게 센다
  → 버스트도 엄격히 막고 싶을 때 (로그인 시도, 인증 코드 발송)

무엇을 기준으로 셀 것인가:
  IP 기준     — 비로그인 구간의 기본 방어선 (프록시 뒤 공유 IP 주의)
  유저 기준   — 로그인 후 공정한 사용량 배분
  API 키 기준 — 외부 파트너별 계약 용량(쿼터) 관리`;

const CODE_SHEDDING = `// 부하 제한(load shedding) — 다 같이 죽느니 일부를 '정중히' 거절한다
// 서버가 수용량을 넘기면: 전 요청이 다 느려짐 → 타임아웃 → 재시도 폭주 → 전멸
// 대신 넘치는 만큼만 즉시 429/503으로 돌려보내면 나머지는 정상 속도를 지킨다

if (inFlightRequests.get() > MAX_CONCURRENT) {
  response.setStatus(503);
  response.setHeader("Retry-After", "2");   // "2초 뒤에 다시 와 주세요"
  return;                                    // DB 근처에도 못 가게 초입에서 차단
}

// 거절에도 등급을 둘 수 있다:
//   헬스체크·결제 같은 핵심 요청  → 끝까지 보호
//   목록 새로고침·자동완성        → 먼저 거절 (사용자가 다시 누르면 그만)
// '무엇을 포기할지'를 평시에 정해 두는 것이 과부하 설계의 절반이다.`;

const CODE_BACKPRESSURE = `// 무제한 큐가 위험한 이유 — 큐는 문제를 없애지 않고 '숨긴다'
// 유입 1,000/s, 처리 800/s이면 큐에는 매초 200개가 쌓인다.
//   10분 뒤: 대기 12만 건, 대기 시간 2.5분 — 이미 클라이언트는 타임아웃으로 떠났고
//   서버는 '아무도 기다리지 않는 요청'을 처리하다 메모리까지 고갈된다.

// 백프레셔 = 소비 속도를 생산자에게 거슬러 올려 보내는 신호
BlockingQueue<Task> queue = new ArrayBlockingQueue<>(1000);  // ① 반드시 유한하게

boolean accepted = queue.offer(task);       // ② 가득 차면 즉시 false
if (!accepted) {
  return tooManyRequests();                 // ③ 상류에 "지금은 늦춰라"를 알린다
}

// TCP의 수신 윈도우, Kafka 컨슈머의 poll 속도, Reactive Streams의 request(n) —
// 전부 같은 원리다: 하류가 버거우면 상류가 보내는 속도를 늦춘다.`;

const CODE_CIRCUIT = `서킷 브레이커의 3상태 — 두꺼비집이 회로를 보호하듯

  닫힘(CLOSED)   평상시. 요청을 통과시키며 실패율을 계측한다.
      │  최근 10초간 실패율 > 50% (임계값)
      ▼
  열림(OPEN)     호출 자체를 차단하고 즉시 폴백으로 응답.
      │          죽어 가는 하류를 더 때리지 않고 회복할 틈을 준다.
      │  대기 시간 경과 (예: 30초)
      ▼
  반열림(HALF-OPEN)  탐색 요청 몇 개만 살짝 흘려 본다.
      │ 성공 → 닫힘 복귀        │ 실패 → 다시 열림
      ▼                         ▼

임계값 설계의 감각:
  - 표본이 적을 때(요청 10개 미만) 열리지 않게 최소 호출 수를 둔다
  - 타임아웃도 실패로 센다 — '느린 성공'이 제일 위험한 실패다`;

const CODE_K6 = `// load-test.js — k6로 게이트웨이에 단계적 부하 걸기 (이 강좌의 실습 골격)
import http from 'k6/http';
import { check } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 50 },    // 워밍업: 가상 사용자 50명까지
    { duration: '3m', target: 200 },   // 평시 부하
    { duration: '3m', target: 800 },   // 피크의 4배 — 어디서 무너지나?
    { duration: '2m', target: 0 },     // 회복 관찰
  ],
  thresholds: {
    http_req_duration: ['p(99)<500'],  // 성능 예산: p99 500ms — 관문(gate)
    http_req_failed: ['rate<0.01'],    // 실패율 1% 미만
  },
};

export default function () {
  const res = http.get('http://localhost:8080/api/market/quote?symbol=AAPL');
  check(res, { 'status 200': (r) => r.status === 200 });
}

// 실행: k6 run load-test.js
// 캐시·레이트리밋을 넣기 '전'과 '후'에 같은 스크립트를 돌려
// p99·실패율·무너지기 시작한 VU 수를 표로 비교하는 것이 과제다.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. props만 바꿔 재사용.
function Section({ n, title, sub, children }) {
  return (
    <section className="step-card" id={`sec-${n}`}>
      <div className="step-head">
        <span className="step-num">{n}</span>
        <div>
          <h3>{title}</h3>
          {sub && <span className="step-sub">{sub}</span>}
        </div>
      </div>
      <div className="step-body">{children}</div>
    </section>
  );
}

function Code({ children }) {
  return <pre className="code-block"><code>{children}</code></pre>;
}

const SECTIONS = [
  { n: 1, label: '용량 산정' },
  { n: 2, label: '로드밸런싱' },
  { n: 3, label: 'CDN과 엣지' },
  { n: 4, label: '다층 캐시' },
  { n: 5, label: '레이트리밋' },
  { n: 6, label: '백프레셔' },
  { n: 7, label: '장애 격리' },
  { n: 8, label: '종합 설계 연습' },
];

export default function Lv4HighTrafficArchitecturePage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 특급 과정</div>
        <h1>🚦 대용량 트래픽 설계<br />— 밀려오는 요청 앞에서 무너지지 않기</h1>
        <p>
          기능이 도는 것과 <strong>10배의 트래픽에서도 도는 것</strong>은 다른 문제입니다.
          로드밸런싱, CDN, 캐시 계층, 레이트리밋, 백프레셔 — 요청이 밀려올 때 시스템을
          지키는 도구들을 하나의 지도로 엮어, <strong>어느 층에서 무엇으로 버틸지</strong>를
          설계하는 시야를 기릅니다. 특급 과정의 핵심은 코드 한 줄이 아니라 이 시야예요.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 90분</span>
          <span className="chip">실습: k6 부하 테스트 → p99 전후 비교</span>
          <span className="chip">대상: 실무 수준 개발자</span>
        </div>
      </div>

      {/* 섹션 바로가기 */}
      <nav className="pill-nav">
        {SECTIONS.map((s) => (
          <a key={s.n} href={`#sec-${s.n}`}>
            <b>{s.n}</b>
            {s.label}
          </a>
        ))}
      </nav>

      <Section n={1} title="부하를 숫자로 말하기 — 용량 산정" sub="DAU에서 피크 RPS까지, 봉투 뒷면 계산">
        <p>
          대용량 설계의 첫걸음은 화려한 아키텍처 그림이 아니라 <strong>숫자</strong>입니다.
          "많이 들어와요"는 설계의 입력이 될 수 없어요. DAU(일간 활성 사용자)에서 출발해
          평균 RPS, 그리고 <strong>피크 RPS</strong>까지 내려가는 짧은 계산 —
          이른바 <strong>봉투 뒷면 계산(back-of-envelope)</strong>이 모든 판단의 출발점입니다.
        </p>
        <Code>{CODE_CAPACITY}</Code>
        <p>
          이 계산의 목적은 정밀함이 아니라 <strong>자릿수를 맞추는 것</strong>이에요.
          580 RPS인지 583 RPS인지는 중요하지 않지만, 500인지 5,000인지는 서버 대수·캐시 전략·
          DB 선택을 전부 바꿉니다. 그리고 설계는 반드시 <strong>평균이 아니라 피크</strong>에
          맞춰야 해요 — 시스템은 하루 중 가장 바쁜 1분에 무너지니까요.
        </p>
        <p>
          두 번째 숫자 감각은 <strong>지연의 분포</strong>입니다. 평균 응답시간은 소수의 아주
          느린 요청을 다수의 빠른 요청이 희석해 버려요. 그래서 실무에서는 p95·p99 같은{' '}
          <strong>백분위수(percentile)</strong>로 말합니다.
        </p>
        <Code>{CODE_P99}</Code>
        <p>
          마지막으로 <strong>읽기:쓰기 비율</strong>. 읽기가 100:1로 압도적이라면 캐시와
          복제본(replica)으로 수평 확장하기 좋은, 비교적 다루기 쉬운 문제입니다. 반대로 쓰기가
          많다면 캐시가 거의 못 돕고, 샤딩·큐잉 같은 훨씬 무거운 도구가 필요해져요. 이 비율
          하나가 아키텍처의 큰 방향을 정합니다.
        </p>
        <div className="tip">
          <b>현업의 감각</b> 설계 리뷰에서 "이거 트래픽 얼마나 봐요?"라는 질문에 30초 안에
          위의 네 줄 계산으로 답할 수 있으면, 그 순간부터 대화의 수준이 달라집니다. 숫자가
          없으면 취향 싸움이 되고, 숫자가 있으면 공학이 돼요.
        </div>
      </Section>

      <Section n={2} title="로드밸런싱 — 요청을 나누는 기술" sub="L4 vs L7, 알고리즘, 그리고 죽은 서버 걸러내기">
        <p>
          서버 한 대의 한계를 넘는 가장 기본적인 방법은 여러 대에 <strong>요청을 나누는 것</strong>
          입니다. 그 교통정리를 맡는 것이 로드밸런서(LB)인데, 어느 층에서 보느냐에 따라 둘로
          갈려요. <strong>L4(전송 계층)</strong>는 IP·포트만 보고 TCP 연결 단위로 빠르게
          분배합니다 — 내용을 안 보니 가볍고 빠르죠. <strong>L7(애플리케이션 계층)</strong>은
          HTTP를 해석해 <span className="icode">/api/market</span>은 시세 서버로,{' '}
          <span className="icode">/api/auth</span>는 인증 서버로 — <strong>경로·헤더 기반
          라우팅</strong>이 가능합니다. 우리 traderbox의 게이트웨이가 하는 일이 바로 L7
          라우팅이에요.
        </p>
        <Code>{CODE_LB_ALGO}</Code>
        <p>
          분배만큼 중요한 것이 <strong>죽은 서버를 걸러내는 일</strong>입니다. 3대 중 1대가
          죽었는데 LB가 모른다면, 사용자의 1/3은 에러를 봐요. 그래서 LB는 헬스체크로 각 서버의
          생사를 확인하고, 실패가 반복되는 서버를 풀에서 <strong>자동 제외</strong>합니다.
        </p>
        <Code>{CODE_HEALTHCHECK}</Code>
        <p>
          마지막 쟁점은 <strong>스티키 세션(sticky session)</strong> — "같은 사용자는 늘 같은
          서버로" 보내는 설정입니다. 서버 메모리에 세션을 저장하는 구조라면 필요하지만, 대가가
          커요. 특정 서버에 부하가 쏠리고, 그 서버가 죽으면 거기 붙어 있던 사용자들의 세션이
          통째로 날아갑니다. 그래서 현대적인 설계는 세션을 서버 밖(토큰 또는 Valkey 같은 외부
          저장소)으로 빼서 <strong>어느 서버가 받아도 되는 무상태(stateless)</strong>로 만들고,
          스티키 세션 자체를 없애는 쪽을 선호해요. 우리 서비스들이 JWT를 쓰는 이유이기도 합니다.
        </p>
        <div className="tip">
          <b>선택 기준 요약</b> 경로 기반 라우팅·헤더 검사·쿠키가 필요하면 L7, 극한의 처리량과
          단순 분배면 L4. 알고리즘은 "서버 증감 시 트래픽이 어떻게 재배치되는가"를 먼저 물어보고
          고르세요. 그리고 무엇을 고르든 — <strong>무상태 설계가 로드밸런싱을 가장 쉽게
          만듭니다.</strong>
        </div>
      </Section>

      <Section n={3} title="CDN과 엣지 — 요청이 서버에 오기 전에 끝내기" sub="가장 싼 요청은 서버에 도착하지 않은 요청">
        <p>
          트래픽을 버티는 가장 우아한 방법은 서버를 늘리는 게 아니라{' '}
          <strong>요청이 서버에 도착하기 전에 끝내는 것</strong>입니다. CDN(Content Delivery
          Network)은 전 세계 곳곳의 <strong>엣지 서버</strong>에 응답 사본을 두고, 사용자와
          가장 가까운 곳에서 대신 응답해요. 원본 서버 입장에선 그만큼의 트래픽이{' '}
          <strong>아예 존재하지 않는 것</strong>과 같습니다.
        </p>
        <p>
          엣지가 무엇을 얼마나 들고 있을지는 원본이 <strong>HTTP 캐시 헤더</strong>로
          지시합니다. 핵심은 세 가지 — <span className="icode">Cache-Control</span>(얼마나
          오래), <span className="icode">ETag</span>(바뀌었는지 확인), 그리고 동적 콘텐츠를 위한{' '}
          <span className="icode">stale-while-revalidate</span>(낡은 걸 먼저 주고 뒤에서 갱신).
        </p>
        <Code>{CODE_CACHE_CONTROL}</Code>
        <p>
          <span className="icode">stale-while-revalidate</span>는 특히 눈여겨보세요. "인기 글
          목록"처럼 <strong>몇 초쯤 낡아도 되는 동적 콘텐츠</strong>를 엣지에서 즉시 응답하게
          해 줍니다. 사용자는 항상 캐시의 속도를 경험하고, 원본은 백그라운드 재검증 한 번만
          감당하면 돼요. "동적이니까 캐시 불가"라는 고정관념을 깨는 패턴입니다.
        </p>
        <p>
          남는 문제는 <strong>무효화(invalidation)</strong> — 배포했는데 사용자에게 옛 파일이
          보이는 사고를 어떻게 막을까요? 정답은 둘로 갈립니다. 정적 자산은{' '}
          <strong>버전 해시 파일명</strong>(<span className="icode">app.3f9a2c.js</span>) 전략 —
          내용이 바뀌면 URL 자체가 바뀌니 무효화가 필요 없고, 1년짜리{' '}
          <span className="icode">immutable</span> 캐시를 걸 수 있어요. 반면 URL을 바꿀 수 없는
          것(이미지 교체, API 응답)은 CDN의 <strong>purge API</strong>로 지목해서 지웁니다.
          purge는 전파 지연과 실수 여지가 있으니, <strong>해시 파일명으로 해결할 수 있는 건
          전부 그쪽으로</strong> 미는 것이 정석이에요.
        </p>
        <div className="tip">
          <b>비유</b> CDN은 <strong>동네 편의점</strong>이에요. 서울 본사 창고(원본)까지 안 가도
          집 앞에서 물건을 사죠. <span className="icode">max-age</span>는 유통기한,{' '}
          <span className="icode">ETag</span>는 "본사에 전화해서 신상품 나왔는지만 확인",
          stale-while-revalidate는 "일단 진열된 걸 팔고, 그 사이 새 물건을 채워 넣기"입니다.
        </div>
      </Section>

      <Section n={4} title="다층 캐시 설계 — 브라우저부터 DB 앞까지" sub="트래픽은 위층에서 끝내고, DB엔 진짜 필요한 것만">
        <p>
          캐시는 한 군데가 아니라 <strong>여러 겹의 관문</strong>으로 설계합니다. 요청이
          지나가는 길목마다 캐시가 있고, 앞쪽에서 끝날수록 싸고 빠르죠.
        </p>
        <Code>{CODE_CACHE_LAYERS}</Code>
        <p>
          애플리케이션 캐시 층에서는 <strong>읽고 쓰는 패턴</strong>을 골라야 합니다. 가장
          흔한 것이 <strong>cache-aside</strong> — 캐시를 먼저 보고, 없으면 원본에서 가져와
          캐시에 채워 넣는 방식이에요.
        </p>
        <Code>{CODE_CACHE_PATTERNS}</Code>
        <p>
          실전 예가 우리 곁에 있습니다. traderbox의 시세 조회는 Valkey의{' '}
          <span className="icode">market:quote</span> 캐시를 먼저 봅니다. 시세는 수천 명이{' '}
          <strong>같은 종목을 동시에</strong> 조회하는 극단적 읽기 편중 데이터라, 짧은 TTL의
          캐시 한 층만으로 원본(외부 시세 API·DB)으로 가는 트래픽이 수백분의 일로 줄어요.
          "몇 초쯤 낡아도 되는가?"에 예(5초)라고 답할 수 있는 데이터였기에 가능한 설계입니다.
        </p>
        <p>
          그런데 캐시에는 유명한 함정이 있어요. <strong>캐시 스탬피드(cache stampede)</strong> —
          인기 키의 TTL이 만료되는 순간, 그동안 캐시가 막아 주던 수천 개의 요청이{' '}
          <strong>한꺼번에</strong> 원본으로 돌진하는 현상입니다. 평소엔 멀쩡하다가 "정각마다
          DB가 죽어요" 같은 미스터리 장애로 나타나죠.
        </p>
        <Code>{CODE_STAMPEDE}</Code>
        <div className="tip">
          <b>캐시 설계 3문</b> 새 캐시를 넣기 전에 스스로 물어보세요. ① 이 데이터는{' '}
          <strong>몇 초까지 낡아도 되는가?</strong>(TTL) ② 만료 순간 <strong>몇 개의 요청이
          동시에 miss</strong>가 되는가?(스탬피드 대비) ③ 원본이 바뀔 때 캐시를{' '}
          <strong>어떻게 알리는가?</strong>(무효화) — 셋 중 하나라도 답이 없으면 아직 설계가
          끝난 게 아닙니다.
        </div>
      </Section>

      <Section n={5} title="레이트리밋과 과부하 차단" sub="한계를 아는 시스템이 한계에서 살아남는다">
        <p>
          아무리 캐시를 쌓아도 수용량은 유한합니다. 그래서 성숙한 시스템은 자신의 한계를{' '}
          <strong>명시적인 규칙</strong>으로 만들어요 — "이 이상은 받지 않는다"를 미리 정해 두는
          것이 <strong>레이트리밋(rate limit)</strong>입니다. 대표 알고리즘은 두 가지예요.
        </p>
        <Code>{CODE_RATELIMIT}</Code>
        <p>
          알고리즘보다 먼저 정할 것은 <strong>무엇을 기준으로 셀 것인가</strong>입니다. 최근
          우리가 traderbox 로그인에 <strong>IP 기준 레이트리밋</strong>을 넣은 이유를 떠올려
          보세요. 로그인 전에는 "누구인지"를 모르니 유저 기준으로 셀 수 없고, 크리덴셜 스터핑
          (탈취 계정 대입 공격)은 소수의 IP에서 대량 시도로 나타나기에, <strong>비로그인
          구간에서는 IP가 사실상 유일하고 유효한 기준</strong>이기 때문입니다. 로그인 뒤에는
          유저 기준으로, 외부 파트너에겐 API 키 기준으로 — 구간마다 기준이 달라져요.
        </p>
        <p>
          레이트리밋이 "개별 클라이언트의 과속"을 막는다면, <strong>부하 제한(load
          shedding)</strong>은 "시스템 전체의 침수"를 막습니다. 수용량을 넘어선 요청을 받아서
          다 같이 느려지느니, 넘치는 만큼을 <strong>초입에서 즉시, 정중하게 거절</strong>하는
          거예요.
        </p>
        <Code>{CODE_SHEDDING}</Code>
        <div className="tip">
          <b>정중한 거절의 형식</b> 거절에도 프로토콜이 있습니다. 상태 코드는{' '}
          <span className="icode">429 Too Many Requests</span>(클라이언트 과속) 또는{' '}
          <span className="icode">503 Service Unavailable</span>(서버 과부하),{' '}
          <span className="icode">Retry-After</span> 헤더로 "언제 다시 오라"까지 알려 주세요.
          클라이언트가 이 신호를 존중해 지수 백오프로 재시도하면, 거절은 장애가 아니라{' '}
          <strong>시스템이 숨을 고르는 방법</strong>이 됩니다.
        </div>
      </Section>

      <Section n={6} title="백프레셔 — 하류가 버거우면 상류를 늦춰라" sub="큐는 문제를 없애지 않는다, 숨길 뿐">
        <p>
          처리 속도보다 유입이 빠를 때 흔한 대응이 "중간에 큐를 두자"입니다. 짧은 스파이크엔
          유효하지만, 유입이 <strong>지속적으로</strong> 처리량을 넘는다면 무제한 큐는 문제를
          해결하는 게 아니라 <strong>연기하며 키우는 것</strong>이에요. 대기 시간은 무한히
          늘어나고(지연 폭발), 큐 자체가 메모리를 삼키다 프로세스를 쓰러뜨립니다.
        </p>
        <Code>{CODE_BACKPRESSURE}</Code>
        <p>
          <strong>백프레셔(backpressure)</strong>는 이 흐름을 거꾸로 세우는 원리입니다 —
          소비자(하류)의 처리 속도를 생산자(상류)에게 <strong>신호로 거슬러 올려 보내</strong>,
          받는 쪽이 버거우면 보내는 쪽이 속도를 늦추게 하는 거예요. TCP의 수신 윈도우, Kafka
          컨슈머가 자기 속도로 <span className="icode">poll</span>하는 구조, Reactive Streams의{' '}
          <span className="icode">request(n)</span> — 겉모습은 달라도 전부 같은 원리입니다.
        </p>
        <p>
          스트리밍에서는 이 문제가 더 예민해집니다. traderbox는 시세를 SSE로 흘려보내는데,
          프론트 설정에 <span className="icode">compress: false</span>가 박혀 있어요. 중간
          계층(Next.js·프록시)의 gzip 압축과 버퍼링은 이벤트를 <strong>모았다가 한꺼번에</strong>{' '}
          내보내려 하는데, 실시간 스트림에서 그 버퍼링은 곧 지연이고, 흐름 신호를 왜곡하기
          때문입니다. 또 하나의 고전 문제는 <strong>느린 클라이언트</strong> — 수신이 느린
          구독자 한 명 때문에 서버가 그 연결의 송신 버퍼에 데이터를 무한정 쌓아 주면, 한 명이
          전체 서버의 메모리를 위협해요. 그래서 스트리밍 서버는 연결별 버퍼에 상한을 두고,
          못 따라오는 클라이언트는 <strong>이벤트를 건너뛰게 하거나 연결을 끊는</strong> 선택을
          설계에 포함해야 합니다.
        </p>
        <div className="tip">
          <b>설계 감각</b> 시스템 어딘가에 큐가 보이면 반사적으로 물어보세요 — "이 큐,{' '}
          <strong>상한이 있나? 가득 차면 무슨 일이 일어나나?</strong>" 이 두 질문에 답이 없는
          큐는 아직 설계되지 않은 장애 지점입니다. '거절'과 '버림(drop)'도 어엿한 설계
          선택지예요 — 무너지는 것보다 낫습니다.
        </div>
      </Section>

      <Section n={7} title="장애 격리 패턴 — 서킷 브레이커와 벌크헤드" sub="한 곳의 불이 건물 전체로 번지지 않게">
        <p>
          분산 시스템에서 진짜 무서운 것은 장애 자체가 아니라 <strong>장애의 전염</strong>
          입니다. 추천 서비스 하나가 느려졌을 뿐인데, 그걸 호출하며 기다리던 스레드들이 쌓여
          주문 서버까지 마비되는 식이죠. 이를 끊는 첫 번째 도구가{' '}
          <strong>서킷 브레이커(circuit breaker)</strong> — 집의 두꺼비집처럼, 하류 호출의
          실패율이 임계값을 넘으면 <strong>회로를 열어 호출 자체를 차단</strong>합니다.
        </p>
        <Code>{CODE_CIRCUIT}</Code>
        <p>
          두 번째 도구는 <strong>벌크헤드(bulkhead)</strong> — 배의 격벽에서 온 이름입니다.
          한 구획에 물이 차도 배 전체는 가라앉지 않도록, <strong>스레드풀·커넥션풀을 기능별로
          나눠</strong> 두는 거예요. 예컨대 외부 시세 API 호출용 스레드 20개, 결제용 30개를
          분리해 두면, 시세 API가 멈춰 스레드 20개가 모두 잠겨도 결제는 멀쩡히 돕니다. 나누지
          않았다면? 공용 풀 전체가 시세 대기에 잠식되어 <strong>아무 상관 없는 기능까지 함께</strong>{' '}
          죽었을 거예요.
        </p>
        <p>
          차단했다면 그 자리에 <strong>무엇을 줄지</strong>가 남습니다 — 폴백(fallback) 설계예요.
          선택지는 세 가지: ① <strong>캐시된 값</strong>("5분 전 기준 데이터입니다" — 낡음을
          표시하고 제공), ② <strong>기본값</strong>(추천 실패 시 빈 목록 대신 인기 목록),
          ③ <strong>정중한 에러</strong>("잠시 후 다시 시도해 주세요"). 기준은 데이터의
          성격입니다 — 시세처럼 낡아도 참고가 되는 건 캐시 폴백이 좋지만, <strong>결제 잔액을
          낡은 캐시로 보여 주는 건 재앙</strong>이에요. 차라리 정직한 에러가 낫습니다.
        </p>
        <div className="tip">
          <b>세 패턴은 세트다</b> 타임아웃(무한 대기 금지) → 서킷 브레이커(반복 실패 시 차단)
          → 벌크헤드(번짐 방지) → 폴백(차단 시의 응답). Spring 생태계에선 Resilience4j가 이
          네 가지를 애너테이션으로 제공합니다. 도구는 쉬워요 — 어려운 건{' '}
          <strong>임계값과 폴백을 기능별로 '결정'하는 일</strong>이고, 그게 설계자의 몫입니다.
        </div>
      </Section>

      <Section n={8} title="종합 설계 연습 — 트래픽 10배 시나리오" sub="어느 층부터 무너지는지 추적하고, 관문을 세워라">
        <p>
          이제 배운 것을 한 판에 올려 봅시다. 시나리오: 우리 서비스가 방송을 타서 트래픽이{' '}
          <strong>순식간에 10배</strong>가 됐습니다. 어디부터 무너질까요? 층별로 추적해 보면 —
          ① <strong>DB가 먼저</strong>입니다. 커넥션풀이 마르고 쿼리가 밀리죠(캐시가 없다면).
          ② 다음은 <strong>앱 서버</strong> — DB를 기다리는 스레드가 쌓여 스레드풀 고갈.
          ③ 그다음 <strong>큐와 메모리</strong> — 밀린 요청이 쌓이며 지연 폭발. 무너짐은 늘{' '}
          <strong>가장 늘리기 어려운 자원(DB)에서 시작해 위로 번집니다.</strong> 그래서 방어는
          역순이에요: 캐시·CDN으로 DB 도달량을 줄이고, 레이트리밋·load shedding으로 초입에서
          수문을 조절하고, 백프레셔·벌크헤드로 번짐을 끊는 것.
        </p>
        <p>
          여기서 특급 개발자의 구분법 하나 — <strong>돈으로 살 수 있는 것과 설계로만 되는
          것</strong>을 가르는 눈입니다. 무상태 앱 서버의 부족은 스케일아웃, 즉 돈으로 해결돼요
          (인스턴스를 늘리면 됩니다). 하지만 <strong>핫키(hot key)</strong> — 전 국민이 같은
          인기 종목 하나를 조회하는 상황 — 는 서버를 100대로 늘려도 그 키를 가진 캐시 노드
          한 대에 부하가 쏠립니다(로컬 캐시 복제나 키 분산 같은 <strong>설계</strong>가 필요).{' '}
          <strong>단일 리더 쓰기</strong>도 마찬가지 — 쓰기는 리더 한 대로 모이는 구조라면 읽기
          복제본을 아무리 늘려도 쓰기 처리량은 늘지 않아요(샤딩이라는 설계 결정이 필요).
          스케일아웃 버튼이 안 통하는 지점을 미리 알아보는 것, 그게 설계 리뷰의 핵심입니다.
        </p>
        <p>
          마지막 조각은 이 모든 것을 <strong>배포 전 관문(gate)</strong>으로 만드는 일입니다.
          "p99 500ms 이하, 실패율 1% 미만"처럼 <strong>성능 예산</strong>을 숫자로 정하고,
          부하 테스트가 그 예산을 어기면 배포가 멈추게 하는 거예요. 성능이 "나중에 볼 일"이
          아니라 테스트처럼 <strong>매번 통과해야 하는 조건</strong>이 됩니다.
        </p>
        <Code>{CODE_K6}</Code>
        <p>
          위 스크립트가 이 강좌의 실습 골격입니다. <strong>① 현재 상태</strong>의 게이트웨이에
          단계적 부하를 걸어 p99가 치솟기 시작하는 VU(가상 사용자) 수 — 즉{' '}
          <strong>무너지는 지점</strong>을 찾고, <strong>② 캐시와 레이트리밋을 추가</strong>한 뒤
          같은 스크립트를 다시 돌려, 전후의 p99·실패율·한계 VU를 <strong>표로 비교하는
          리포트</strong>를 작성하세요. "캐시를 넣으니 좋아졌다"가 아니라 "한계 VU가 200에서
          650으로, p99가 1.8s에서 320ms로"라고 말하는 것 — 그것이 이 과정 전체가 기른
          근육입니다.
        </p>
        <div className="tip">
          <b>실습 체크리스트</b> ① 부하 대상은 반드시 <strong>로컬/개발 환경</strong>(운영에
          k6를 쏘지 마세요!) ② 단계(stages)를 줄여 가며 무너지는 지점을 두 번 이상 재현해
          우연이 아님을 확인 ③ 전후 비교는 <strong>같은 스크립트·같은 환경</strong>에서 —
          조건이 다르면 비교가 아니라 착시입니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🚦 여기까지 왔다면</h3>
          <p className="muted">
            이제 "트래픽이 늘면 어떡하죠?"라는 막연한 불안 대신, <strong>층별 지도</strong>를
            갖게 됐어요. 숫자로 부하를 말하고(용량 산정), 요청을 나누고(로드밸런싱), 서버에
            오기 전에 끝내고(CDN·다층 캐시), 한계를 규칙으로 만들고(레이트리밋·load shedding),
            흐름을 거슬러 신호를 보내고(백프레셔), 번짐을 끊는(서킷 브레이커·벌크헤드) —
            열 배의 트래픽 앞에서 꺼내 들 도구들이 손에 잡힙니다. 그런데 이 설계대로 서버를
            늘리고 줄이는 일을 <strong>매번 사람이 손으로</strong> 할 수는 없겠죠. 다음{' '}
            <Link to="/learn/lv4-kubernetes-cloud-native"><strong>쿠버네티스와 클라우드
            네이티브 — 선언하면 알아서 굴러가는 인프라</strong></Link> 강좌에서, 오늘 그린
            아키텍처를 선언 한 줄로 스스로 굴러가게 만드는 법을 이어서 익혀 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
