// 이 파일이 하는 일: "캐싱 전략 — 로컬과 Redis" 코스 — 캐시를 '빨라지는 마법'이 아니라
// 무효화와 일관성을 감당하는 설계 문제로 다루는 고급 과정 페이지. 캐시를 쓰기 전 질문에서
// 출발해, 계층 선택(로컬 vs Redis) → 무효화 → 패턴(Cache-Aside 등) → 장애(스탬피드·관통·눈사태)
// → 일관성·보안 → 운영·측정까지 7개 섹션으로 안내한다. 프레이밍은 "서비스를 만들어 본 사람"이
// 우리 플랫폼의 실제 설계(market:quote 시세 캐시, 리프레시 토큰 저장)를 읽어 내는 것.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.

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

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

const CODE_CHECKLIST = `캐시를 붙이기 전에 답해야 할 세 가지 질문

① 이 데이터, 읽기가 쓰기보다 압도적으로 많은가?
   - 시세 조회: 1초에 수천 번 읽고, 갱신은 초당 몇 번 → 캐시감
   - 주문 내역: 쓰자마자 바로 정확히 읽어야 함     → 캐시 비추천

② 조금 낡은(stale) 값을 보여 줘도 되는가? 몇 초까지?
   - 공지사항 목록: 30초 늦어도 아무도 모름        → TTL 30s
   - 계좌 잔고: 1원이라도 틀리면 사고              → 캐시 말고 DB

③ 캐시가 전부 날아가도 서비스가 버티는가?
   - 캐시는 '있으면 빠른' 층이지 '없으면 죽는' 층이 아니어야 함
   - 이 질문에 "아니오"라면, 그건 캐시가 아니라 두 번째 DB를
     만들고 있는 것 — 설계를 다시 봐야 함`;

const CODE_LAYERS = `로컬(인메모리) 캐시              분산 캐시 (Valkey/Redis)
─────────────────────────       ─────────────────────────
같은 JVM 힙 안의 Map/Caffeine    별도 서버, TCP로 왕복
지연: ~수십 나노초 (메모리 접근)  지연: ~1ms 안팎 (네트워크 포함)
서버가 3대면 캐시도 3벌          모든 서버가 같은 값을 봄
프로세스 재시작 = 캐시 증발       서버 재시작에도 살아남음
서로 다른 값 들고 있을 수 있음    한 곳만 무효화하면 끝

선택 기준: "여러 인스턴스가 같은 값을 봐야 하는가?"
- 그렇다 → 분산 캐시. 우리 시세 캐시 market:quote가 Valkey에 사는
  이유가 이것 — 수집기가 쓴 최신가를 어떤 API 서버가 받아도
  똑같이 읽어야 하고, 서버를 늘려도 캐시가 쪼개지면 안 된다.
- 아니다(불변 설정, 코드표 등) → 로컬로 충분. 네트워크 왕복조차 아깝다.`;

const CODE_MULTILAYER = `// 다계층 캐시: L1(로컬) + L2(Redis) — 큰 서비스의 흔한 구성
// 요청 → L1(나노초) → 미스 → L2(1ms) → 미스 → DB(수십 ms)

String getQuoteName(String code) {
    String v = l1.getIfPresent(code);      // ① L1: JVM 안, 공짜에 가까움
    if (v != null) return v;

    v = redis.get("market:quote:" + code); // ② L2: 네트워크 1회
    if (v != null) {
        l1.put(code, v);                   //    L1에 되채워 다음 요청 대비
        return v;
    }

    v = db.findName(code);                 // ③ 원본: 제일 비싼 길
    redis.setex("market:quote:" + code, 5, v); // L2에 TTL 5초로 적재
    l1.put(code, v);
    return v;
}

// 대가: 층이 늘수록 '서로 다른 값'을 들고 있을 확률도 는다.
// 그래서 L1의 TTL은 L2보다 훨씬 짧게(예: 1초) 잡아 불일치 창을 좁힌다.`;

const CODE_INVALIDATION = `무효화 전략 두 갈래 — 언제 캐시를 버릴 것인가

① TTL(Time-To-Live) 만료: "n초 지나면 알아서 버려"
   SET market:quote:005930 "72,300" EX 5     # 5초 뒤 자동 소멸
   - 장점: 구현이 없다. 잊어도 결국 낡은 값이 사라진다 (안전망)
   - 단점: TTL 동안은 낡은 값을 그대로 서빙

② 이벤트 기반: "원본이 바뀌는 순간, 내가 직접 버려"
   // 설정 저장 API 안에서
   db.save(config);                          // 1) 원본 갱신
   redis.del("config:" + config.key());      // 2) 캐시 삭제
   - 장점: 불일치 창이 밀리초 단위로 준다
   - 단점: '바뀌는 모든 경로'를 빠짐없이 찾아 심어야 한다 (누락 = 버그)

실무 정석: 둘을 겹친다. 이벤트로 즉시 지우되,
TTL을 안전망으로 깔아 "지우는 걸 깜빡한 경로"의 피해를 시간으로 제한.`;

const CODE_UPDATE_VS_DELETE = `쓰기 시점에 캐시를 '갱신'할까, '삭제'할까?

  갱신(SET 새값)                     삭제(DEL)
  ──────────────                     ──────────
  다음 읽기도 즉시 히트               다음 읽기 한 번은 미스(DB행)
  두 요청이 엇갈리면                  값을 다시 계산하는 주체가
  낡은 값이 이길 수 있음 ←위험        '읽기' 하나로 통일됨 ←단순

  [갱신의 함정] 요청 A(값=1 계산)와 B(값=2 계산)가 거의 동시에 쓰면
  DB엔 2가 남았는데 캐시엔 A가 늦게 도착해 1이 남을 수 있다.
  삭제는 이 경합 자체가 없다 — 그래서 기본 선택은 '삭제'다.
  갱신은 "계산이 아주 비싸고 쓰기 경합이 없다"고 확신할 때만.`;

const CODE_ASIDE = `// Cache-Aside(지연 로딩) — 캐싱 패턴의 기본형
// "캐시를 먼저 보고, 없으면 DB에서 읽어 캐시에 채워 넣는다"

public Quote getQuote(String code) {
    String key = "market:quote:" + code;

    String cached = redis.get(key);        // ① 캐시 먼저
    if (cached != null) {
        return parse(cached);              //    히트: DB 안 감
    }

    Quote q = quoteRepository.find(code);  // ② 미스: 원본에서 읽고
    redis.setex(key, 5, serialize(q));     // ③ 캐시에 채운다 (TTL 5초)
    return q;
}

// 왜 기본형인가:
// - 캐시가 통째로 날아가도 그냥 '전부 미스'가 될 뿐, 서비스는 돈다
// - 실제로 읽힌 것만 캐시에 올라간다 (안 쓰는 데이터로 메모리 안 낭비)
// - DB 쓰기 코드는 캐시의 존재를 몰라도 된다 (삭제 한 줄만 얹으면 끝)`;

const CODE_WRITE_PATTERNS = `쓰기까지 캐시가 책임지는 패턴들 — Cache-Aside와의 비교

Write-Through: 쓰기가 캐시를 '통과'해 DB까지 간다
  app → cache → DB   (둘 다 성공해야 쓰기 완료)
  + 캐시와 DB가 항상 같다        - 쓰기가 느려진다(두 번 씀)

Write-Behind(Write-Back): 캐시에만 쓰고, DB엔 나중에 몰아서
  app → cache ---(비동기 배치)--→ DB
  + 쓰기가 매우 빠르다            - 캐시가 죽으면 아직 안 옮긴
                                    쓰기가 '유실'된다 ←무거운 위험

Refresh-Ahead: 만료 직전에 미리 백그라운드로 갱신
  TTL 60초짜리 인기 키를 55초쯤에 미리 다시 읽어 채움
  + 인기 키는 미스를 아예 안 맞음  - '인기'를 예측해야, 빗나가면 낭비

기본기: 읽기 가속이 목적이면 Cache-Aside부터. 나머지는
쓰기 지연·유실 감수라는 명확한 대가를 알고 고르는 특수해.`;

const CODE_STAMPEDE = `// 캐시 스탬피드: 인기 키 하나가 만료되는 순간,
// 대기하던 요청 1,000개가 '동시에 미스' → 전부 DB로 돌진 → DB 사망

// 방어 ① 락: 처음 한 놈만 DB에 가고, 나머지는 잠깐 기다린다
String v = redis.get(key);
if (v == null) {
    boolean won = redis.setnx(key + ":lock", "1", 3);  // 3초짜리 락
    if (won) {
        v = db.load();                    // 이긴 1명만 원본 조회
        redis.setex(key, 60, v);
        redis.del(key + ":lock");
    } else {
        Thread.sleep(50);                 // 진 999명: 잠깐 쉬고
        v = redis.get(key);               // 채워진 캐시를 다시 읽음
    }
}

// 방어 ② 확률적 조기 만료: 만료를 '다 같이' 맞지 않게
// 남은 TTL이 짧을수록 높은 확률로 '내가 미리 갱신'을 자원한다.
// 여럿이 각자 주사위를 굴리므로 갱신 시점이 자연히 흩어진다.
if (remainingTtl < renewWindow * random()) {
    refreshInBackground(key);             // 만료 전에 미리 채움
}`;

const CODE_METRICS = `# 운영 점검 — Valkey/Redis에서 캐시 건강 읽기
redis-cli INFO stats | grep keyspace
# keyspace_hits:184220        ← 캐시가 답한 횟수
# keyspace_misses:9711        ← DB까지 간 횟수
# 히트율 = hits / (hits + misses) = 184220 / 193931 = 약 95%

redis-cli INFO memory | grep -E "used_memory_human|maxmemory"
# used_memory_human:412.33M   ← 지금 쓰는 메모리
redis-cli CONFIG GET maxmemory-policy
# allkeys-lru                 ← 가득 차면 '가장 오래 안 읽힌 키'부터 축출

redis-cli INFO stats | grep evicted_keys
# evicted_keys:120394         ← 메모리가 모자라 쫓겨난 키 수
# 이 값이 빠르게 늘면: 메모리 증설 or TTL 단축 or 캐시 대상 축소

# 판단 기준(경험칙):
# 히트율 90%+  → 캐시가 제 몫을 함
# 히트율 50%-  → 캐시 유지비(코드 복잡도+불일치 위험)가 이득을 넘음
#                → 키 설계를 고치거나, 캐시를 떼는 게 답일 수 있다`;

const CODE_LAB = `실습: 자주 조회되는 조회 API 하나에 Cache-Aside 적용하기

① 대상 고르기 — "읽기 많고, 몇 초 낡아도 되는" 조회 하나
   (예: 종목 기본정보 조회, 화면 설정 조회)

② 측정 먼저 — 캐시 없이 현재 상태를 숫자로 남긴다
   for i in 1 2 3 4 5; do
     time curl -s "http://localhost:8080/api/..." > /dev/null
   done                       # 5회 응답시간 기록 (예: 평균 38ms)

③ Cache-Aside 구현 — 섹션 4의 코드 골격 그대로
   get → 미스면 DB → setex(TTL은 ①에서 정한 '감당 가능한 낡음')

④ 다시 측정 — 같은 요청 5회 (예: 평균 2ms), INFO stats로 히트율 확인

⑤ 무효화 검증 — 데이터를 바꾼 직후 조회해 본다
   낡은 값이 TTL 동안 보이는가? 그게 ①에서 합의한 범위 안인가?

제출물: 전/후 응답시간, 히트율, 그리고 "TTL을 왜 그 값으로 정했나" 한 단락.
숫자 없는 "빨라졌어요"는 실습 완료가 아닙니다.`;

// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: '캐싱 패턴' },
  { n: 5, label: '무너질 때' },
  { n: 6, label: '일관성과 위험' },
  { n: 7, label: '운영과 측정' },
];

export default function Lv3CachingStrategyPage() {
  return (
    <div>
      {/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
      <div className="hero">
        <div className="eyebrow">Course · 고급 과정</div>
        <h1>⚡ 캐싱 전략<br />— 로컬과 Redis</h1>
        <p>
          캐시는 "붙이면 빨라지는 마법"으로 소개되곤 하지만, 진짜 비용은 붙인 다음에 옵니다.
          <strong> 언제 버릴 것인가(무효화), 낡은 값을 얼마나 감당할 것인가(일관성)</strong> —
          이 두 질문에 답하지 못한 캐시는 성능 개선이 아니라 '가끔 틀린 값을 아주 빠르게
          보여 주는 버그'예요. 이 코스에서는 우리 플랫폼의 실제 설계(시세 캐시, 토큰 저장소)를
          교재 삼아, 캐시를 <strong>설계 문제</strong>로 다루는 법을 익힙니다.
        </p>
        <div className="chip-row">
          <span className="chip">예상 소요 60분</span>
          <span className="chip">실습: Cache-Aside 적용 + 히트율·응답시간 측정</span>
          <span className="chip">선수 지식: DB 조회 API를 만들어 본 경험</span>
        </div>
      </div>

      {/* 섹션 바로가기 — 학습 포인트: 정적 앵커(#sec-n)만으로 만드는 목차 */}
      <div className="chip-row">
        {SECTIONS.map((s) => (
          <a key={s.n} className="chip" href={`#sec-${s.n}`}>
            {s.n}. {s.label}
          </a>
        ))}
      </div>

      <Section n={1} title="캐시를 쓰기 전 질문" sub="캐시는 '읽기 많고 자주 안 바뀌는' 데이터의 것">
        <p>
          캐시의 원리는 단순해요. <strong>비싸게 구한 답을 가까운 곳에 적어 두고, 같은 질문이
          오면 적어 둔 걸 보여 준다.</strong> 그래서 캐시가 이득을 보는 조건도 명확합니다 —
          같은 질문이 <strong>충분히 자주</strong> 반복되고(읽기가 많고), 적어 둔 답이{' '}
          <strong>한동안 유효</strong>해야 하죠(자주 안 바뀜). 이 둘 중 하나라도 무너지면
          캐시는 일을 안 하거나, 틀린 답을 합니다.
        </p>
        <Code>{CODE_CHECKLIST}</Code>
        <p>
          특히 첫 번째 질문이 숫자로 이어집니다. 캐시가 답한 비율을{' '}
          <strong>히트율(hit rate)</strong>이라고 하는데, 히트율이 낮으면 캐시는 오히려
          손해예요. 요청마다 <span className="icode">캐시 확인(미스) + DB 조회 + 캐시 적재</span>
          라는 <strong>세 단계</strong>를 밟게 되니, 캐시 없이 DB만 보던 때보다 일이 늘어난
          셈이죠. 여기에 무효화 코드의 복잡도, 불일치 사고의 위험까지 얹으면 — 히트율 낮은
          캐시는 이자만 나가는 빚입니다.
        </p>
        <div className="tip">
          <b>고급 과정의 첫 감각</b> "캐시 없이 풀 수 있으면 캐시를 쓰지 마라." 인덱스 추가,
          쿼리 개선, 불필요한 조회 제거로 충분히 빨라진다면 그쪽이 항상 낫습니다. 캐시는
          상태(복사본)를 하나 더 만드는 일이고, <strong>상태가 하나 늘면 틀릴 수 있는 곳도
          하나 는다</strong>는 게 이 코스 전체를 관통하는 전제예요.
        </div>
      </Section>

      <Section n={2} title="캐시 계층 고르기" sub="로컬 vs 분산 — 그리고 우리 market:quote가 Valkey에 사는 이유">
        <p>
          캐시를 쓰기로 했다면, 다음 질문은 <strong>어디에 둘 것인가</strong>입니다. 크게 두
          자리가 있어요. 앱 프로세스 안의 <strong>로컬(인메모리) 캐시</strong> — 자바라면
          Map이나 Caffeine 같은 라이브러리 — 그리고 별도 서버로 띄우는{' '}
          <strong>분산 캐시</strong>, 대표적으로 Redis와 그 오픈소스 포크인 Valkey죠.
        </p>
        <Code>{CODE_LAYERS}</Code>
        <p>
          속도만 보면 로컬이 압도적입니다. 메모리 접근이니까요. 그런데도 우리 시세 캐시{' '}
          <span className="icode">market:quote</span>는 Valkey에 삽니다. 이유는 표의 마지막
          기준 그대로예요. 시세는 <strong>수집기가 쓰고, 여러 API 서버가 읽는</strong>{' '}
          데이터입니다. 로컬 캐시로 하면 서버마다 다른 가격을 들고 있게 되고, 수집기가 새
          가격을 어느 서버 메모리에 넣어 줄 방법도 없죠. <strong>쓰는 쪽과 읽는 쪽이 다르고,
          읽는 쪽이 여럿이면 분산 캐시</strong>가 유일한 답입니다.
        </p>
        <p>
          큰 서비스는 둘을 <strong>겹쳐</strong> 씁니다. L1(로컬)에서 나노초에 걸러 내고,
          미스만 L2(Redis)로, 거기서도 미스면 DB로 — 층마다 지연이 수십~수백 배씩 뛰기
          때문에 위층에서 많이 막을수록 이득이죠.
        </p>
        <Code>{CODE_MULTILAYER}</Code>
        <div className="tip">
          <b>대가 없는 층은 없다</b> 다계층은 빠르지만, <strong>불일치할 수 있는 곳이 두
          군데</strong>가 됩니다. L2를 지워도 L1엔 낡은 값이 남아 있을 수 있어요. 그래서
          L1의 TTL은 L2보다 훨씬 짧게 잡아 "낡아도 1초"로 창을 좁힙니다. 층을 하나 더 얹는
          결정은 항상 "지연이 얼마나 줄고, 불일치 창이 얼마나 느는가"의 교환이에요.
        </div>
      </Section>

      <Section n={3} title="무효화 — 언제 캐시를 버리나" sub="캐싱에서 가장 어려운 문제">
        <p>
          "컴퓨터 과학에는 어려운 문제가 둘 있다. 캐시 무효화, 그리고 이름 짓기." — 농담처럼
          도는 말이지만 절반은 진담입니다. 캐시에 값을 <strong>넣는</strong> 코드는 누구나
          짜요. 어려운 건 원본이 바뀌었을 때 그 복사본을 <strong>언제, 누가, 어떻게
          버리느냐</strong>입니다. 여기서 실수하면 사용자는 '어제의 데이터'를 보고, 개발자는
          "분명 저장했는데 왜 안 바뀌죠?"라는 문의를 받게 되죠.
        </p>
        <Code>{CODE_INVALIDATION}</Code>
        <p>
          TTL은 <strong>게으르지만 확실한</strong> 방식이에요. 우리 시세 캐시가 짧은 TTL을 쓰는
          것도 이 맥락입니다 — 시세는 어차피 계속 새로 들어오니, "몇 초 안에 알아서 사라진다"는
          보장만으로 충분하거든요. 반대로 관리자가 바꾸는 <strong>설정값</strong>처럼 "바뀌는
          순간이 명확하고, 바뀌면 즉시 반영돼야 하는" 데이터는 이벤트 기반 삭제가 어울립니다.
        </p>
        <p>
          이벤트 기반을 고르면 한 가지 선택이 더 남아요. 쓰기 시점에 캐시를 새 값으로{' '}
          <strong>갱신</strong>할 것인가, 그냥 <strong>삭제</strong>할 것인가.
        </p>
        <Code>{CODE_UPDATE_VS_DELETE}</Code>
        <div className="tip">
          <b>기본값은 삭제</b> 갱신은 "다음 읽기도 히트"라는 이득이 있지만, 동시에 쓰는 두
          요청의 순서가 꼬이면 <strong>DB와 다른 값이 캐시에 눌러앉는</strong> 위험을 삽니다.
          삭제는 그 경합 자체를 없애고, 값을 만드는 책임을 '읽기 경로 하나'로 모아 코드도
          단순해져요. 헷갈리면 삭제 — 갱신은 근거가 있을 때만.
        </div>
      </Section>

      <Section n={4} title="캐싱 패턴" sub="Cache-Aside가 기본형인 이유, 그리고 나머지 패턴들">
        <p>
          "캐시 먼저 보고, 없으면 DB에서 읽어 채운다" — 섹션 2와 3에서 이미 여러 번 스친 이
          흐름에는 이름이 있습니다. <strong>Cache-Aside</strong>(지연 로딩, lazy loading).
          애플리케이션이 캐시를 <strong>옆에(aside) 두고</strong> 직접 확인·적재를 관리하는
          패턴이에요.
        </p>
        <Code>{CODE_ASIDE}</Code>
        <p>
          Cache-Aside가 기본형인 이유는 <strong>실패에 강해서</strong>입니다. 캐시 서버가
          통째로 재시작돼도 결과는 "한동안 전부 미스" — 느려질 뿐 틀리지 않아요. 또 실제로
          읽힌 키만 캐시에 올라가니 메모리도 수요를 따라갑니다. 반면 쓰기까지 캐시가 책임지는
          패턴들은 더 강한 보장을 주는 대신, 더 무거운 대가를 요구해요.
        </p>
        <Code>{CODE_WRITE_PATTERNS}</Code>
        <div className="tip">
          <b>Write-Behind의 무게</b> 셋 중 Write-Behind만은 특별히 조심하세요. "캐시에만 쓰고
          DB엔 나중에"라는 말은, <strong>캐시가 죽는 순간 아직 안 옮긴 쓰기가 사라진다</strong>는
          뜻입니다. 조회수 집계처럼 몇 건 잃어도 되는 데이터라면 좋은 도구지만, 주문·잔고처럼
          한 건도 잃으면 안 되는 데이터에 썼다간 사고가 됩니다. 패턴 이름보다{' '}
          <strong>"유실을 감당할 수 있는가"</strong>를 먼저 물으세요.
        </div>
      </Section>

      <Section n={5} title="캐시가 무너질 때" sub="스탬피드 · 관통 · 눈사태 — 캐시 장애의 세 얼굴">
        <p>
          캐시는 평소엔 방패지만, 무너지는 순간 <strong>뒤에 숨어 있던 DB가 그대로 노출</strong>
          됩니다. 캐시 덕에 DB가 전체 트래픽의 5%만 받고 있었다면, 캐시가 비는 순간 갑자기
          20배의 부하가 밀려드는 거죠. 무너지는 모양엔 이름이 붙어 있어요.
        </p>
        <p>
          <strong>· 스탬피드(stampede, 동시 미스 폭주)</strong> — 인기 키 <strong>하나</strong>가
          만료되는 순간, 그 키를 기다리던 요청 수백 개가 동시에 미스를 맞고 전부 DB로
          돌진합니다. 답은 하나면 충분한데 같은 쿼리가 수백 번 실행되는 낭비이자 위험이죠.
        </p>
        <p>
          <strong>· 관통(penetration)</strong> — <strong>존재하지 않는 키</strong>를 계속
          조회당하는 경우예요. 없는 데이터는 캐시에 안 담기니 매번 DB까지 뚫고 내려갑니다.
          악의적인 요청이 무작위 ID를 쏘면 캐시가 무력화되죠. 방어는 "없음"이라는 사실
          자체를 짧은 TTL로 캐시하는 것(negative caching)입니다.
        </p>
        <p>
          <strong>· 눈사태(avalanche)</strong> — 같은 시각에 적재된 키들이 <strong>같은 TTL</strong>
          로 <strong>동시에 만료</strong>되거나, 캐시 서버 자체가 죽어서 미스가 한꺼번에
          쏟아지는 상황. 배포 직후 캐시를 일괄 예열했다면, 정확히 TTL 뒤에 일괄 만료가
          돌아옵니다. 방어는 TTL에 무작위 지터(jitter)를 더해 만료 시점을 흩는 것.
        </p>
        <Code>{CODE_STAMPEDE}</Code>
        <div className="tip">
          <b>공통 원리</b> 세 방어(락, 확률적 조기 만료, TTL 지터)의 본질은 하나예요 —{' '}
          <strong>"동시에"를 없애는 것.</strong> 미스가 나더라도 한 명만 DB에 가게 하고(락),
          만료가 오더라도 다 같이 맞지 않게 흩는다(지터·조기 갱신). 캐시 장애 대비란 결국
          동시성을 시간 축으로 펼쳐 놓는 설계입니다.
        </div>
      </Section>

      <Section n={6} title="일관성과 위험" sub="불일치를 '없애는' 게 아니라 '감당 가능한 범위'에 두기">
        <p>
          여기서 불편한 진실 하나를 정면으로 봅시다. <strong>캐시와 DB는 어차피 완벽히 같을
          수 없습니다.</strong> 원본을 고치고 캐시를 지우는 그 짧은 틈에도 낡은 값은 읽혀요.
          그래서 고급 설계의 질문은 "불일치를 어떻게 없애나"가 아니라, <strong>"몇 초짜리
          불일치까지 이 데이터가 감당하는가"</strong>입니다. 시세 5초, 공지 30초, 잔고 0초 —
          이 숫자를 데이터마다 정해 TTL과 무효화 전략으로 옮기는 게 캐싱 설계의 실체예요.
          0초짜리 데이터의 답은 간단합니다. 캐시하지 않는 것.
        </p>
        <p>
          이 렌즈로 우리 플랫폼의 설계 하나를 읽어 봅시다. 우리 인증 서버는{' '}
          <strong>리프레시 토큰을 Valkey에 저장</strong>합니다. "캐시에 토큰을?" 싶지만,
          이건 캐시가 아니라 Valkey를 <strong>TTL이 내장된 원본 저장소</strong>로 쓰는
          경우예요. 토큰은 만료 시각이 곧 삭제 시각이니 TTL이 삶의 주기와 정확히 일치하고,
          로그아웃 시 <span className="icode">DEL</span> 한 번이면 즉시 무효화되고, 검증
          요청마다 밀리초로 응답해야 하죠. 같은 도구라도 <strong>복사본(캐시)으로 쓰는가,
          원본(저장소)으로 쓰는가</strong>에 따라 설계 규칙이 완전히 달라집니다 — 원본으로
          쓴다면 유실 대비(영속화·백업)가 필요해지니까요.
        </p>
        <p>
          마지막으로 보안. 캐시는 <strong>데이터가 사는 곳이 하나 늘어난다</strong>는 뜻이고,
          지켜야 할 곳도 하나 늡니다.
        </p>
        <p style={{ marginTop: -6 }}>
          <strong>· 민감 데이터는 캐시 대상에서 먼저 의심하라</strong> — 개인정보·인증 정보를
          캐시에 넣는 순간, DB 접근 통제를 우회해 읽을 수 있는 경로가 생깁니다. 넣어야만
          한다면 TTL을 최소로, 접근 통제는 DB와 동급으로.
        </p>
        <p style={{ marginTop: -6 }}>
          <strong>· 키 설계에 권한을 담아라</strong> — <span className="icode">user:42:portfolio</span>
          처럼 소유자가 키에 박혀 있어야, "남의 캐시를 읽는" 사고를 구조적으로 막기 쉽습니다.
          공유 키에 사용자별 데이터를 섞는 실수가 캐시 보안 사고의 단골이에요.
        </p>
        <p style={{ marginTop: -6 }}>
          <strong>· 캐시 서버 자체를 잠가라</strong> — Redis/Valkey는 기본 설정이 느슨한
          편입니다. 비밀번호 없이 외부에 노출된 Redis는 공격자에게 데이터 열람은 물론 서버
          장악 발판까지 내주는 고전적인 사고 경로예요. 내부망 한정 + 인증은 기본값이어야
          합니다.
        </p>
        <div className="tip">
          <b>설계 감각</b> "이 데이터가 캐시에서 유출되면 DB에서 유출된 것과 피해가 같은가?"
          — 같다면 캐시에도 DB와 같은 수준의 보호를 하거나, 아예 캐시하지 않아야 합니다.
          성능을 위해 복사한 데이터라고 해서 보안 등급까지 복사본 취급하면 안 돼요.
        </div>
      </Section>

      <Section n={7} title="운영과 측정" sub="히트율·메모리·축출 — 숫자 없는 캐시는 믿지 않는다">
        <p>
          캐시는 붙이는 순간이 아니라 <strong>운영되는 내내</strong> 검증돼야 합니다. 트래픽
          패턴이 바뀌면 어제 95%던 히트율이 오늘 60%가 될 수 있고, 데이터가 늘면 메모리가
          차서 멀쩡한 키가 쫓겨나기 시작하거든요. 지켜볼 숫자는 세 가지입니다 —{' '}
          <strong>히트율, 메모리 사용량, 축출(eviction) 수.</strong>
        </p>
        <Code>{CODE_METRICS}</Code>
        <p>
          <strong>축출 정책</strong>도 선택입니다. 메모리가 가득 찼을 때 무엇부터 버릴지 —{' '}
          <span className="icode">allkeys-lru</span>(전체 키 중 가장 오래 안 읽힌 것부터)가
          캐시 용도의 무난한 기본값이에요. LRU(Least Recently Used)는 "최근에 읽힌 건 또
          읽힐 것"이라는 가정인데, 대부분의 조회 패턴과 맞습니다. 반대로 섹션 6의 리프레시
          토큰처럼 <strong>원본으로 쓰는 데이터가 같은 인스턴스에 있다면</strong> 함부로
          축출되면 안 되니, 용도가 섞일수록 인스턴스 분리를 고민해야 하죠.
        </p>
        <p>
          그리고 이 코스의 마무리, 실습입니다. 핵심은 구현이 아니라 <strong>측정</strong>이에요 —
          캐시 도입 전후의 응답시간과 히트율을 숫자로 남겨, "이 캐시는 밥값을 하는가"를
          스스로 판정해 보세요.
        </p>
        <Code>{CODE_LAB}</Code>
        <div className="tip">
          <b>측정이 곧 설득</b> "캐시 붙였더니 빨라진 것 같아요"는 감상이고, "평균 38ms →
          2ms, 히트율 94%, DB 조회 수 16분의 1"은 설계 근거입니다. 반대로 측정했더니 히트율이
          40%라면? 캐시를 떼자고 말할 수 있는 것도 숫자를 가진 사람뿐이에요. 고급 과정의
          모든 결정은 이렇게 숫자로 시작하고 숫자로 끝납니다.
        </div>
      </Section>

      <div className="step-card">
        <div className="step-body">
          <h3 style={{ marginBottom: 6 }}>⚡ 여기까지 왔다면</h3>
          <p className="muted">
            이제 캐시가 '빨라지는 마법'이 아니라 <strong>무효화와 일관성의 값을 치르고 사는
            속도</strong>라는 걸 알게 됐어요. 쓰기 전 세 가지 질문, 로컬과 Redis의 자리,
            삭제가 기본인 무효화, Cache-Aside라는 기본형, 스탬피드를 흩는 법, 그리고 "감당
            가능한 불일치"라는 설계 언어까지 — 우리 플랫폼의 시세 캐시와 토큰 저장소가 왜
            그렇게 생겼는지 읽어 낼 수 있게 됐습니다. 그런데 섹션 7에서 계속 강조한 그{' '}
            <strong>숫자</strong>, 응답시간과 병목은 어떻게 정확히 재는 걸까요? 평균의 함정,
            p99라는 언어, 프로파일러로 병목을 짚는 법 — 다음{' '}
            <Link to="/learn/lv3-performance-profiling"><strong>성능 측정과 프로파일링</strong></Link>{' '}
            코스에서, 캐시를 붙일지 말지를 판정하던 그 '측정'을 본격적인 기술로 벼려 봅시다.
          </p>
        </div>
      </div>
    </div>
  );
}
