mirim-app/frontend/src/pages/levels/Lv3PerformanceProfilingPage.jsx
AWESOMEDEV c5f875d990
All checks were successful
CI / backend-test (push) Successful in 1m1s
CI / frontend-build (push) Successful in 42s
CI / backend-dep-scan (push) Successful in 30s
feat(learn): 수준별 학습 과정 4종 — 초급·중급·고급·특급 48강좌 + 퀴즈 240문항
기존 84강좌는 수습용으로 유지하고, 헤더 셀렉트 박스로 과정을 전환한다.

과정 구성 (각 12강좌, 배우는 순서대로):
- 초급 — 코딩 첫걸음: 프로그램의 개념→HTML→JS→변수·조건·반복·함수→에러읽기→첫 게임
- 중급 — 실전 웹 개발: 비동기·배열→API설계→Express CRUD→DB·트랜잭션→인증→React 실전→테스트→배포
- 고급 — 아키텍처·성능·보안: 레이어드·패턴→인덱스·격리→캐싱·최적화→OWASP·인증설계→관측·무중단배포
- 특급 — 대규모 시스템: 분산기초·CAP→메시지큐→MSA→샤딩→대용량 설계→k8s→SRE→시스템설계 3연작→ADR

구현:
- trackContext(localStorage 유지) + 헤더 TrackSelect + 테스트 5개
- levelCatalog(lv1-~lv4- 슬러그, 기존과 충돌 없음) + 라우트·퀴즈 자동 연결
- LearnHubPage 과정 인식(과정별 인사말·진도), CertificatePage 분모 버그 선제 수정
- 퀴즈 240문항: quizzes.json 415→655 + 운영 DB 삽입(655/131 확인)

생성 파이프라인: 설계 4 → 작성 48(병렬) → 검수 4 에이전트.
검수 BLOCKER 2건(백틱 충돌·OpenAPI 표기) 수정 후 빌드 통과.
운영 검증: 4개 과정 전환·강좌 렌더·퀴즈 로드·선택 유지 브라우저 확인, 스모크 11/11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:13:25 +09:00

445 lines
27 KiB
JavaScript

// 이 파일이 하는 일: "성능 측정과 프로파일링" 코스 — 감으로 하는 최적화가 왜 실패하는지에서
// 출발해, 백분위 지표·부하 테스트·JVM 프로파일링·병목 진단·상시 관측·성능 예산까지 7개 섹션으로
// 안내하는 정적 학습 페이지. 프레이밍은 "추측하지 말고 측정하라" — 서비스를 이미 만들어 본
// 수습이, 잘 만드는 법(설계·성능·보안)으로 넘어가는 고급 과정의 성능 편이다.
// 학습 포인트: API 호출이 없는 '내용 고정' 페이지다. 스타일은 styles/global.css의
// 가이드 전용 클래스(step-card, code-block, tip 등)를 재사용하고 색은 var(--*)만 쓴다(하드코딩 금지).
// 예제 코드는 JSX 중괄호/백틱 충돌을 피하려고 파일 상단의 백틱 문자열 상수로 정의한다.
import { Link } from 'react-router-dom';
// ── 예제 코드 상수들 ──
const CODE_LOOP = `느낌으로 하는 최적화 측정으로 하는 최적화
───────────────────── ─────────────────────
"여기가 느릴 것 같아" ① 측정: 지금 얼마나 느린가? (기준선)
→ 일주일 코드 수술 ② 분석: 어디서 시간을 쓰는가? (프로파일)
→ ...별로 안 빨라짐 ③ 개선: 가장 큰 병목 '하나만' 고친다
→ 코드만 복잡해짐 ④ 재측정: 정말 빨라졌나? 숫자로 확인
→ ①로 돌아가 반복
핵심: 병목은 대개 예상과 다른 곳에 있다.
"쿼리가 느리겠지" 했는데 직렬화가 범인이고,
"코드가 느리겠지" 했는데 커넥션 풀 대기가 범인인 일이 흔하다.
그래서 순서가 생명 — 고치기 전에 반드시 잰다.`;
const CODE_PERCENTILE = `어떤 API의 응답 시간 100건 (ms):
대부분 50ms 근처, 그런데 5건이 2,000ms
평균 = 약 147ms ← "음, 나쁘지 않네?" (거짓말)
p50 = 52ms ← 절반의 사용자는 52ms 이하
p95 = 1,980ms ← 상위 5%는 2초 가까이 기다림!
p99 = 2,050ms ← 100명 중 1명은 2초 넘게 기다림
p95 = "요청 100건을 느린 순으로 세웠을 때, 95번째까지는 이 시간 안에 끝났다"
평균은 소수의 아주 느린 요청을 다수의 빠른 요청 속에 숨겨 버린다.
사용자가 체감하는 '느리다'는 평균이 아니라 꼬리(p95·p99)에서 온다.
하루 100만 요청이면 p99 밖의 1%만 해도 1만 건 — 결코 적지 않다.`;
const CODE_K6 = `// load-test.js — k6로 핵심 API에 현실적인 부하 만들기
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 10 }, // ① 웜업: 가볍게 몸 풀기 (JIT·캐시 덥히기)
{ duration: '1m', target: 100 }, // ② 램프업: 서서히 100 VU까지 올리기
{ duration: '3m', target: 100 }, // ③ 지속: 100 VU를 3분간 유지 (본 측정 구간)
{ duration: '30s', target: 0 }, // ④ 램프다운
],
thresholds: {
http_req_duration: ['p(95)<300'], // p95가 300ms를 넘으면 테스트 실패!
http_req_failed: ['rate<0.01'], // 에러율 1% 미만
},
};
export default function () {
const res = http.get('http://localhost:8080/api/market/quotes/005930');
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1); // 실제 사용자처럼 요청 사이에 숨 고르기
}
// 실행: k6 run load-test.js
// 끝나면 p90·p95 응답시간, 초당 요청수(TPS), 에러율이 요약되어 나온다.`;
const CODE_PROFILER = `# async-profiler — 돌고 있는 JVM에 붙여서 30초간 CPU 샘플링
./asprof -d 30 -f cpu-profile.html <PID>
# → 불꽃 그래프(flame graph) HTML 생성. 가로로 넓은 함수 = CPU를 많이 먹는 함수
# 할당(alloc) 프로파일 — "누가 객체를 이렇게 많이 만드나?" (GC 압박의 범인 찾기)
./asprof -e alloc -d 30 -f alloc-profile.html <PID>
# 힙 덤프 — 메모리 누수 의심 시, 힙의 스냅샷을 떠서 분석 도구로 연다
jcmd <PID> GC.heap_dump /tmp/heap.hprof
# GC 로그 켜기 (JVM 옵션) — stop-the-world가 언제 얼마나 길었는지 기록
-Xlog:gc*:file=gc.log:time,uptime
# gc.log에서 이런 줄을 찾는다:
# Pause Young (Normal) ... 12.3ms ← 짧은 젊은 세대 수거, 정상
# Pause Full ... 1843.7ms ← 1.8초 전체 멈춤! 이 동안 모든 요청이 정지
# Full GC가 잦다면: 힙이 작거나, 누수가 있거나, 객체를 너무 많이 만드는 것.`;
const CODE_POOL = `# 커넥션 풀 포화의 전형적 증상 — 코드는 멀쩡한데 p95만 치솟는다
# HikariCP 기본 풀 크기: 10개. 동시 요청 100개가 들어오면?
# → 10개는 DB 작업, 90개는 '커넥션 나올 때까지 줄서기'
# → 프로파일러엔 안 잡힌다 (CPU를 안 쓰고 그냥 기다리니까)
# → 풀 대기 시간을 재는 지표를 따로 봐야 보인다
spring:
datasource:
hikari:
maximum-pool-size: 10 # 무작정 늘리면 DB가 먼저 쓰러진다
connection-timeout: 3000 # 3초 기다려도 못 얻으면 예외 (무한 대기 금지)
# 스레드 풀도 같은 원리 — 톰캣 기본 최대 200 스레드.
# 스레드 하나가 느린 외부 API를 '동기로' 3초씩 기다리면,
# 200개가 전부 기다림에 묶이는 순간 서버는 새 요청을 아예 못 받는다.
# 게이트웨이처럼 모든 트래픽이 지나는 길목에서 이게 터지면 전 서비스가 멈춘다.`;
const CODE_MICROMETER = `// Micrometer — 코드 안에 '측정 지점'을 심어 두는 표준 계측 라이브러리
// Spring Boot Actuator를 넣으면 기본 지표(HTTP·JVM·풀)는 공짜로 나온다.
// 우리 서비스 고유의 지표는 직접 심는다:
@Service
public class QuoteService {
private final Counter cacheMiss; // ① 카운터: 늘어나기만 하는 누적값
private final Timer fetchTimer; // ② 타이머: 횟수 + 걸린 시간 분포(백분위까지)
public QuoteService(MeterRegistry registry, TickBuffer buffer) {
this.cacheMiss = registry.counter("quote.cache.miss");
this.fetchTimer = registry.timer("quote.fetch");
// ③ 게이지: '지금 이 순간'의 값을 들여다보는 창 (오르내림)
registry.gauge("tick.buffer.size", buffer, TickBuffer::size);
}
public Quote fetch(String symbol) {
return fetchTimer.record(() -> { // 이 블록의 소요 시간이 자동 기록
Quote hit = cache.get(symbol);
if (hit != null) return hit;
cacheMiss.increment(); // 캐시 미스 1건 추가
return loadFromUpstream(symbol);
});
}
}
// 구분법: 몇 번? → 카운터 / 얼마나 걸렸나? → 타이머 / 지금 얼마나? → 게이지`;
const CODE_BUDGET = `# 성능 예산(performance budget) — 기능마다 미리 정해 두는 '속도의 상한선'
기능 예산(p95) 근거
──────────────────────────────────────────────────────
시세 단건 조회 API 150ms 화면 갱신 체감 한계
관심종목 목록 (로그인 직후) 300ms 첫 화면 이탈률과 직결
공시 검색 800ms '검색'은 사용자가 조금 기다려 줌
SSE 첫 tick 도착 2s 연결 후 빈 화면 허용 한도
# CI에 회귀 감시 붙이기 — 예산을 어기면 머지가 막힌다
# (k6 thresholds가 곧 예산의 코드화 — 섹션 3의 p(95)<300이 그것)
perf-check:
script:
- k6 run --quiet load-test.js # thresholds 위반 시 exit code 99
rules:
- if: $CI_MERGE_REQUEST_ID # MR마다 자동 실행
# 예산 안에 들어왔으면? 거기서 멈춘다.
# 150ms 예산에 120ms면 충분 — 80ms로 줄이는 일주일은 다른 기능에 쓰는 게 낫다.`;
// ── 이 페이지 안에서만 쓰는 작은 부품들 ──
// 학습 포인트: 같은 모양이 반복되면 컴포넌트로 뽑는다. 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: 'JVM 프로파일링' },
{ n: 5, label: '앱 병목' },
{ n: 6, label: '상시 관측' },
{ n: 7, label: '성능 예산' },
];
export default function Lv3PerformanceProfilingPage() {
return (
<div>
{/* 히어로: 이 코스가 어디서 출발해 어디까지 가는지 */}
<div className="hero">
<div className="eyebrow">Course · 고급 과정</div>
<h1>📈 성능 측정과<br />프로파일링</h1>
<p>
추측하지 말고 측정하라 <strong>병목은 예상 밖에 숨어 있습니다.</strong>{' '}
"여기가 느릴 것 같은데"라는 감으로 시작한 최적화는 대개 엉뚱한 곳을 수술하고
끝나요. 코스에선 <strong>측정분석개선재측정</strong> ,
부하 테스트로 숫자를 얻고, 프로파일러로 범인을 지목하는 법을 배웁니다.
</p>
<div className="chip-row">
<span className="chip">예상 소요 65</span>
<span className="chip">실습: 부하 p95 측정 병목 3 지목</span>
<span className="chip">선수 지식: Spring Boot·DB 튜닝 기초</span>
</div>
</div>
{/* 섹션 바로가기 */}
<nav className="chip-row">
{SECTIONS.map((s) => (
<a key={s.n} className="chip" href={`#sec-${s.n}`}>
<b>{s.n}</b> {s.label}
</a>
))}
</nav>
<Section n={1} title="추측하지 말고 측정하라" sub="성급한 최적화가 비싼 이유">
<p>
성능 이야기의 출발점은 유명한 격언 하나예요. <strong>"성급한 최적화는 만악의
근원이다."</strong> '' ?
치르기 때문입니다. <strong> 시간</strong>
쓰고, <strong> 복잡도</strong> ,{' '}
<strong> 착각</strong> " " 믿
남습니다.
</p>
<Code>{CODE_LOOP}</Code>
<p>
병목이 예상과 다른 곳에 있는 우연이 아니에요. 우리가 코드를 읽을 눈에
띄는 <strong>복잡해 보이는 로직</strong>, {' '}
<strong>기다림</strong> DB , API, I/O
많거든요. 기다림은 코드에 보입니다. 그래서 잽니다.
</p>
<p>
루프에서 하나 눈여겨볼 : <strong>③에서 병목을 '하나만' 고친다</strong>
부분이에요. 번에 군데를 고치면, 재측정에서 빨라져도{' '}
<strong>무엇이 효과였는지 없어요.</strong>
똑같습니다.
</p>
<div className="tip">
<b>오해 방지</b> " " " " .
인덱스 없는 테이블, 무한정 쌓이는 캐시처럼 <strong>나중에 고치기 어려운 구조적
선택</strong> . '
튜닝'이지, '상식적인 설계'가 아니에요.
</div>
</Section>
<Section n={2} title="무엇을 측정하나" sub="평균은 거짓말을 한다 — p95·p99를 보라">
<p>
측정하기로 했으면 다음 질문은 "무엇을?"입니다. 가장 먼저 떠오르는 건{' '}
<strong>평균 응답 시간</strong>이겠지만, 평균에는 치명적인 함정이 있어요.
</p>
<Code>{CODE_PERCENTILE}</Code>
<p>
그래서 실무에선 <strong>백분위(percentile)</strong>를 봅니다.{' '}
<span className="icode">p95</span>는 "95%의 요청이 이 시간 안에 끝났다",{' '}
<span className="icode">p99</span>는 "99%가 이 안에 끝났다"는 뜻이에요. 느린
꼬리를 숨길 수 없죠. 지연시간 목표를 말할 땐 반드시{' '}
<strong>"p95 300ms"처럼 백분위로</strong> 말하는 습관을 들이세요.
</p>
<p>
지연시간(latency) 말고도 함께 봐야 할 기본 지표가 셋 있습니다.{' '}
<strong>① 처리량(throughput)</strong> — 초당 몇 건을 처리하나(TPS),{' '}
<strong>② 에러율</strong> — 빨라도 실패하면 의미 없으니까,{' '}
<strong>③ 자원 사용률</strong> — CPU·메모리·커넥션이 얼마나 차 있나. 넷은 서로를
비추는 거울이에요. 예를 들어 에러율이 0인데 p95만 치솟는다면 "죽진 않았지만
어딘가에서 줄을 서고 있다"는 신호입니다.
</p>
<div className="tip">
<b>레이턴시 vs 스루풋</b> 이 둘은 종종 <strong>맞바꿈(trade-off)</strong> 관계예요.
요청을 모아서 한꺼번에 처리(배치)하면 처리량은 오르지만 개별 요청의 대기는
길어지고, 반대로 매 건 즉시 처리하면 지연은 짧지만 총 처리량이 줄 수 있어요.
"무엇이 더 중요한 API인가"를 먼저 정해야 튜닝의 방향이 정해집니다 — 시세
스트림은 지연이, 야간 정산 배치는 처리량이 왕이죠.
</div>
</Section>
<Section n={3} title="부하 테스트" sub="k6로 현실적인 부하를 만들어 숫자를 얻는다">
<p>
p95를 재려면 요청이 많이 필요해요. 혼자 브라우저로 새로고침 몇 번 해서는 아무것도
알 수 없죠. 그래서 <strong>부하 테스트 도구</strong>로 가상 사용자(VU)를 만들어
동시에 요청을 퍼붓습니다. 대표 주자는 <strong>k6</strong>(스크립트가 JS라
가볍고 CI 친화적)와 <strong>JMeter</strong>(GUI 기반, 오래된 만큼 기능이 방대)
— 이 코스에선 k6로 갑니다.
</p>
<Code>{CODE_K6}</Code>
<p>
시나리오의 4단계엔 각각 이유가 있어요. <strong>웜업</strong>을 건너뛰면 JVM의
JIT 컴파일러가 코드를 아직 기계어로 덥히기 전이라, 실제보다 훨씬 느린 숫자가
나옵니다(캐시도 비어 있고요). <strong>램프업</strong>은 갑자기 100명을 때려 넣는
대신 서서히 올려서 "몇 명부터 무너지는가"를 관찰하게 해 주고,{' '}
<strong>지속 구간</strong>이 진짜 측정 구간 — 안정 상태(steady state)의 p95가
우리가 원하는 숫자입니다.
</p>
<p>
한 가지 주의: <strong>SSE·스트리밍 엔드포인트</strong>는 일반 API처럼 재면
안 돼요. 요청-응답이 짧게 끝나는 게 아니라 <strong>연결을 오래 물고 있는</strong>{' '}
모델이라, "응답 시간"보다 <strong>동시 연결 수를 몇 개까지 버티나</strong>,{' '}
<strong>첫 이벤트가 도착할 때까지 얼마나 걸리나</strong>가 진짜 질문입니다.
연결 1만 개가 각자 스레드를 하나씩 잡는 구조라면 그게 곧 한계가 되죠.
</p>
<div className="tip">
<b>운영 서버에 쏘지 마세요</b> 부하 테스트는 이름 그대로 서버를{' '}
<strong>고의로 한계까지 미는 행위</strong>예요. 반드시 로컬이나 스테이징 같은
격리된 환경에서 하고, 공용 환경이라면 팀에 미리 알리세요. 남의 서비스에 무단으로
쏘면 공격(DoS)과 구분되지 않습니다.
</div>
</Section>
<Section n={4} title="JVM 프로파일링" sub="불꽃 그래프로 핫스팟, 힙 덤프로 누수, GC 로그로 멈춤">
<p>
부하 테스트가 "<strong>얼마나</strong> 느린가"를 알려 줬다면, 프로파일러는
"<strong>어디서</strong> 느린가"를 알려 줍니다. JVM 진영의 사실상 표준은{' '}
<strong>async-profiler</strong> — 돌고 있는 프로세스에 붙여서 주기적으로 "지금 뭐
하고 있어?"를 묻고(샘플링), 그 답을 쌓아{' '}
<strong>불꽃 그래프(flame graph)</strong>로 그려 줘요. 가로로 넓은 함수일수록
CPU 시간을 많이 쓴 함수 — 넓은 막대를 따라가면 핫스팟이 나옵니다.
</p>
<Code>{CODE_PROFILER}</Code>
<p>
CPU만 보면 절반이에요. <span className="icode">-e alloc</span>으로{' '}
<strong>할당 프로파일</strong>을 뜨면 "누가 객체를 쏟아내는가"가 보이는데, 객체를
많이 만들수록 GC가 자주 돌고, GC가 돌수록 앱이 자주 멈추니까요. 메모리가 계속
우상향으로 차오르며 안 내려온다면 <strong>누수</strong> 의심 —{' '}
<strong>힙 덤프</strong>를 떠서 "어떤 타입의 객체가 몇 개 살아 있고, 누가 그걸
붙들고 있나"를 추적합니다.
</p>
<p>
마지막 조각은 <strong>GC 로그</strong>예요. GC 중에서도{' '}
<strong>stop-the-world</strong> — 수거하는 동안 <strong>모든 애플리케이션
스레드가 정지</strong>하는 구간 — 이 길면, 그 순간 들어온 요청 전부가 그만큼
지연됩니다. "p99만 이상하게 튀는" 미스터리의 단골 범인이 바로 이 Full GC예요.
로그에서 <span className="icode">Pause Full</span>이 자주, 길게 보인다면 힙 크기·
할당 패턴·누수를 순서대로 의심하세요.
</p>
<div className="tip">
<b>샘플링이라 부담이 적다</b> async-profiler는 모든 호출을 기록하는 게 아니라
주기적으로 스냅샷을 뜨는 <strong>샘플링 방식</strong>이라 오버헤드가 작아요.
그래서 부하 테스트 중인 서버에 붙여도 측정 자체가 결과를 크게 왜곡하지 않습니다
— "측정이 대상을 바꾸면 안 된다"는 원칙에 맞는 도구죠.
</div>
</Section>
<Section n={5} title="애플리케이션 병목" sub="쿼리·직렬화·풀 포화 — 자주 나오는 범인 도감">
<p>
프로파일을 떠 보면 병목은 대개 몇 가지 익숙한 얼굴로 나타나요. 첫째 그룹은{' '}
<strong>데이터 접근</strong> — 인덱스 없는 느린 쿼리, 목록 하나 그리는데 쿼리가
N+1번 나가는 패턴(앞 강 <strong>DB 심화</strong>에서 다룬 그 녀석들), 그리고
거대한 객체 그래프를 통째로 JSON으로 바꾸는 <strong>과한 직렬화</strong>. 불꽃
그래프에서 직렬화 라이브러리가 넓게 차지하고 있다면, 응답에 안 쓰는 필드까지
퍼 나르고 있을 확률이 높습니다 — DTO로 필요한 만큼만 담으세요.
</p>
<p>
둘째 그룹이 더 교묘해요. <strong>풀(pool) 포화</strong> — 코드가 아니라
'줄서기'가 병목인 경우입니다.
</p>
<Code>{CODE_POOL}</Code>
<p>
이 유형이 무서운 이유는 <strong>CPU 프로파일에 안 잡히기 때문</strong>이에요.
기다리는 스레드는 CPU를 안 쓰니까 불꽃 그래프에선 조용하죠. 그래서 풀 지표
(활성 커넥션 수·대기 시간·대기 큐 길이)를 <strong>따로</strong> 봐야 합니다 —
다음 섹션의 상시 관측이 필요한 이유이기도 해요.
</p>
<p>
셋째, <strong>동기 블로킹 호출의 전염</strong>. 서비스 하나가 느려지는 건 사고가
아니지만, 그 느린 서비스를 <strong>모든 트래픽이 지나는 게이트웨이가 동기로
기다리면</strong> 이야기가 달라져요. 게이트웨이의 스레드 풀이 그 기다림에 전부
묶이는 순간, 멀쩡한 다른 서비스로 가는 요청까지 통째로 막힙니다. 한 칸의 정체가
고속도로 전체의 정체가 되는 거죠. 타임아웃을 짧게 걸고, 실패가 반복되면 아예
빠르게 포기하는 방어선이 필요합니다.
</p>
<div className="tip">
<b>실습 힌트</b> 이 코스의 실습 — "부하를 주고 p95를 잰 뒤 상위 병목 3개 지목" —
에서 병목 후보를 찾을 땐 이 도감 순서로 훑으세요. ① 불꽃 그래프의 넓은 막대
(쿼리·직렬화) → ② 풀 지표(커넥션·스레드 대기) → ③ GC 로그(Pause Full). 세 곳을
보면 열에 아홉은 걸립니다.
</div>
</Section>
<Section n={6} title="지표 상시 관측" sub="Micrometer로 코드 안에 측정 지점을 심는다">
<p>
부하 테스트와 프로파일링은 <strong>일회성 검사</strong>예요. 건강검진처럼
유용하지만, 검진일 사이에 생기는 문제는 못 봅니다. 그래서 잘 만든 서비스는
지표를 <strong>상시로</strong> 내보내요 — 그 표준 도구가{' '}
<strong>Micrometer</strong>입니다. Spring Boot Actuator를 붙이면 HTTP 응답
시간·JVM 메모리·커넥션 풀 같은 기본 지표는 자동으로 나오고, 우리 도메인 고유의
지표는 코드에 직접 심습니다.
</p>
<Code>{CODE_MICROMETER}</Code>
<p>
세 가지 계측기의 구분이 핵심이에요. <strong>카운터</strong>는 늘어나기만 하는
누적값(총 캐시 미스 횟수, 총 에러 수) — 그 자체보다 <strong>증가 속도</strong>를
봅니다. <strong>타이머</strong>는 횟수에 더해 <strong>소요 시간의 분포</strong>까지
기록해서 p95·p99를 뽑을 수 있고요. <strong>게이지</strong>는 지금 이 순간의 값
(버퍼 크기, 활성 연결 수)을 들여다보는 창이라 오르내립니다. "몇 번?"은 카운터,
"얼마나 걸렸나?"는 타이머, "지금 얼마나?"는 게이지 — 이 한 줄만 기억하세요.
</p>
<div className="tip">
<b>다음 강으로 이어지는 다리</b> 심어 둔 지표는 어딘가에 모아서 그래프로 보고,
이상하면 알림을 받아야 비로소 쓸모가 있죠. 그 수집·시각화·경보의 세계
(Prometheus·Grafana)가 <strong>모니터링 강</strong>의 주제입니다. 이번 강에서
지표를 <strong>심는 법</strong>을 익혀 두면, 다음 강에선 그걸{' '}
<strong>읽고 대응하는 법</strong>으로 자연스럽게 이어져요.
</div>
</Section>
<Section n={7} title="성능 예산과 회귀 방지" sub="어디까지 빠르면 충분한가 — 멈출 지점을 정한다">
<p>
마지막 질문은 의외로 이겁니다. <strong>"언제 멈추나?"</strong> 최적화는 중독성이
있어서, 기준이 없으면 끝없이 파고들게 돼요. 그래서 잘하는 팀은 기능마다{' '}
<strong>성능 예산(performance budget)</strong> — "이 API는 p95 300ms까지 허용" —
을 <strong>미리</strong> 정해 둡니다. 예산이 있으면 두 가지가 명확해져요. 예산을
넘으면 <strong>고쳐야 하고</strong>, 예산 안이면 <strong>멈춰도 됩니다.</strong>
</p>
<Code>{CODE_BUDGET}</Code>
<p>
예산의 진짜 힘은 <strong>회귀(regression) 방지</strong>에서 나와요. 성능은 한 번
좋게 만들어 놓아도, 기능이 추가될 때마다 조금씩 무거워지다가 어느 날 갑자기
"느려졌어요"가 터집니다. 그 '조금씩'을 잡으려면 사람의 기억이 아니라{' '}
<strong>CI가 감시</strong>해야 해요 — 섹션 3에서 본 k6의{' '}
<span className="icode">thresholds</span>가 바로 예산의 코드화입니다. MR마다
자동으로 부하를 걸고, p95가 예산을 넘으면 빌드를 깨뜨리는 거죠.
</p>
<p>
그리고 멈출 용기. 예산이 150ms인데 지금 120ms라면, 80ms로 줄이겠다고 일주일을
쓰는 건 <strong>최적화가 아니라 낭비</strong>예요. 그 일주일은 예산을 넘긴 다른
API나, 아예 새로운 기능에 쓰는 게 팀 전체로 이득입니다. 성능 작업도 결국{' '}
<strong>우선순위 게임</strong> — 숫자가 있어야 그 판단이 가능해지고, 그래서 이
코스의 처음과 끝이 같은 문장인 거예요. 추측하지 말고, 측정하라.
</p>
<div className="tip">
<b>예산은 사용자에게서 거꾸로</b> 예산 숫자는 서버 사정이 아니라{' '}
<strong>사용자 경험에서 역산</strong>하세요. "시세 화면은 0.15초 안에 떠야
거래에 지장이 없다" → 그게 API 예산이 됩니다. 기술이 아니라 제품이 기준을 정하는
것 — 고급 과정 전체를 관통하는 감각이에요.
</div>
</Section>
<div className="step-card">
<div className="step-body">
<h3 style={{ marginBottom: 6 }}>📈 여기까지 왔다면</h3>
<p className="muted">
이제 "느려요"라는 말을 들었을 때 코드부터 뒤지는 대신, <strong>측정→분석→개선→
재측정</strong>의 루프를 돌릴 수 있게 됐어요. p95로 말하고, k6로 부하를 만들고,
불꽃 그래프와 GC 로그로 범인을 지목하고, Micrometer로 측정 지점을 심고, 성능
예산으로 멈출 지점까지 정하는 것 — 잘 만드는 개발자의 성능 감각을 손에
쥐었습니다. 그런데 서비스가 빨라지고 커질수록, 노리는 사람도 많아져요. 다음{' '}
<Link to="/learn/lv3-web-security"><strong>웹 보안 심화 — OWASP Top 10</strong></Link>{' '}
코스에서, 공격자의 눈으로 우리 서비스를 살펴보는 법을 이어서 익혀 봅시다.
</p>
</div>
</div>
</div>
);
}