// 이 파일이 하는 일: "시스템 설계 훈련 1 — URL 단축기를 처음부터 끝까지" 코스 —
// 가장 고전적인 설계 문제를 재료 삼아 '요구사항 → 용량 산정 → API → 데이터 모델 →
// 상세 설계 → 확장'이라는 설계 루틴을 처음부터 끝까지 한 바퀴 도는 정적 학습 페이지.
// 프레이밍은 "그림이 아니라 질문이 먼저" — 특급 과정답게 숫자로 산정하고,
// 트레이드오프를 말로 방어할 수 있는 상태를 목표로 한다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_ROUTINE = `설계 문제를 받았을 때의 루틴 — 면접이든 실무든 순서는 같다

① 요구사항 명확화   "무엇을, 얼마나, 얼마나 빨리?"  ← 여기가 절반
② 용량 산정        RPS · 저장 용량 · 대역폭을 숫자로
③ API 설계         바깥에서 보이는 계약부터 확정
④ 데이터 모델      테이블 · 키 · 인덱스
⑤ 상세 설계        핵심 알고리즘과 컴포넌트 다이어그램
⑥ 확장과 운영      캐시 · 샤딩 · 다중 리전 · SLO

흔한 실패: ①을 건너뛰고 ⑤의 그림부터 그리기 시작하는 것.
요구사항이 다르면 정답도 다르다 — "일단 그림부터"가 아니라 "일단 질문부터".`;

const CODE_CAPACITY = `가정: 단축 생성(쓰기) 하루 100만 건, 읽기:쓰기 = 100:1

■ RPS (초당 요청 수)
쓰기  = 1,000,000 / 86,400초  ≈ 12 writes/sec
읽기  = 12 × 100              ≈ 1,200 reads/sec
피크  = 평균의 2~3배 잡으면    ≈ 3,000~4,000 reads/sec

■ 저장 용량 (레코드 크기 × 건수 × 보존 기간)
레코드 1건 ≈ 500바이트 (키 + 원본 URL + 메타데이터)
하루      = 500B × 100만        = 500MB/일
1년       = 500MB × 365         ≈ 182GB/년
5년 보존   = 182GB × 5           ≈ 1TB — 단일 DB로도 감당 가능한 크기

■ 결론
쓰기는 한가하고(12/sec), 읽기가 지배한다(수천/sec).
→ 이 "읽기 편중"이라는 특성이 이후 모든 설계 결정을 지배한다.
   캐시를 두껍게, 읽기 경로를 최단으로, 쓰기는 단순하게.`;

const CODE_BASE62 = `base62 = 0-9, a-z, A-Z (62개 문자)로 숫자를 표현하는 인코딩

키 길이별 표현 가능한 개수:
6자 → 62^6 ≈ 568억 개
7자 → 62^7 ≈ 3.5조 개   ← 하루 100만 건 × 100년을 써도 남는다

■ 방식 A: 해시 잘라 쓰기
md5(원본URL) 앞 7자 사용
장점: 무상태 — 어느 서버든 즉시 계산
단점: 충돌 — 다른 URL이 같은 7자가 될 수 있어 재시도 로직 필수

■ 방식 B: 카운터 기반 발급
전역 카운터 1, 2, 3... 을 base62로 변환 (1억 → "6LAze")
장점: 충돌이 원천적으로 없다
단점: 키가 순차적 → 다음 키를 예측해 남의 URL을 긁을 수 있고,
      전역 카운터 자체가 단일 장애점 + 병목

■ 방식 C: 키 생성 서비스(KGS)
키 '범위'를 서버마다 미리 떼어 준다
서버1 ← [1 ~ 100만]   서버2 ← [100만+1 ~ 200만]
각 서버는 자기 범위 안에서 로컬 카운터만 증가 — 조율 없이 발급.
서버가 죽으면 그 범위만 버린다(3.5조 개 중 100만 개쯤은 아깝지 않다).`;

const CODE_API = `■ API — 바깥에서 보이는 계약은 딱 두 개
POST /shorten
  body: 원본 URL (+ 선택: 커스텀 별칭, 만료일)
  응답: 201 Created, 단축 URL

GET /:key
  응답: 301 또는 302 + Location: 원본 URL

■ 301 vs 302 — 리다이렉트 한 줄에 숨은 트레이드오프
301 Moved Permanently
  브라우저가 결과를 캐시 → 다음부터 우리 서버를 거치지 않는다
  + 서버 부하 감소   - 클릭 집계 불가, 목적지 변경 반영 안 됨
302 Found (임시)
  매 클릭이 우리 서버를 통과
  + 클릭 분석 가능, 만료·차단 즉시 반영   - 부하는 그대로
→ 분석이 상품인 서비스(bit.ly류)는 302를 고른다. 요구사항이 결정한다.

■ 테이블 설계
urls (
  key         VARCHAR(7)  PK,   -- base62 단축 키
  original    TEXT        NOT NULL,
  created_by  BIGINT,           -- 생성자 (익명 허용 시 NULL)
  created_at  TIMESTAMPTZ NOT NULL,
  expires_at  TIMESTAMPTZ       -- NULL이면 무기한
)
조회는 오직 key 등가 조회 하나 — 조인도 범위 스캔도 없다.

■ 만료 처리 두 갈래
지연 삭제(lazy): 조회 시 expires_at 지났으면 404 주고 그때 지운다
배치 청소: 새벽마다 만료 행 일괄 삭제
→ 보통 둘 다: 정확성은 lazy가, 공간 회수는 배치가 맡는다.`;

const CODE_READPATH = `읽기 경로 — 요청 1,200/sec가 흐르는 길을 최단으로

GET /abc1234
  ① 캐시(Redis) 조회 ── 적중 ──→ 즉시 301/302 응답  (대부분 여기서 끝)
  ② 캐시 미스 → DB 조회 → 캐시에 적재 → 응답
  ③ DB에도 없음 → 404

■ 왜 캐시가 잘 먹히나
단축 URL 트래픽은 전형적인 멱법칙: 상위 20% 키가 트래픽의 80%.
전체 1TB를 다 캐시할 필요 없이, 뜨거운 20%만 메모리에 올려도
적중률 80%+ — DB가 받는 부하는 1/5 이하로 줄어든다.

■ 존재하지 않는 키 폭격 (캐시 관통, cache penetration)
공격자가 무작위 키를 대량 조회하면? 캐시엔 당연히 없으니
전부 DB로 관통 — DB가 먼저 쓰러진다. 방어 두 가지:
· 네거티브 캐시: "없음"이라는 사실도 짧은 TTL로 캐시
· 블룸 필터: "확실히 없다"를 메모리에서 판정하는 확률적 자료구조
  (있다고 하면 진짜 있을 수도/없을 수도, 없다고 하면 확실히 없음)

■ 극단: CDN 레벨 리다이렉트
리다이렉트 응답 자체를 엣지(CDN)에 캐시하면
요청이 아예 우리 서버에 도착하지도 않는다.
단, 302로 분석을 하기로 했다면 이 최적화와 충돌 — 또 트레이드오프.`;

const CODE_SCALE = `■ 키 기반 샤딩 — 앞 강좌의 샤드 키 논의를 그대로 적용
shard = hash(key) % N
· key는 균등 분포(base62 랜덤/범위 발급) → 핫스팟 없음
· 조회가 전부 key 등가 조회 → 크로스 샤드 쿼리가 아예 없다
→ URL 단축기는 샤딩하기 '가장 쉬운' 워크로드다.

■ 다중 리전 — 어느 지역 유저가 어느 DB를 치나
서울 유저가 미국 DB를 치면 왕복 150ms+ — 리다이렉트가 눈에 띄게 느리다.
· 읽기: 리전마다 읽기 복제본(replica) + 캐시 → 가까운 곳에서 응답
· 쓰기: 생성은 상대적으로 드물다(12/sec) → 단일 프라이머리로 몰아도
  복제 지연 몇 초는 허용 가능 ("방금 만든 링크가 지구 반대편에서
  1초 늦게 열린다" — 대개 받아들일 수 있는 결과 정합성)

■ SLO — 단축기가 죽으면 '모든 링크'가 죽는다
이 서비스의 장애 반경은 자기 자신이 아니라, 이 링크를 박아 둔
전 세계의 문서·메일·광고 전부다. 그래서 가용성 요구가 유독 높다.
99.9%  = 월 43분 장애 허용 — 링크 서비스엔 부족하다는 평가가 많다
99.99% = 월 4.3분 — 읽기 경로만큼은 이 수준을 노린다
힌트: 읽기(리다이렉트)와 쓰기(생성)의 SLO를 분리하라.
생성이 10분 죽는 것과 리다이렉트가 10분 죽는 것은 사고의 급이 다르다.`;

const CODE_ANALYTICS = `요구 추가: "클릭 통계를 보여 주세요" — 기존 설계를 어떻게 흔들까?

■ 후보 1: 동기 기록
리다이렉트 처리 중에 클릭 로그를 DB에 INSERT하고 응답
- 읽기 경로에 쓰기가 끼어든다 → 1,200/sec 읽기가 1,200/sec 쓰기로 둔갑
- DB가 느려지면 리다이렉트까지 느려진다 — 핵심 경로 오염

■ 후보 2: 이벤트 발행 (비동기)
리다이렉트는 즉시 응답, 클릭 이벤트만 Kafka에 발행
소비자(consumer)가 뒤에서 받아 집계 테이블에 반영
+ 읽기 경로는 그대로 빠르다, 집계가 밀려도 리다이렉트는 무사
- 통계는 몇 초~몇 분 지연 (준실시간)

■ 그래서 다시 요구사항 질문으로
"통계는 실시간이어야 하나요, 몇 분 늦어도 되나요?"
→ 대시보드용이면 준실시간으로 충분 — 후보 2 채택.
→ 기존 설계는 거의 그대로: 읽기 경로에 '이벤트 발행 한 줄'만 얹는다.
좋은 설계의 증거는 '기능 추가가 기존 구조를 크게 흔들지 않는 것'이다.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: '짧은 키 만들기' },
  { n: 4, label: 'API·데이터 모델' },
  { n: 5, label: '읽기 경로 최적화' },
  { n: 6, label: '확장과 가용성' },
  { n: 7, label: '분석 기능 추가' },
];

export default function Lv4SystemDesignUrlShortenerPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 특급 과정</div>
        <h1>🔗 시스템 설계 훈련 1<br />— URL 단축기를 처음부터 끝까지</h1>
        <p>
          긴 URL을 넣으면 <span className="icode">tdl.ink/abc1234</span> 같은 짧은 주소를
          돌려주는 서비스 — 기능만 보면 한나절 과제 같지만, <strong>하루 100만 건 생성,
          초당 수천 건 리다이렉트</strong>라는 규모가 붙는 순간 캐시·샤딩·가용성 논의가 전부
          소환됩니다. 그래서 이 문제는 시스템 설계의 가장 고전적인 훈련장이에요. 이 강좌에서{' '}
          <strong>요구사항 → 용량 산정 → API → 데이터 → 확장</strong>이라는 설계 루틴을
          처음부터 끝까지 한 바퀴 돌며 몸에 새깁니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 75분</span>
          <span className="chip">실습: 45분 화이트보드 전체 설계</span>
          <span className="chip">선수 지식: DB·캐시·샤딩 기초</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="요구사항 명확화가 절반이다">
        <p>
          설계 문제를 받으면 손이 먼저 그림을 그리고 싶어 합니다. 박스 몇 개, 화살표 몇 개 —
          그럴듯해 보이죠. 하지만 <strong>요구사항이 다르면 정답도 다릅니다.</strong> 하루
          1만 건짜리 사내 도구와 하루 1억 건짜리 글로벌 서비스는 이름만 같은 다른 문제예요.
          그래서 훈련된 설계자는 그림 대신 <strong>질문</strong>부터 꺼냅니다.
        </p>
        <p>
          질문은 두 갈래로 나눠요. <strong>기능 요구</strong>(무엇을 하나 — 단축, 리다이렉트,
          만료, 커스텀 별칭?)와 <strong>비기능 요구</strong>(어떤 품질로 — 규모는? 지연은?
          가용성은?). 설계를 실제로 좌우하는 건 대개 후자입니다. 그리고 순서는 면접이든
          실무든 같습니다.
        </p>
        <Code>{CODE_ROUTINE}</Code>
        <p>
          이 루틴의 힘은 <strong>되돌아갈 지점을 만들어 준다</strong>는 데 있어요. 뒤 단계에서
          막히면 "요구사항을 잘못 잡았나?"로 돌아가고, 기능이 추가되면(섹션 7) 다시 ①부터
          짧게 도는 겁니다. 그림은 이 루틴의 <strong>결과물</strong>이지 출발점이 아니에요.
        </p>
        <div className="tip">
          <b>실무에서도 똑같다</b> 우리 트레이딩 플랫폼에 "실시간 알림 넣어 주세요"라는 요청이
          오면, 훈련된 사람은 코드를 열기 전에 묻습니다 — "몇 명에게? 지연은 몇 초까지 허용?
          놓치면 안 되는 알림인가?" 이 세 질문의 답에 따라 구현이 폴링/SSE/푸시로 완전히
          갈라지죠. 이 강좌의 루틴이 바로 그 근육입니다.
        </div>
      </Section>

      <Section n={2} title="요구사항과 용량 산정" sub="봉투 뒷면 계산이 설계의 방향을 정한다">
        <p>
          요구사항이 잡혔으면 숫자로 바꿉니다. 이른바 <strong>봉투 뒷면 계산(back-of-the-envelope)</strong> —
          정밀할 필요는 없고, <strong>자릿수(order of magnitude)</strong>만 맞으면 됩니다.
          우리의 가정: 단축 생성(쓰기)이 하루 100만 건, 읽기:쓰기 비율은 100:1.
        </p>
        <Code>{CODE_CAPACITY}</Code>
        <p>
          계산에서 두 가지 사실이 드러났어요. 첫째, <strong>저장 용량은 문제가 아닙니다.</strong>{' '}
          5년치가 1TB — 요즘 DB 한 대로 충분한 크기죠. 둘째, <strong>읽기가 쓰기의 100배</strong>입니다.
          초당 12건의 쓰기는 한가하지만, 피크 수천 건의 읽기는 진짜 부하예요. 그러니 이 시스템의
          설계 에너지는 온통 <strong>읽기 경로</strong>에 쏟아야 합니다 — 캐시는 두껍게(섹션 5),
          리다이렉트는 최단 거리로, 쓰기는 단순하게.
        </p>
        {/* 학습 포인트: 산정의 목적은 정확한 숫자가 아니라 '설계 결정의 근거'다.
            "읽기 편중 → 캐시 중심" 같은 문장으로 연결되지 않는 계산은 장식일 뿐이다. */}
        <div className="tip">
          <b>산정이 틀리면?</b> 괜찮습니다. 봉투 뒷면 계산의 목적은 예언이 아니라{' '}
          <strong>방향 결정</strong>이에요. 100만이 300만이 되어도 "읽기 편중, 저장은 여유"라는
          결론은 안 바뀝니다. 결론이 뒤집히는 지점(예: 쓰기가 초당 수만 건)이 어디인지 아는 것,
          그게 산정의 진짜 수확이에요.
        </div>
      </Section>

      <Section n={3} title="핵심 알고리즘 — 짧은 키 만들기" sub="base62, 그리고 충돌·예측가능성·단일 장애점">
        <p>
          이제 이 시스템의 심장, <strong>"긴 URL에 어떤 짧은 키를 붙일 것인가"</strong>입니다.
          먼저 키가 몇 자여야 하는지부터 계산하죠. 재료는 <strong>base62</strong> — 숫자 10개,
          소문자 26개, 대문자 26개로 62진법 표기를 하는 겁니다. URL에 안전하게 들어가는
          문자만 쓰는 거죠.
        </p>
        <Code>{CODE_BASE62}</Code>
        <p>
          <strong>7자면 3.5조 개.</strong> 하루 100만 건씩 100년을 만들어도 365억 개 — 공간은
          넉넉합니다. 문제는 "그 키를 <strong>누가, 어떻게, 겹치지 않게</strong> 발급하나"예요.
          해시 방식(A)은 서버가 아무 상태도 안 가져서 좋지만 <strong>충돌</strong>을 감수해야
          하고, 카운터 방식(B)은 충돌이 없지만 키가 <strong>순차적이라 예측 가능</strong>하고
          전역 카운터가 <strong>단일 장애점</strong>이 됩니다.
        </p>
        <p>
          그래서 실전 답안이 <strong>키 생성 서비스(KGS, Key Generation Service)</strong>{' '}
          패턴이에요. 키 <strong>범위</strong>를 애플리케이션 서버마다 미리 떼어 주면, 각 서버는
          자기 범위 안에서 아무 조율 없이 로컬 카운터만 올리며 발급합니다. 범위가 바닥나면 다음
          범위를 받아 가고요. 서버가 죽어 범위 일부가 버려져도 — 3.5조 개 중 100만 개는
          <strong> 버려도 되는 비용</strong>입니다. 낭비를 허용해서 조율을 없애는, 분산 시스템의
          전형적인 거래죠.
        </p>
        {/* 학습 포인트: 세 방식 중 '정답'을 외우는 게 아니라, 충돌·예측가능성·단일 장애점이라는
            세 축으로 각 방식을 평가하는 눈을 기르는 것이 목적이다. */}
        <div className="warn">
          <b>순차 키의 함정</b> 카운터 키가 <span className="icode">abc1234</span>였다면 다음 키는{' '}
          <span className="icode">abc1235</span> — 공격자가 번호를 하나씩 올려 가며{' '}
          <strong>남이 만든 단축 URL을 전부 수집</strong>할 수 있어요. 비공개 문서 링크가 섞여
          있다면 사고입니다. KGS로 범위를 무작위 배정하거나 발급 시 키를 섞는(셔플) 이유가
          여기 있어요. 설계 논의에서 보안 관점을 한 번씩 스치는 습관을 들이세요.
        </div>
      </Section>

      <Section n={4} title="API와 데이터 모델" sub="301이냐 302냐 — 리다이렉트 한 줄의 트레이드오프">
        <p>
          바깥에서 보이는 계약은 단출합니다. 만들기(<span className="icode">POST /shorten</span>)와
          이동하기(<span className="icode">GET /&#123;key&#125;</span>) — 끝. 하지만 그 단출한
          리다이렉트 응답에서 상태 코드를 <strong>301로 줄지 302로 줄지</strong>가 이 서비스의
          성격을 가릅니다.
        </p>
        <Code>{CODE_API}</Code>
        <p>
          <strong>301(영구 이동)</strong>은 브라우저가 결과를 캐시해서, 같은 링크를 다시 누르면
          우리 서버에 <strong>오지도 않고</strong> 목적지로 갑니다. 서버 부하는 최소가 되지만 —
          클릭이 안 잡히니 <strong>통계가 불가능</strong>하고, 목적지를 바꾸거나 링크를 차단해도
          이미 캐시한 브라우저엔 반영이 안 돼요. <strong>302(임시)</strong>는 정반대: 매 클릭이
          서버를 통과하니 분석·차단·만료가 다 살아 있는 대신 부하를 그대로 받습니다. 클릭 분석이
          상품인 서비스가 302를 고르는 이유죠. <strong>요구사항이 상태 코드 하나까지 결정합니다.</strong>
        </p>
        <p>
          데이터 모델은 겸손할수록 좋아요. 테이블 하나, 조회는 <strong>키 등가 조회 하나</strong> —
          조인도 범위 스캔도 없습니다. 이 단순함이 섹션 6에서 샤딩을 아주 쉽게 만들어 줘요.
          만료 처리는 <strong>지연 삭제(lazy)</strong>와 <strong>배치 청소</strong>를 겸용합니다.
          조회 순간에 만료 여부를 판정하니 정확성은 lazy가 보장하고, 디스크 공간 회수는 새벽
          배치가 맡는 분업이에요.
        </p>
        <div className="tip">
          <b>커스텀 별칭이라는 복병</b> "<span className="icode">tdl.ink/summer-sale</span>처럼
          원하는 이름을 쓰게 해 주세요"라는 요구가 오면 — 발급이 아니라 <strong>선점</strong>
          문제가 됩니다. 중복 확인과 동시 요청 처리(유니크 제약 + 충돌 시 409 응답)가 필요해지죠.
          기능 한 줄이 데이터 모델에 어떤 파문을 일으키는지 감지하는 것도 설계 감각입니다.
        </div>
      </Section>

      <Section n={5} title="읽기 경로 최적화" sub="상위 20% 키가 트래픽 80%를 만든다">
        <p>
          섹션 2의 결론을 실행할 차례입니다. 초당 수천 건의 <span className="icode">GET /&#123;key&#125;</span>를
          DB가 직접 받게 둘 수는 없어요. 다행히 단축 URL 트래픽은 전형적인{' '}
          <strong>멱법칙 분포</strong>를 따릅니다 — 방금 SNS에 공유된 소수의 링크가 트래픽을
          독식하고, 대부분의 링크는 잠들어 있죠. <strong>상위 20% 키가 트래픽의 80%</strong>를
          차지하니, 전체 1TB가 아니라 <strong>뜨거운 소수만 메모리에 올려도</strong> 적중률이
          치솟습니다.
        </p>
        <Code>{CODE_READPATH}</Code>
        <p>
          평화로운 시나리오만 있는 건 아닙니다. 누군가 <strong>존재하지 않는 키</strong>를
          무작위로 대량 조회하면? 캐시엔 당연히 없으니 요청이 전부 DB로 <strong>관통(cache
          penetration)</strong>합니다 — 캐시가 무용지물이 되고 DB부터 쓰러져요. 방어는 두 겹:
          "없음"이라는 결과 자체를 짧게 캐시하는 <strong>네거티브 캐시</strong>, 그리고 "이 키는
          확실히 없다"를 메모리에서 판정해 주는 <strong>블룸 필터</strong>입니다. 블룸 필터는
          '있다'는 답엔 오차가 있지만 <strong>'없다'는 답은 확실</strong>하다는 성질 덕분에,
          없는 키 폭격을 DB 앞에서 걸러 내는 데 딱 맞아요.
        </p>
        {/* 학습 포인트: 최적화는 공짜가 아니다 — CDN 캐싱은 302로 확보한 '분석 가능성'과
            정면충돌한다. 섹션 4의 결정이 섹션 5의 선택지를 제한하는 것, 이게 설계의 연쇄다. */}
        <p>
          더 극단으로 가면 <strong>CDN 레벨 리다이렉트</strong>가 있습니다. 리다이렉트 응답
          자체를 엣지 서버에 캐시해서 요청이 우리 인프라에 <strong>도착조차 안 하게</strong>{' '}
          만드는 거죠. 다만 여기서 섹션 4의 결정이 발목을 잡습니다 — 302를 골라 클릭 분석을
          하기로 했다면, 엣지에서 응답을 돌려보내는 순간 클릭이 안 잡혀요. 최적화 하나하나가
          앞선 결정과 <strong>연쇄</strong>되어 있다는 것, 이게 시스템 설계의 실감입니다.
        </p>
        <div className="tip">
          <b>우리 스택과의 연결</b> 이 캐시 우선 조회는 우리 플랫폼이 시세를 다루는 방식과
          같은 문법이에요 — Valkey에 <span className="icode">market:quote</span> 캐시를 두고
          DB 아닌 메모리에서 응답하죠. "읽기가 지배하면 캐시를 앞세운다"는 원칙은 도메인이
          바뀌어도 그대로입니다.
        </div>
      </Section>

      <Section n={6} title="규모 확장과 가용성" sub="샤딩·다중 리전, 그리고 이 서비스의 SLO">
        <p>
          트래픽이 10배, 100배가 되면? 캐시로 버티다가 결국 DB를 쪼갤 때가 옵니다. 다행히
          섹션 4에서 데이터 모델을 겸손하게 지어 둔 보상이 여기서 옵니다 — 조회가 전부{' '}
          <strong>키 등가 조회</strong>뿐이라, 앞 강좌에서 배운 샤드 키 논의를 교과서처럼
          적용할 수 있어요. <span className="icode">hash(key) % N</span>으로 나누면 크로스 샤드
          쿼리가 <strong>아예 존재하지 않습니다.</strong>
        </p>
        <Code>{CODE_SCALE}</Code>
        <p>
          지리적 확장도 같은 논리로 풉니다. 읽기는 초당 수천 건이고 지연에 민감하니{' '}
          <strong>리전마다 복제본과 캐시</strong>를 두고 가까운 곳에서 응답합니다. 쓰기는 초당
          12건으로 한가하니 <strong>단일 프라이머리</strong>로 몰아도 되고, 복제가 몇 초 늦어
          "방금 만든 링크가 지구 반대편에서 1초 늦게 열리는" 정도는 대개 수용 가능한{' '}
          <strong>결과 정합성</strong>이에요. 읽기와 쓰기의 요구 수준이 다르면, 아키텍처도
          다르게 대접하는 겁니다.
        </p>
        <p>
          마지막으로 가용성. 이 서비스의 무서운 점은 <strong>장애 반경이 자기 자신이 아니라는
          것</strong>입니다. 단축기가 10분 죽으면, 그 10분 동안 전 세계 문서·메일·광고에 박힌{' '}
          <strong>모든 링크가 함께 죽어요.</strong> 그래서 SLO 논의에서 이 서비스는 유독 높은
          기준을 요구받습니다 — 그리고 영리한 답은 <strong>읽기와 쓰기의 SLO를 분리</strong>하는
          거예요. 링크 생성이 10분 안 되는 것과 리다이렉트가 10분 안 되는 것은 사고의 급이
          다르니까, 읽기 경로(리다이렉트)만큼은 99.99%를 노리고 그쪽에 이중화를 몰아 줍니다.
        </p>
        <div className="warn">
          <b>숫자의 무게를 알기</b> 99.9%는 월 43분, 99.99%는 월 4.3분의 장애 허용입니다.
          9 하나를 더 붙일 때마다 비용은 곱절로 뛰어요(다중 AZ → 다중 리전 → 자동 페일오버…).
          "우리 서비스는 무조건 99.999%"라고 외치는 건 설계가 아니라 구호입니다 — 어느 경로에
          어느 수준이 <strong>필요한지</strong>부터 말할 수 있어야 해요.
        </div>
      </Section>

      <Section n={7} title="분석 기능 추가 — 설계 변경 연습" sub="좋은 설계는 기능 추가에 크게 흔들리지 않는다">
        <p>
          설계가 끝났다고 생각한 순간, 요구가 도착합니다 — <strong>"링크별 클릭 통계를 보여
          주세요."</strong> 실무의 설계는 늘 이렇게 움직이는 표적이에요. 여기서 훈련할 것은
          두 가지: <strong>핵심 경로를 오염시키지 않고 기능을 얹는 법</strong>, 그리고 루틴의
          ①로 돌아가 <strong>요구사항을 재질문하는 습관</strong>입니다.
        </p>
        <Code>{CODE_ANALYTICS}</Code>
        <p>
          동기 기록(후보 1)의 문제는 명확합니다. 리다이렉트라는 <strong>가장 빠르고 가장 높은
          SLO를 받는 경로</strong>에 DB 쓰기를 끼워 넣는 순간, 읽기 1,200/sec가 쓰기 1,200/sec로
          둔갑하고 통계 DB가 느려지면 리다이렉트까지 함께 느려져요. 이벤트 발행(후보 2)은 그
          결합을 끊습니다 — 리다이렉트는 즉시 응답하고, 클릭 이벤트만 Kafka에 던지면 소비자가
          뒤에서 집계하죠. 우리 플랫폼이 감사 로그를 <span className="icode">audit.events</span>{' '}
          토픽으로 흘려보내는 것과 정확히 같은 패턴입니다.
        </p>
        <p>
          다만 후보 2를 고르기 전에 반드시 물어야 합니다 — <strong>"통계는 실시간이어야
          하나요?"</strong> 몇 분 늦어도 되는 대시보드라면 비동기 집계로 충분하지만, "지금 이
          순간의 클릭"이 상품이라면 얘기가 달라지니까요. 기능 하나가 추가될 때마다 루틴의 ①로
          짧게 돌아가는 것 — 이게 설계를 <strong>루틴</strong>이라고 부르는 이유입니다. 그리고
          최종 그림을 보세요: 기존 설계는 거의 그대로고, 읽기 경로에 <strong>이벤트 발행 한
          줄</strong>이 얹혔을 뿐입니다. 좋은 설계의 증거는 바로 이 <strong>변경의 작음</strong>이에요.
        </p>
        {/* 학습 포인트: 실습의 핵심은 '완성'이 아니라 '방어'다. 트래픽 100배 추궁에
            "캐시 적중률·샤드 수·복제 구조가 어떻게 변하는가"를 숫자로 답할 수 있어야 한다. */}
        <div className="tip">
          <b>실습 — 45분 화이트보드</b> 타이머를 45분에 맞추고, 화이트보드(또는 excalidraw)에
          URL 단축기 전체 설계를 완성하세요 — 요구사항 가정, 용량 산정, API, 테이블, 키 발급,
          캐시, 샤딩까지 이 강좌의 루틴 순서대로. 끝나면 동료에게 부탁하세요: <strong>"트래픽이
          100배가 되면?"</strong> 하고 추궁해 달라고. 캐시 적중률은? 샤드는 몇 개로? 프라이머리
          쓰기는 버티나? — 숫자로 방어하지 못한 지점이 바로 다음에 공부할 지점입니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>🔗 여기까지 왔다면</h3>
          <p className="muted">
            축하해요 — 설계 문제 하나를 <strong>요구사항부터 확장까지</strong> 처음부터 끝까지
            완주했습니다. base62와 KGS, 301과 302, 캐시 관통과 블룸 필터 같은 부품도 얻었지만,
            진짜 수확은 <strong>루틴</strong>이에요. 어떤 설계 문제가 와도 "일단 질문부터,
            그다음 숫자로" 시작할 수 있는 순서 감각이요. 다만 URL 단축기는 읽기 편중에
            상태가 단순한, 어찌 보면 '착한' 문제였습니다. 다음{' '}
            <Link to="/learn/lv4-system-design-chat"><strong>시스템 설계 훈련 2 — 실시간 채팅
            시스템</strong></Link>에서는 연결이 계속 살아 있고, 메시지 순서가 중요하고, 읽음
            표시까지 얽히는 <strong>상태 가득한 문제</strong>로 같은 루틴을 밀어붙여 봅니다.
          </p>
        </div>
      </div>
    </div>
  );
}
