<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>고민보단 실천을</title>
    <link>https://bigtae1007.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 11 Apr 2026 00:24:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Just-Do-It</managingEditor>
    <item>
      <title>이미지 업로드 파이프라인 최적화: 리사이징, 썸네일, WebP/AVIF 변환을 어디서 처리할까</title>
      <link>https://bigtae1007.tistory.com/458</link>
      <description>&lt;h1&gt;이미지 업로드 파이프라인 최적화: 리사이징, 썸네일, WebP/AVIF 변환을 어디서 처리할까&lt;/h1&gt;&lt;p&gt;이미지 업로드는 저장만 되는 순간 끝나는 기능이 아니다. 원본 보관, 파생 이미지 생성, 포맷 변환, 캐시 전략까지 연결돼야 비용과 성능이 안정된다.&lt;/p&gt;&lt;p&gt;중급 설계에서는 클라이언트, 엣지, 서버, 비동기 워커 중 어디에서 어떤 처리를 할지 역할을 나눠야 한다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;원본을 그대로 서비스하면 대역폭과 LCP가 모두 악화된다.&lt;/li&gt;&lt;li&gt;모든 변환을 동기 요청에서 처리하면 업로드 성공률과 응답 시간이 떨어진다.&lt;/li&gt;&lt;li&gt;포맷 변환은 성능 개선 도구이지만 브라우저 호환성과 저장 비용을 함께 고려해야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;업로드 경로는 원본 저장과 메타데이터 기록을 우선 완료하고, 변환은 비동기 워커로 넘긴다.&lt;/li&gt;&lt;li&gt;썸네일, 리스트용, 상세용처럼 실제 사용 크기 기준으로 파생 이미지를 정의한다.&lt;/li&gt;&lt;li&gt;WebP/AVIF는 클라이언트 지원 범위와 CDN 협상을 고려해 선택한다.&lt;/li&gt;&lt;li&gt;원본과 파생 이미지 키 규칙을 맞춰 purge와 재생성을 쉽게 만든다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;client upload -&amp;gt; object storage(original)&lt;br&gt;original stored -&amp;gt; queue publish(image_id)&lt;br&gt;worker -&amp;gt; resize 320/768/1280 + WebP/AVIF variants&lt;br&gt;CDN -&amp;gt; Accept header 기반 서빙 또는 명시적 파일 경로 사용&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;실제 화면에서 필요한 이미지 크기와 포맷을 먼저 목록화한다.&lt;/li&gt;&lt;li&gt;업로드 성공 경로와 변환 파이프라인을 분리해 요청 시간을 줄인다.&lt;/li&gt;&lt;li&gt;비동기 워커에 재시도, 실패 보상, 중복 실행 방지 키를 넣는다.&lt;/li&gt;&lt;li&gt;CDN 캐시 키와 format negotiation 전략을 설계한다.&lt;/li&gt;&lt;li&gt;실제 네트워크 환경에서 LCP와 전송량 개선을 측정한다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;변환 실패 이미지를 재처리할 수 있는 운영 툴이 필요하다.&lt;/li&gt;&lt;li&gt;클라이언트 업로드 직후에는 placeholder 또는 original fallback 전략을 둔다.&lt;/li&gt;&lt;li&gt;이미지 품질 설정은 감으로 정하지 말고 실제 파일 크기와 시각 품질을 비교한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;업로드 성공률과 처리 파이프라인 지연&lt;/li&gt;&lt;li&gt;리사이즈/변환 큐 적체와 실패 비율&lt;/li&gt;&lt;li&gt;원본/파생 이미지 저장 비용과 CDN hit ratio&lt;/li&gt;&lt;li&gt;모바일/저대역폭 환경에서의 LCP 변화&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# 업로드 원본 크기/형식 상위 10개 확인&lt;br&gt;# 변환 worker 처리 시간과 실패율 비교&lt;br&gt;# CDN hit/miss와 이미지 포맷 협상 결과 점검&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;4bf92f...&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;req_123&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/example&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 200,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;모든 포맷 변환을 동기 업로드 요청에서 처리한다: 성공률이 떨어진다.&lt;/li&gt;&lt;li&gt;화면 요구와 무관한 크기를 많이 만든다: 저장 비용과 캐시 효율이 나빠진다.&lt;/li&gt;&lt;li&gt;브라우저 호환성 검증 없이 AVIF만 밀어붙인다: 특정 환경에서 깨진다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;이미지 파이프라인 템플릿:&lt;br&gt;original 저장 -&amp;gt; async transform&lt;br&gt;variant size 목록(thumb/list/detail/hero)&lt;br&gt;format policy(WebP/AVIF/JPEG fallback)&lt;br&gt;재처리 및 purge 절차&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;저대역폭 모바일 환경에서 이미지 최적화 전후 LCP와 전송량을 비교한다.&lt;/li&gt;&lt;li&gt;변환 워커 장애 후 재처리 절차로 누락된 파생 이미지가 복구되는지 확인한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;이미지 업로드 파이프라인 최적화: 리사이징, 썸네일, WebP/AVIF 변환을 어디서 처리할까&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#avif_image&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;MDN: AVIF image format&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#webp_image&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;MDN: WebP image format&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>avif</category>
      <category>backend</category>
      <category>CDN</category>
      <category>frontend</category>
      <category>image-upload</category>
      <category>media-processing</category>
      <category>Performance</category>
      <category>thumbnail</category>
      <category>WebP</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/458</guid>
      <comments>https://bigtae1007.tistory.com/458#entry458comment</comments>
      <pubDate>Fri, 10 Apr 2026 20:59:22 +0900</pubDate>
    </item>
    <item>
      <title>GraphQL 성능 튜닝: N+1 문제, DataLoader, Persisted Query로 병목 줄이기</title>
      <link>https://bigtae1007.tistory.com/457</link>
      <description>&lt;h1&gt;GraphQL 성능 튜닝: N+1 문제, DataLoader, Persisted Query로 병목 줄이기&lt;/h1&gt;&lt;p&gt;GraphQL은 필요한 데이터를 정확히 요청할 수 있게 해 주지만, 서버가 자동으로 효율적인 쿼리를 만들어 주는 것은 아니다.&lt;/p&gt;&lt;p&gt;중급 운영에서는 스키마 설계, resolver 호출 패턴, 캐싱, persisted query를 함께 다뤄야 실제 성능이 좋아진다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;N+1 문제는 작은 데이터셋에서는 안 보이다가 실서비스에서 폭발한다.&lt;/li&gt;&lt;li&gt;복잡한 쿼리는 DB 병목뿐 아니라 네트워크 payload와 캐시 무효화 비용도 키운다.&lt;/li&gt;&lt;li&gt;쿼리 자유도가 높을수록 서버는 guardrail을 명확히 둬야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;resolver는 각각 독립적으로 DB를 때리지 않도록 DataLoader 또는 batch layer를 둔다.&lt;/li&gt;&lt;li&gt;persisted query를 사용해 쿼리 길이와 캐시 키를 안정화한다.&lt;/li&gt;&lt;li&gt;depth/complexity 제한을 두어 과도한 쿼리를 차단한다.&lt;/li&gt;&lt;li&gt;필드별 비용이 큰 경우 캐시 가능한 읽기 모델을 분리한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const userLoader = new DataLoader(async (ids) =&amp;gt; {&lt;br&gt;  const rows = await db.users.findByIds(ids);&lt;br&gt;  return ids.map((id) =&amp;gt; rows.find((row) =&amp;gt; row.id === id));&lt;br&gt;});&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;상위 10개 GraphQL query와 resolver 호출 패턴을 먼저 측정한다.&lt;/li&gt;&lt;li&gt;N+1이 심한 경로에 DataLoader를 적용하고, 배치 키를 설계한다.&lt;/li&gt;&lt;li&gt;persisted query를 도입해 캐시와 보안 정책을 단순화한다.&lt;/li&gt;&lt;li&gt;complexity limit를 붙이고 운영 알람 기준을 정한다.&lt;/li&gt;&lt;li&gt;쿼리 변경 시 DB 플랜과 payload 크기를 같이 검토한다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;GraphQL query 이름(operationName)을 로그에 남겨야 병목 분석이 가능하다.&lt;/li&gt;&lt;li&gt;persisted query 캐시 미스율이 높으면 배포/빌드 파이프라인을 점검한다.&lt;/li&gt;&lt;li&gt;schema 변경 시 클라이언트 쿼리 수집 데이터를 함께 본다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;버전별 트래픽 비율과 미지원 버전 접근 수&lt;/li&gt;&lt;li&gt;deprecation/sunset 헤더 적용률&lt;/li&gt;&lt;li&gt;클라이언트 오류율(4xx)과 호환성 이슈 건수&lt;/li&gt;&lt;li&gt;변경 후 고객사별 migration 진행률&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -I https://example.com/api/resource&lt;br&gt;curl -H &amp;#x27;Accept: application/vnd.example.v2+json&amp;#x27; https://example.com/api/resource&lt;br&gt;rg &amp;#x27;Sunset|Deprecation|api-version&amp;#x27; ./logs -n&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;4bf92f...&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;req_123&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/example&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 200,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;DataLoader를 전역 싱글턴으로 둔다: 요청 간 캐시 오염이 생길 수 있다.&lt;/li&gt;&lt;li&gt;복잡도 제한 없이 외부 공개 GraphQL을 연다: 비용 예측이 어려워진다.&lt;/li&gt;&lt;li&gt;persisted query를 도입하면서 버전 관리가 없다: 배포 직후 미스가 늘어난다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;GraphQL 성능 템플릿:&lt;br&gt;operationName 로깅&lt;br&gt;요청 스코프 DataLoader&lt;br&gt;persisted query + hash registry&lt;br&gt;depth/complexity limit&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;같은 쿼리에서 resolver 호출 횟수가 DataLoader 전후로 줄었는지 확인한다.&lt;/li&gt;&lt;li&gt;persisted query 강제 후 미등록 쿼리가 적절히 차단되는지 검증한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;GraphQL 성능 튜닝: N+1 문제, DataLoader, Persisted Query로 병목 줄이기&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://graphql.org/learn/&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;GraphQL Learn&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.apollographql.com/docs/react/data/persisted-queries&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Apollo Persisted Queries&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>API</category>
      <category>backend</category>
      <category>DataLoader</category>
      <category>frontend</category>
      <category>graphql</category>
      <category>n-plus-one</category>
      <category>Performance</category>
      <category>persisted-query</category>
      <category>Schema</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/457</guid>
      <comments>https://bigtae1007.tistory.com/457#entry457comment</comments>
      <pubDate>Fri, 10 Apr 2026 19:59:53 +0900</pubDate>
    </item>
    <item>
      <title>Feature Flag 운영 가이드: 점진 배포, A/B 테스트, Kill Switch를 안전하게 설계하는 방법</title>
      <link>https://bigtae1007.tistory.com/456</link>
      <description>&lt;h1&gt;Feature Flag 운영 가이드: 점진 배포, A/B 테스트, Kill Switch를 안전하게 설계하는 방법&lt;/h1&gt;&lt;p&gt;Feature Flag는 배포를 기능 공개와 분리해 주지만, 규칙 없이 늘리면 두 번째 설정 시스템이 된다.&lt;/p&gt;&lt;p&gt;중급 팀에서 중요한 것은 '플래그를 만드는 법'보다 '언제 제거하고, 누가 소유하며, 장애 때 어떻게 끄는가'를 정의하는 것이다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;배포와 공개를 분리하면 리스크를 줄일 수 있지만, 오래된 플래그는 코드 복잡도를 폭발시킨다.&lt;/li&gt;&lt;li&gt;점진 배포와 실험은 목적이 다르다. 하나의 플래그에 두 목적을 섞으면 해석이 꼬인다.&lt;/li&gt;&lt;li&gt;kill switch가 없다면 플래그는 비상 장치가 아니라 장식에 가깝다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;release flag, experiment flag, ops flag를 타입별로 구분한다.&lt;/li&gt;&lt;li&gt;타깃 규칙은 사용자 속성, 지역, 앱 버전처럼 안정적인 차원을 우선 사용한다.&lt;/li&gt;&lt;li&gt;기본값과 fallback을 코드에 남기고, 원격 설정 실패 시 동작을 명확히 한다.&lt;/li&gt;&lt;li&gt;플래그 만료일과 제거 owner를 생성 시점에 같이 기록한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;flag: checkout_redesign&lt;br&gt;type: release&lt;br&gt;default: false&lt;br&gt;targeting: internal-users -&amp;gt; 5% cohort -&amp;gt; country=KR 25%&lt;br&gt;kill-switch: true면 즉시 기존 checkout으로 fallback&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;기능 위험도에 따라 플래그 타입과 소유 팀을 정한다.&lt;/li&gt;&lt;li&gt;0% -&gt; 내부 사용자 -&gt; 5% -&gt; 25% -&gt; 100%처럼 단계별 rollout 계획을 만든다.&lt;/li&gt;&lt;li&gt;각 단계에서 보는 성공 지표와 rollback 기준을 미리 적는다.&lt;/li&gt;&lt;li&gt;릴리스 후 만료된 플래그를 정리하는 정기 리뷰를 캘린더에 넣는다.&lt;/li&gt;&lt;li&gt;실험용 플래그는 분석 이벤트와 함께 설계해 해석 충돌을 막는다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;플래그 목록에 owner, 생성일, 만료일, 대체 코드 위치를 포함한다.&lt;/li&gt;&lt;li&gt;모든 플래그 변경은 감사 로그와 알림 채널에 남긴다.&lt;/li&gt;&lt;li&gt;운영 플래그는 UI 클릭만 믿지 말고 API/CLI로도 비상 전환 가능해야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;flag 평가 실패율과 fallback 사용 비율&lt;/li&gt;&lt;li&gt;점진 배포 단계별 에러율/전환율 변화&lt;/li&gt;&lt;li&gt;kill switch 발동 횟수와 복구 시간&lt;/li&gt;&lt;li&gt;환경별 설정 drift 여부&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# 환경별 flag 기본값과 targeting rule diff 확인&lt;br&gt;# kill switch가 없는 flag 목록이 있는지 점검&lt;br&gt;# rollout 대상 세그먼트와 실제 트래픽이 일치하는지 확인&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;4bf92f...&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;req_123&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/example&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 200,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실험과 장애 대응 플래그를 하나로 만든다: 롤백 기준이 충돌한다.&lt;/li&gt;&lt;li&gt;플래그를 제거하지 않는다: 코드 경로가 두 배가 된다.&lt;/li&gt;&lt;li&gt;원격 설정 실패 시 기본값이 정의되지 않았다: 장애 때 더 큰 장애가 난다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Feature Flag 템플릿:&lt;br&gt;name / type / owner / createdAt / expiresAt&lt;br&gt;defaultValue / fallbackBehavior&lt;br&gt;rolloutPlan(단계별 대상, 지표, rollback 기준)&lt;br&gt;cleanupTicket(제거 일정)&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;원격 설정 서버가 응답하지 않을 때 fallback이 의도대로 동작하는지 확인한다.&lt;/li&gt;&lt;li&gt;kill switch를 켠 뒤 1~2분 내 트래픽과 에러율이 안정화되는지 리허설한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;Feature Flag 운영 가이드: 점진 배포, A/B 테스트, Kill Switch를 안전하게 설계하는 방법&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://openfeature.dev/docs/&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;OpenFeature Documentation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.getunleash.io/&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Unleash Documentation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>ab-test</category>
      <category>backend</category>
      <category>experimentation</category>
      <category>feature-flag</category>
      <category>frontend</category>
      <category>kill-switch</category>
      <category>Operations</category>
      <category>progressive-delivery</category>
      <category>Release</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/456</guid>
      <comments>https://bigtae1007.tistory.com/456#entry456comment</comments>
      <pubDate>Fri, 10 Apr 2026 15:59:43 +0900</pubDate>
    </item>
    <item>
      <title>Elasticsearch 샤드 설계 실전: shard/replica 개수와 검색 성능을 같이 보는 기준</title>
      <link>https://bigtae1007.tistory.com/455</link>
      <description>&lt;h1&gt;Elasticsearch 샤드 설계 실전: shard/replica 개수와 검색 성능을 같이 보는 기준&lt;/h1&gt;&lt;p&gt;Elasticsearch는 샤드를 많이 쪼갠다고 빨라지지 않는다. 작은 샤드가 많아질수록 메타데이터, merge, relocation 비용이 눈에 띄게 커진다.&lt;/p&gt;&lt;p&gt;중급 운영에서는 인덱스 설계를 데이터 크기와 검색 패턴이 아니라, 장애 복구 시간과 운영 인력까지 포함해 봐야 한다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;샤드 수는 성능뿐 아니라 장애 복구 시간과 노드 증설 전략에 직접 영향을 준다.&lt;/li&gt;&lt;li&gt;replica는 가용성 도구이면서 검색 처리량 레버이기도 하다.&lt;/li&gt;&lt;li&gt;hot shard를 방치하면 특정 노드만 포화되고 클러스터 전체 성능이 흔들린다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;인덱스당 샤드 수는 예상 총 데이터와 일 단위 증가량, 보관 정책으로 산정한다.&lt;/li&gt;&lt;li&gt;샤드 크기는 너무 작지도, 너무 크지도 않게 유지해야 merge와 relocation이 감당 가능하다.&lt;/li&gt;&lt;li&gt;시간 기반 데이터는 rollover와 ILM으로 관리하고, 고정 도메인 데이터는 검색 패턴 중심으로 나눈다.&lt;/li&gt;&lt;li&gt;replica는 읽기 분산과 장애 복구 목표를 기준으로 결정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;settings&amp;quot;: {&lt;br&gt;    &amp;quot;number_of_shards&amp;quot;: 3,&lt;br&gt;    &amp;quot;number_of_replicas&amp;quot;: 1,&lt;br&gt;    &amp;quot;index.lifecycle.name&amp;quot;: &amp;quot;logs-hot-warm&amp;quot;&lt;br&gt;  }&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;현재 인덱스별 데이터 크기와 일일 증가량을 측정한다.&lt;/li&gt;&lt;li&gt;읽기/쓰기 비율과 장애 시 복구 목표 시간을 기준으로 샤드 목표 크기를 정한다.&lt;/li&gt;&lt;li&gt;rollover, index template, ILM 정책을 함께 설계한다.&lt;/li&gt;&lt;li&gt;hot shard 여부를 대시보드로 추적하고 재인덱싱 기준을 문서화한다.&lt;/li&gt;&lt;li&gt;운영 중 샤드 증감은 비용이 크므로 초반에 작은 PoC로 검증한다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;샤드 재배치가 잦은 시간대에는 롤오버와 대규모 배포를 겹치지 않는다.&lt;/li&gt;&lt;li&gt;template 변경 전후로 새 인덱스에만 반영되는지 확인한다.&lt;/li&gt;&lt;li&gt;검색 latency와 인덱싱 throughput을 분리해서 본다. 둘은 자주 충돌한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;검색 지연(p95/p99)과 timeout 비율&lt;/li&gt;&lt;li&gt;shard size, segment merge, relocation 횟수&lt;/li&gt;&lt;li&gt;refresh/replication 지연과 인덱싱 처리량&lt;/li&gt;&lt;li&gt;hot shard 여부와 노드별 디스크 사용률&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -s http://localhost:9200/_cat/shards?v&lt;br&gt;curl -s http://localhost:9200/_cluster/health?pretty&lt;br&gt;curl -s http://localhost:9200/index/_stats?pretty&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;4bf92f...&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;req_123&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/example&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 200,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;작게 쪼개면 무조건 빠를 거라 믿는다: 오히려 관리 비용이 급증한다.&lt;/li&gt;&lt;li&gt;replica를 0으로 두고 운영한다: 노드 장애가 곧 서비스 장애가 된다.&lt;/li&gt;&lt;li&gt;hot shard를 애플리케이션 키 설계 탓으로만 본다: 실제로는 인덱스 전략이 원인인 경우가 많다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;샤드 설계 체크리스트:&lt;br&gt;총 데이터 크기 / 일 증가량 / 보관 기간&lt;br&gt;목표 샤드 크기 / replica 수 / rollover 기준&lt;br&gt;장애 복구 시간 목표(RTO)와 재인덱싱 허용 여부&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;샤드 재배치 또는 노드 장애를 가정해 클러스터가 목표 시간 안에 회복되는지 확인한다.&lt;/li&gt;&lt;li&gt;실제 검색 쿼리 상위 10개로 p95 지연이 목표 범위인지 측정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;Elasticsearch 샤드 설계 실전: shard/replica 개수와 검색 성능을 같이 보는 기준&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Elastic: Size your shards&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/index-lifecycle-management.html&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Elastic: Index lifecycle management&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>backend</category>
      <category>Elasticsearch</category>
      <category>index-design</category>
      <category>Operations</category>
      <category>Performance</category>
      <category>replica</category>
      <category>scaling</category>
      <category>search</category>
      <category>shard</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/455</guid>
      <comments>https://bigtae1007.tistory.com/455#entry455comment</comments>
      <pubDate>Fri, 10 Apr 2026 14:59:23 +0900</pubDate>
    </item>
    <item>
      <title>분산 스케줄링 설계: Cron, Quartz, ShedLock 중 어떤 방식이 운영에 유리한가</title>
      <link>https://bigtae1007.tistory.com/454</link>
      <description>&lt;h1&gt;분산 스케줄링 설계: Cron, Quartz, ShedLock 중 어떤 방식이 운영에 유리한가&lt;/h1&gt;&lt;p&gt;스케줄러는 단순해 보이지만, 인스턴스가 두 대만 넘어가도 '누가 실행할 것인가'가 운영 이슈가 된다.&lt;/p&gt;&lt;p&gt;중급 설계에서는 스케줄 실행 정확도보다 중복 실행 방지, 실패 재시도, 관측 가능성을 먼저 다뤄야 한다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;단일 인스턴스 cron은 간단하지만 장애 조치가 취약하다.&lt;/li&gt;&lt;li&gt;Quartz는 강력하지만 운영 복잡도와 상태 저장 비용이 따른다.&lt;/li&gt;&lt;li&gt;ShedLock은 간단한 분산 락 기반이지만 정밀한 스케줄 엔진을 대체하진 못한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;단순 반복 작업이면 플랫폼 수준 cron(Kubernetes CronJob 포함)부터 검토한다.&lt;/li&gt;&lt;li&gt;정확한 일정, misfire 처리, 달력 기반 스케줄이 필요하면 Quartz가 유리하다.&lt;/li&gt;&lt;li&gt;기존 애플리케이션 내부 작업을 최소 변경으로 분산 환경에 올리려면 ShedLock이 현실적이다.&lt;/li&gt;&lt;li&gt;어떤 방식을 택하든 실행 이력과 중복 실행 감지를 남겨야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cron: 플랫폼이 주기적으로 컨테이너 실행&lt;br&gt;Quartz: trigger + job store + misfire policy&lt;br&gt;ShedLock: @Scheduled + distributed lock + max lock duration&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;작업 종류를 단순 반복, 시간 정밀, 장시간 실행, 고가용성으로 분류한다.&lt;/li&gt;&lt;li&gt;중복 실행 허용 여부와 최대 실행 시간, 재시도 정책을 정한다.&lt;/li&gt;&lt;li&gt;락 저장소(DB/Redis) 또는 scheduler 저장소의 가용성을 확인한다.&lt;/li&gt;&lt;li&gt;실행 성공률, misfire, 지연을 모니터링하는 대시보드를 만든다.&lt;/li&gt;&lt;li&gt;수동 재실행 절차와 비상 중지 절차를 문서화한다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;NTP와 timezone이 어긋나면 스케줄러 디버깅이 매우 어려워진다.&lt;/li&gt;&lt;li&gt;장시간 실행 작업은 락 만료와 heartbeat 전략을 같이 설계한다.&lt;/li&gt;&lt;li&gt;실패한 작업을 수동으로 다시 돌릴 수 있는 운영 도구가 필요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;실행 성공률과 misfire 횟수&lt;/li&gt;&lt;li&gt;중복 실행 감지 건수와 락 획득 실패율&lt;/li&gt;&lt;li&gt;스케줄 지연 시간과 backlog 증가량&lt;/li&gt;&lt;li&gt;실행 노드별 편중 여부&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# 최근 24시간 misfire/중복 실행 로그 확인&lt;br&gt;# 락 키별 획득 실패 건수와 재시도 간격 점검&lt;br&gt;# 노드 시계 오차(NTP)와 timezone 설정 확인&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;job processed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;8d3f...&amp;quot;,&lt;br&gt;  &amp;quot;eventId&amp;quot;: &amp;quot;evt_123&amp;quot;,&lt;br&gt;  &amp;quot;queue&amp;quot;: &amp;quot;outbox-relay&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: &amp;quot;DONE&amp;quot;,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 184,&lt;br&gt;  &amp;quot;retryAttempt&amp;quot;: 0&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;분산 환경에서 단순 `@Scheduled`만 쓴다: 인스턴스 수만큼 중복 실행된다.&lt;/li&gt;&lt;li&gt;락 만료 시간을 너무 짧게 잡는다: 작업 중복 실행 가능성이 커진다.&lt;/li&gt;&lt;li&gt;실행 이력을 저장하지 않는다: 실패 재현과 감사가 불가능하다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;스케줄링 선택 템플릿:&lt;br&gt;중복 실행 허용 여부&lt;br&gt;최대 실행 시간 / 재시도 정책 / 수동 재실행 절차&lt;br&gt;락 또는 job store 종류&lt;br&gt;모니터링 지표(misfire, success, duration)&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;인스턴스를 2대로 늘린 뒤 같은 시각에 작업이 한 번만 실행되는지 검증한다.&lt;/li&gt;&lt;li&gt;작업 도중 프로세스 종료 시 락 만료와 재실행이 의도대로 동작하는지 확인한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;분산 스케줄링 설계: Cron, Quartz, ShedLock 중 어떤 방식이 운영에 유리한가&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.quartz-scheduler.org/documentation/&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Quartz Scheduler Documentation&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/lukas-krecan/ShedLock&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;ShedLock Documentation&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>architecture</category>
      <category>backend</category>
      <category>batch</category>
      <category>cron</category>
      <category>distributed-system</category>
      <category>Operations</category>
      <category>Quartz</category>
      <category>Scheduler</category>
      <category>shedlock</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/454</guid>
      <comments>https://bigtae1007.tistory.com/454#entry454comment</comments>
      <pubDate>Thu, 9 Apr 2026 20:59:55 +0900</pubDate>
    </item>
    <item>
      <title>DB 커넥션 풀 튜닝 실전: HikariCP maximumPoolSize를 감으로 정하면 안 되는 이유</title>
      <link>https://bigtae1007.tistory.com/453</link>
      <description>&lt;h1&gt;DB 커넥션 풀 튜닝 실전: HikariCP maximumPoolSize를 감으로 정하면 안 되는 이유&lt;/h1&gt;&lt;p&gt;커넥션 풀은 많을수록 좋지 않다. DB가 동시에 처리할 수 있는 양보다 커넥션이 더 많아지면 대기열만 늘어난다.&lt;/p&gt;&lt;p&gt;중급 운영에서는 애플리케이션 스레드 수와 DB 동시 실행 능력, 쿼리 특성을 함께 보고 풀 크기를 정해야 한다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;커넥션 부족은 바로 느껴지지만, 과도한 풀 크기는 조용히 DB를 포화시킨다.&lt;/li&gt;&lt;li&gt;풀 대기 시간은 애플리케이션 병목인지 DB 병목인지 구분하는 핵심 지표다.&lt;/li&gt;&lt;li&gt;thread pool, HTTP timeout, transaction 길이와 연결하지 않으면 숫자만 바꾸는 튜닝이 된다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;pool size는 DB CPU 코어 수와 평균 쿼리 시간, 동시성 모델을 기준으로 계산한다.&lt;/li&gt;&lt;li&gt;connection timeout은 사용자 요청 timeout보다 짧아야 한다.&lt;/li&gt;&lt;li&gt;읽기/쓰기 풀 분리나 bulk job 전용 풀을 통해 서로 다른 워크로드를 격리한다.&lt;/li&gt;&lt;li&gt;긴 트랜잭션과 N+1 쿼리를 먼저 줄이지 않으면 풀 확장은 임시 처치에 그친다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;spring:&lt;br&gt;  datasource:&lt;br&gt;    hikari:&lt;br&gt;      maximum-pool-size: 24&lt;br&gt;      minimum-idle: 8&lt;br&gt;      connection-timeout: 2000&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;현재 Hikari 지표(active, idle, pending, acquire time)를 수집한다.&lt;/li&gt;&lt;li&gt;DB slow query와 app thread dump를 같이 확인해 병목 위치를 구분한다.&lt;/li&gt;&lt;li&gt;커넥션 풀 크기를 소폭 조정하며 p95 acquire time 변화를 비교한다.&lt;/li&gt;&lt;li&gt;배치 잡과 온라인 요청이 같은 풀을 쓰는지 점검하고 필요하면 분리한다.&lt;/li&gt;&lt;li&gt;최종 값은 문서와 코드 설정에 같이 남긴다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;풀 크기를 올리기 전에 쿼리 최적화와 transaction length를 먼저 본다.&lt;/li&gt;&lt;li&gt;DB max_connections와 애플리케이션 인스턴스 수를 같이 계산한다.&lt;/li&gt;&lt;li&gt;장애 시 pending thread가 폭증하는지 대시보드에 노출한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;event loop lag 또는 GC pause time&lt;/li&gt;&lt;li&gt;CPU 사용률, 스레드/핸들러 대기 시간&lt;/li&gt;&lt;li&gt;메모리 사용량과 heap pressure&lt;/li&gt;&lt;li&gt;timeout/retry 비율과 다운스트림 오류 상관관계&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# event loop lag 또는 GC pause 로그를 먼저 본다&lt;br&gt;# CPU 100% 구간과 외부 호출 timeout 구간을 같이 본다&lt;br&gt;# 병목 함수/핫스팟이 요청 경로와 직접 연결되는지 확인&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;4bf92f...&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;req_123&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/example&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 200,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;응답이 느리니 pool size부터 늘린다: DB 포화를 더 빠르게 만든다.&lt;/li&gt;&lt;li&gt;connection timeout이 너무 길다: 사용자 요청이 불필요하게 오래 매달린다.&lt;/li&gt;&lt;li&gt;batch job과 API 트래픽이 같은 풀을 쓴다: 서로의 tail latency를 망친다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;풀 튜닝 템플릿:&lt;br&gt;DB max_connections / 앱 인스턴스 수 / 목표 pool size&lt;br&gt;connection timeout &amp;lt; request timeout&lt;br&gt;active/pending/acquire time 대시보드&lt;br&gt;긴 쿼리 상위 N개 점검&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;부하 테스트에서 acquire time p95가 줄었는지, 동시에 DB CPU가 포화되지 않는지 확인한다.&lt;/li&gt;&lt;li&gt;일부 인스턴스 재시작 상황에서도 DB max_connections를 넘지 않는지 검증한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;DB 커넥션 풀 튜닝 실전: HikariCP maximumPoolSize를 감으로 정하면 안 되는 이유&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;HikariCP Configuration&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/data/sql.html&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Spring Boot DataSource Configuration&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>backend</category>
      <category>connection-pool</category>
      <category>database</category>
      <category>hikaricp</category>
      <category>Java</category>
      <category>Operations</category>
      <category>Performance</category>
      <category>Spring</category>
      <category>tuning</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/453</guid>
      <comments>https://bigtae1007.tistory.com/453#entry453comment</comments>
      <pubDate>Thu, 9 Apr 2026 19:59:31 +0900</pubDate>
    </item>
    <item>
      <title>Cache-Control 고급 활용: stale-while-revalidate, stale-if-error로 체감 성능 높이기</title>
      <link>https://bigtae1007.tistory.com/452</link>
      <description>&lt;h1&gt;Cache-Control 고급 활용: stale-while-revalidate, stale-if-error로 체감 성능 높이기&lt;/h1&gt;&lt;p&gt;응답을 즉시 보여주면서도 완전히 오래된 데이터만 내보내지 않는 절충안이 필요할 때 `stale-while-revalidate`가 빛난다.&lt;/p&gt;&lt;p&gt;중급 팀에서는 캐시 시간을 늘리는 것이 아니라, '신선도와 가용성을 분리해서 다루는 방식'으로 Cache-Control을 설계해야 한다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;캐시를 너무 짧게 두면 원본 서버 부하가 커지고, 너무 길게 두면 신선도 문제가 생긴다.&lt;/li&gt;&lt;li&gt;`stale-if-error`는 장애 시 사용자 체감 품질을 지키는 강력한 수단이다.&lt;/li&gt;&lt;li&gt;브라우저, CDN, 프록시가 각각 캐시 헤더를 다르게 소비하기 때문에 의도를 명확히 해야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;`max-age`는 일반 정상 상태의 신선도, `stale-while-revalidate`는 백그라운드 재검증 허용 범위다.&lt;/li&gt;&lt;li&gt;`stale-if-error`는 원본 장애 시 얼마나 오래 이전 응답을 보여줄지 정의한다.&lt;/li&gt;&lt;li&gt;ETag/Last-Modified와 함께 쓰면 네트워크 비용과 정합성 균형이 좋아진다.&lt;/li&gt;&lt;li&gt;로그인 사용자, 개인화 응답은 공용 캐시와 명확히 분리한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;Cache-Control: public, max-age=60, stale-while-revalidate=300, stale-if-error=600&lt;br&gt;ETag: &amp;quot;post-list-v42&amp;quot;&lt;br&gt;Vary: Accept-Encoding&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;정적/준정적/개인화 응답을 먼저 분류한다.&lt;/li&gt;&lt;li&gt;각 응답에 허용 가능한 stale window와 오류 시 fallback 시간을 정한다.&lt;/li&gt;&lt;li&gt;브라우저와 CDN 정책을 별도 테스트 환경에서 검증한다.&lt;/li&gt;&lt;li&gt;배포 후 hit ratio와 origin offload 비율을 관측하며 값을 조정한다.&lt;/li&gt;&lt;li&gt;중요 페이지는 stale 응답이 비즈니스에 미치는 영향을 제품 팀과 합의한다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;개인화 응답에 `public` 캐시를 붙이지 않도록 라우트별 검증을 둔다.&lt;/li&gt;&lt;li&gt;CDN이 `stale-while-revalidate`를 어떻게 해석하는지 문서와 실제 동작을 함께 확인한다.&lt;/li&gt;&lt;li&gt;원본 서버 장애 시 stale 응답 제공 여부를 대시보드에서 확인할 수 있어야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;캐시 hit ratio와 stale 응답 비율&lt;/li&gt;&lt;li&gt;원본 저장소 QPS/지연과 캐시 도입 전후 비교&lt;/li&gt;&lt;li&gt;stampede 방지 키의 lock contention&lt;/li&gt;&lt;li&gt;메모리 사용량과 eviction 비율&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;redis-cli INFO stats | rg &amp;#x27;keyspace_hits|keyspace_misses&amp;#x27;&lt;br&gt;redis-cli TTL cache:user:123&lt;br&gt;redis-cli --latency-history&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;4bf92f...&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;req_123&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/example&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 200,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;max-age만 키우고 끝낸다: 신선도와 장애 대응이 분리되지 않는다.&lt;/li&gt;&lt;li&gt;캐시 가능한 응답과 아닌 응답을 라우트 수준에서 분리하지 않는다.&lt;/li&gt;&lt;li&gt;ETag 재검증 없이 stale 응답만 늘린다: 잘못된 값이 오래 남을 수 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Cache-Control 템플릿:&lt;br&gt;public/private&lt;br&gt;max-age=초&lt;br&gt;stale-while-revalidate=초&lt;br&gt;stale-if-error=초&lt;br&gt;ETag 또는 Last-Modified 병행&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;원본 서버를 의도적으로 오류 상태로 만들어 stale-if-error가 실제로 동작하는지 확인한다.&lt;/li&gt;&lt;li&gt;재검증 시 304 비율과 origin 응답시간 감소 효과를 비교한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;Cache-Control 고급 활용: stale-while-revalidate, stale-if-error로 체감 성능 높이기&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;MDN: Cache-Control&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5861&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;RFC 5861&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>backend</category>
      <category>Cache-Control</category>
      <category>CDN</category>
      <category>frontend</category>
      <category>http-cache</category>
      <category>Performance</category>
      <category>stale-if-error</category>
      <category>stale-while-revalidate</category>
      <category>web</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/452</guid>
      <comments>https://bigtae1007.tistory.com/452#entry452comment</comments>
      <pubDate>Thu, 9 Apr 2026 14:59:47 +0900</pubDate>
    </item>
    <item>
      <title>API 버저닝 전략 실전: URI vs Header vs Media Type, 언제 갈아타야 하는가</title>
      <link>https://bigtae1007.tistory.com/451</link>
      <description>&lt;h1&gt;API 버저닝 전략 실전: URI vs Header vs Media Type, 언제 갈아타야 하는가&lt;/h1&gt;&lt;p&gt;API 버저닝은 URL 취향 싸움이 아니라, 호환성 비용을 어떤 계층에서 감당할지 정하는 선택이다.&lt;/p&gt;&lt;p&gt;중급 팀에서는 신규 버전을 만드는 기준, 이전 버전 폐기 절차, 고객사 마이그레이션 가시성까지 포함해 설계해야 한다.&lt;/p&gt;&lt;h2 id=&quot;why&quot;&gt;왜 지금 이 주제가 중요한가&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;버전 전략이 없으면 하위 호환성 부담이 코드 전체로 퍼진다.&lt;/li&gt;&lt;li&gt;버전이 많아질수록 운영/문서/모니터링 비용이 함께 증가한다.&lt;/li&gt;&lt;li&gt;중요한 것은 어디에 버전을 넣느냐보다 언제 breaking change로 판단하느냐이다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;design&quot;&gt;핵심 설계 포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;URI 버저닝은 명시적이고 디버깅이 쉽지만 경로 중복이 커질 수 있다.&lt;/li&gt;&lt;li&gt;Header 또는 media type 버저닝은 자원 식별을 유지하지만 클라이언트와 게이트웨이 지원을 점검해야 한다.&lt;/li&gt;&lt;li&gt;breaking change 기준과 sunset timeline을 문서/헤더/공지 채널로 함께 관리한다.&lt;/li&gt;&lt;li&gt;버전별 트래픽을 관측할 수 있어야 실제 폐기가 가능하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;example&quot;&gt;예시 구성&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /v2/orders/123&lt;br&gt;Accept: application/vnd.example.orders.v2+json&lt;br&gt;Sunset: Wed, 31 Dec 2026 23:59:59 GMT&lt;br&gt;Deprecation: true&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;설계 자체보다도 '작게 도입하고 관측하면서 확장하는 순서'가 운영 성공률을 좌우한다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;지금까지 발생한 breaking change 사례를 모아 버전 정책 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;신규 버전 노출 방식(URI, header, media type)을 API 게이트웨이 제약과 함께 검토한다.&lt;/li&gt;&lt;li&gt;Deprecation/Sunset 헤더, 문서, 고객 공지 템플릿을 만든다.&lt;/li&gt;&lt;li&gt;버전별 사용량 대시보드를 만들고, 지원 종료 기준을 운영 정책으로 박는다.&lt;/li&gt;&lt;li&gt;핵심 SDK 또는 샘플 코드도 버전 전환 흐름에 맞춰 같이 업데이트한다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;지원 종료 전 고객사별 버전 사용량을 확인할 수 있어야 한다.&lt;/li&gt;&lt;li&gt;새 버전 배포 전 SDK/문서/예제 코드가 함께 준비돼야 한다.&lt;/li&gt;&lt;li&gt;버전별 알람과 에러율을 분리해 회귀를 빠르게 잡는다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;버전별 트래픽 비율과 미지원 버전 접근 수&lt;/li&gt;&lt;li&gt;deprecation/sunset 헤더 적용률&lt;/li&gt;&lt;li&gt;클라이언트 오류율(4xx)과 호환성 이슈 건수&lt;/li&gt;&lt;li&gt;변경 후 고객사별 migration 진행률&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -I https://example.com/api/resource&lt;br&gt;curl -H &amp;#x27;Accept: application/vnd.example.v2+json&amp;#x27; https://example.com/api/resource&lt;br&gt;rg &amp;#x27;Sunset|Deprecation|api-version&amp;#x27; ./logs -n&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId처럼 흐름을 이어주는 키를 남긴다.&lt;/li&gt;&lt;li&gt;endpoint/topic/flag/version 등 주제별 핵심 차원을 구조화한다.&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)를 분리한다.&lt;/li&gt;&lt;li&gt;민감정보는 마스킹하고, payload는 샘플링 또는 요약 저장한다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;traceId&amp;quot;: &amp;quot;4bf92f...&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;req_123&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/example&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 200,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대한 성공 경로와 상태 전이가 유지되는지&lt;br&gt;2) 실패: 다운스트림 오류/잘못된 입력이 예측 가능한 에러로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청 또는 이벤트가 반복돼도 부작용이 없는지&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 장애 복구: 프로세스 재시작 후 중간 상태를 정상 회복하는지&lt;br&gt;- 부하: p95/p99와 queue/pool saturation이 임계값 안에 드는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;운영 복잡도를 줄이면 기능 유연성이 떨어질 수 있고, 반대도 마찬가지다.&lt;/li&gt;&lt;li&gt;기본값은 출발점일 뿐이다. 실제 트래픽과 실패 패턴을 보고 다시 조정해야 한다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면 체감 개선과 회귀를 구분하기 어렵다.&lt;/li&gt;&lt;li&gt;팀 경계가 많은 시스템일수록 인터페이스 계약과 문서가 코드만큼 중요하다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;핵심 경로 에러율: 0.1% 이하&lt;/li&gt;&lt;li&gt;핵심 요청/이벤트 p95 지연: 서비스 목표 내 유지&lt;/li&gt;&lt;li&gt;중복 실행 또는 데이터 유실: 0건&lt;/li&gt;&lt;li&gt;장애 감지 후 임시 조치까지 걸리는 시간: 10분 이내&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;버전은 URL에만 있고 정책이 없다: 결국 아무 버전도 지우지 못한다.&lt;/li&gt;&lt;li&gt;사소한 변경까지 버전을 올린다: 문서와 운영 비용만 커진다.&lt;/li&gt;&lt;li&gt;Sunset 공지 없이 구버전을 끊는다: 고객 장애가 곧 신뢰 하락으로 이어진다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;버전 정책 템플릿:&lt;br&gt;breaking change 정의&lt;br&gt;노출 방식(URI/header/media type)&lt;br&gt;Deprecation / Sunset 공지 절차&lt;br&gt;버전별 트래픽 모니터링&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;구버전과 신버전 클라이언트가 같은 배포에서 동시에 동작하는지 통합 테스트한다.&lt;/li&gt;&lt;li&gt;지원 종료 헤더와 문서 링크가 실제 응답에 포함되는지 확인한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 어떤 사용자/서비스/플랫폼에서 무엇이 깨졌는지 한 문장으로 정리한다.&lt;/li&gt;&lt;li&gt;범위: 언제부터 시작됐고, 영향받은 비율과 핵심 경로를 적는다.&lt;/li&gt;&lt;li&gt;증거: 로그 3줄, 지표 1개, 최근 배포/설정 변경 1개를 먼저 모은다.&lt;/li&gt;&lt;li&gt;임시 조치: 차단, 롤백, 스위치 전환, 재시도 제한 중 무엇을 할지 결정한다.&lt;/li&gt;&lt;li&gt;근본 원인: 계약, 타임아웃, 락, 캐시, 버전, 운영 절차 중 어디가 깨졌는지 좁힌다.&lt;/li&gt;&lt;li&gt;재발 방지: 테스트, 알람, 문서, 기본값을 함께 수정한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;실패 시나리오가 문서와 코드에서 같은 의미로 정의돼 있다.&lt;/li&gt;&lt;li&gt;타임아웃/재시도/락/캐시 같은 보호 장치가 상호 충돌하지 않는다.&lt;/li&gt;&lt;li&gt;관측 지표와 상관관계 키가 있어 운영 중 재현이 가능하다.&lt;/li&gt;&lt;li&gt;롤백 또는 비상 스위치가 준비돼 있다.&lt;/li&gt;&lt;li&gt;최소 1개 이상의 동시성/부하/중간 실패 테스트가 자동화돼 있다.&lt;/li&gt;&lt;li&gt;공식 문서 링크와 팀 의사결정 근거가 남아 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 어떤 운영 비용 또는 장애를 줄이려는가&lt;br&gt;2) 범위: API/잡/토픽/디바이스/리전 중 어디까지 적용하는가&lt;br&gt;3) 규칙: 키, 상태, 버전, TTL, timeout, retry 기본값&lt;br&gt;4) 예외: 허용하지 않는 상황과 에러 코드/조치 기준&lt;br&gt;5) 운영: 대시보드, 알람, 소유 팀, 점검 주기&lt;br&gt;6) 장애 대응: 임시 조치, 롤백, 후속 공지 절차&lt;br&gt;7) 변경 이력: 언제 누가 왜 기본값을 바꿨는가&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 처음부터 완벽하게 설계해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 아니다. 핵심 경로 1개부터 적용하고, 운영 지표를 보며 기본값을 보정하는 편이 실제로 더 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;API 버저닝 전략 실전: URI vs Header vs Media Type, 언제 갈아타야 하는가&quot;를 도입했는데도 문제가 남아 있습니다. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 상관관계 키가 있는 로그와 지표로 실패 범위를 좁히고, 최근 배포/설정 차이를 확인한다. 대부분은 기본값보다 경계 조건에서 터진다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 팀 합의가 자꾸 흔들립니다. 무엇을 문서로 남겨야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 상태 전이, 기본값, 예외 처리, 롤백 기준 네 가지는 반드시 남겨야 한다. 이 네 가지가 없으면 장애 때 판단이 흔들린다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;RFC 9110: HTTP Semantics&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/rest/about-the-rest-api/api-versions&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;GitHub REST API Versioning&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>api-versioning</category>
      <category>architecture</category>
      <category>backend</category>
      <category>compatibility</category>
      <category>deprecation</category>
      <category>Header</category>
      <category>HTTP</category>
      <category>media-type</category>
      <category>rest-api</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/451</guid>
      <comments>https://bigtae1007.tistory.com/451#entry451comment</comments>
      <pubDate>Thu, 9 Apr 2026 13:59:21 +0900</pubDate>
    </item>
    <item>
      <title>무중단 스키마 변경(Expand-Contract) 실전: 컬럼 추가/이동/삭제를 안전하게</title>
      <link>https://bigtae1007.tistory.com/450</link>
      <description>&lt;h1&gt;무중단 스키마 변경(Expand-Contract) 실전: 컬럼 추가/이동/삭제를 안전하게&lt;/h1&gt;&lt;p&gt;스키마 변경은 코드 배포보다 무섭다. 데이터가 바뀌면 롤백이 어려워서 '안전한 절차'가 필요하다.&lt;/p&gt;&lt;h2 id=&quot;onepage&quot;&gt;Expand-Contract 한 장 요약&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;Expand: 새 구조 추가(컬럼/테이블/인덱스).&lt;/li&gt;&lt;li&gt;Backfill: 기존 데이터 채우기(청크/멱등).&lt;/li&gt;&lt;li&gt;Dual-write: 일정 기간 구/신 구조 동시 기록.&lt;/li&gt;&lt;li&gt;Switch-read: 읽기를 신 구조로 전환(Feature Flag).&lt;/li&gt;&lt;li&gt;Contract: 구 구조 제거(삭제는 마지막).&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;sql&quot;&gt;예시 SQL(Expand + Backfill)&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;ALTER TABLE users ADD COLUMN first_name TEXT;&lt;br&gt;ALTER TABLE users ADD COLUMN last_name TEXT;&lt;br&gt;UPDATE users&lt;br&gt;SET first_name = split_part(name, &amp;#x27; &amp;#x27;, 1),&lt;br&gt;    last_name  = split_part(name, &amp;#x27; &amp;#x27;, 2)&lt;br&gt;WHERE first_name IS NULL;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;backfill-chunk&quot;&gt;Backfill을 청크로 나누는 이유&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;대용량 UPDATE는 락과 IO를 길게 잡아 서비스 지연을 만들 수 있다.&lt;/li&gt;&lt;li&gt;청크 처리로 락 시간을 줄이고, 중간 실패 시 재시작을 쉽게 한다.&lt;/li&gt;&lt;li&gt;진행률 지표(처리 row 수/속도)를 남기면 운영이 쉬워진다.&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- 예: id 범위로 청크 처리(개념)&lt;br&gt;UPDATE users&lt;br&gt;SET first_name = ..., last_name = ...&lt;br&gt;WHERE id BETWEEN :from AND :to AND first_name IS NULL;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;긴 글을 한 번에 다 적용하기보다, 아래 순서대로 '작게' 넣고 관측하면서 키우는 게 실무에서 성공률이 높다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;현재 상태를 수치로 확인한다(지표/로그/샘플 트래픽).&lt;/li&gt;&lt;li&gt;팀 규칙(키/상태/응답 포맷/설정)을 문서로 고정한다.&lt;/li&gt;&lt;li&gt;핵심 경로 1개(가장 중요한 엔드포인트/잡/토픽)부터 적용한다.&lt;/li&gt;&lt;li&gt;부하/동시성/실패를 재현하는 테스트를 만든다(운영과 비슷하게).&lt;/li&gt;&lt;li&gt;관측(대시보드/알람)을 붙인다: 실패가 '조용히' 넘어가지 않게.&lt;/li&gt;&lt;li&gt;점진적으로 확장한다(적용 범위를 넓히기 전에 효과를 확인).&lt;/li&gt;&lt;li&gt;배포/롤백 계획을 문서화한다(누가, 언제, 어떤 조건에서 되돌릴지).&lt;/li&gt;&lt;li&gt;1~2주 운영 데이터를 보고 규칙/기본값을 재조정한다(처음 값은 대개 틀린다).&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;Contract(삭제) 전에 구 컬럼 참조가 정말 없는지 로그/메트릭으로 확인한다.&lt;/li&gt;&lt;li&gt;Backfill은 멱등하게 만든다(재실행 가능).&lt;/li&gt;&lt;li&gt;대용량 UPDATE는 청크로 나누고 락/IO를 관측한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;에러율(4xx/5xx)과 실패 원인 top N&lt;/li&gt;&lt;li&gt;지연(p95/p99)과 타임아웃 비율&lt;/li&gt;&lt;li&gt;재시도 횟수/비율(있다면)&lt;/li&gt;&lt;li&gt;핵심 비즈니스 지표(성공률 등)와 상관관계&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;p&gt;장애가 났을 때 '어디부터 볼지'가 정해져 있으면 대응 속도가 빨라진다. 아래는 팀에서 그대로 템플릿으로 쓰기 좋은 최소 목록이다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# 지표: 에러율/지연(p95/p99)/타임아웃/재시도 비율 확인&lt;br&gt;# 로그: correlation id로 요청 1건을 end-to-end 추적&lt;br&gt;# 설정: 기본값(타임아웃/리밋/락/인덱스)을 문서에서 재확인&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId 중 하나는 반드시 포함&lt;/li&gt;&lt;li&gt;endpoint/method/status/latencyMs 같은 기본 필드&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)&lt;/li&gt;&lt;li&gt;민감정보 마스킹(토큰/비밀번호/개인정보)&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;...&amp;quot;,&lt;br&gt;  &amp;quot;method&amp;quot;: &amp;quot;POST&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/orders&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 201,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;userId&amp;quot;: 1004,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대 응답/상태 전이가 맞는지&lt;br&gt;2) 실패: 입력 오류/다운스트림 오류가 &amp;#x27;예상한 에러 포맷&amp;#x27;으로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청이 2~3번 들어와도 데이터가 깨지지 않는지(멱등성)&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 지연/타임아웃: 느린 상황에서 서킷/타임아웃이 상한을 지키는지&lt;br&gt;- 재시도 폭주: retry가 장애를 키우지 않는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;기본값은 팀/서비스에 따라 달라진다: 숫자는 '정답'이 아니라 '출발점'이다.&lt;/li&gt;&lt;li&gt;가용성 vs 보호(fail-open vs fail-closed) 결정을 미루면 장애 때 더 큰 혼란이 온다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면: 좋아졌는지 나빠졌는지 판단이 안 된다.&lt;/li&gt;&lt;li&gt;단순한 구현이 항상 좋은 건 아니다: 운영/디버깅 비용까지 합쳐서 판단해야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;에러율: 5xx 0.1% 이하(서비스 특성에 맞게)&lt;/li&gt;&lt;li&gt;지연: p95 300ms 이하(핵심 API 기준)&lt;/li&gt;&lt;li&gt;타임아웃: 전체 요청의 0.01% 이하&lt;/li&gt;&lt;li&gt;중복 실행(멱등): 0건(또는 '부작용 0건')&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;Expand 없이 바로 삭제한다: 구버전이 즉시 터진다.&lt;/li&gt;&lt;li&gt;Backfill 중 서비스 트래픽과 경합한다: 락 대기로 장애가 커진다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;p&gt;팀 문서/코드 리뷰에서 바로 복붙할 수 있게 최소 규격을 템플릿으로 남겨두는 게 반복 작업을 줄인다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Expand-Contract 템플릿:&lt;br&gt;1) Expand -&amp;gt; 2) Backfill -&amp;gt; 3) Dual-write -&amp;gt; 4) Switch-read -&amp;gt; 5) Contract&lt;br&gt;삭제는 마지막, 최소 1회 배포 주기 뒤&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;구버전/신버전 동시 운영 중 읽기/쓰기 호환이 깨지지 않는지 검증한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 무엇이 깨졌나(에러/지연/중복/누락) 한 문장으로 적기&lt;/li&gt;&lt;li&gt;범위: 언제부터/어느 사용자/어느 엔드포인트/어느 파티션인지&lt;/li&gt;&lt;li&gt;증거: 로그 3줄 + 지표 1개로 재현 가능한 단서 만들기&lt;/li&gt;&lt;li&gt;임시 조치: 제한(레이트리밋), 차단(서킷), 롤백/스위치 등&lt;/li&gt;&lt;li&gt;근본 원인: 키/정렬/락/타임아웃/재시도 등 어떤 규칙이 깨졌는지&lt;/li&gt;&lt;li&gt;재발 방지: 테스트 추가 + 대시보드/알람 + 문서 업데이트&lt;/li&gt;&lt;li&gt;후속 조치: 고객 공지/내부 공유(영향 범위, 원인, 재발 방지) 템플릿으로 남기기&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;성공/실패 기준이 수치로 정의돼 있다(지표, 임계값).&lt;/li&gt;&lt;li&gt;입력 검증/에러 처리가 400/409/429 등으로 명확하다(500 남발 금지).&lt;/li&gt;&lt;li&gt;동시성/재시도 상황에서도 데이터가 깨지지 않는다(멱등성/락/유니크).&lt;/li&gt;&lt;li&gt;타임아웃이 상한으로 존재한다(무한 대기 금지).&lt;/li&gt;&lt;li&gt;부하 증가 시 동작이 예측 가능하다(인덱스/큐/풀 고갈 대비).&lt;/li&gt;&lt;li&gt;로그에 상관관계 키(traceId/requestId/eventId)가 있다.&lt;/li&gt;&lt;li&gt;설정 기본값이 문서로 남아 있다(왜 이 값인지).&lt;/li&gt;&lt;li&gt;롤백/비상 조치(runbook)가 준비돼 있다.&lt;/li&gt;&lt;li&gt;테스트에 최소 1개 이상의 실패 케이스가 있다(운영 재현 목적).&lt;/li&gt;&lt;li&gt;참고/출처(공식 문서)로 팀이 더 깊게 확인할 경로가 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 무엇을 해결하나(한 문장)&lt;br&gt;2) 범위: 적용 대상(엔드포인트/잡/토픽/테넌트)&lt;br&gt;3) 규칙: 키/정렬/상태/응답 포맷/기본값&lt;br&gt;4) 예외: 허용하지 않는 케이스(차단/에러 코드)&lt;br&gt;5) 운영: 지표/알람/대시보드 링크&lt;br&gt;6) 장애 대응: 임시 조치 + 롤백 절차&lt;br&gt;7) 변경 이력: 언제/누가/무엇을 바꿨나&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 이걸 도입하면 성능이 무조건 좋아지나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 항상 그렇진 않다. 목표는 보통 '장애 반경 축소'와 '예측 가능성'이다. 성능은 인덱스/타임아웃/풀/캐시처럼 병목을 같이 잡아야 체감이 나온다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;무중단 스키마 변경(Expand-Contract) 실전: 컬럼 추가/이동/삭제를 안전하게&quot;를 적용했는데도 문제가 남아요. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 로그/지표로 실패 지점을 좁히고, 설정/키/인덱스 같은 고정 요소부터 점검한다. 재현 가능한 체크리스트를 먼저 만든다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 운영에서 가장 흔한 실수는요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 규칙을 문서화하지 않고 팀마다 다르게 구현하는 것이다. 같은 이름의 기능이라도 데이터 모델/키/상태 정의가 다르면 장애가 난다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 최소 도입으로 효과를 보려면?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 체크리스트 1~2개만이라도 먼저 적용해 효과가 보이는 변화를 만든 뒤 범위를 넓히는 게 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 테스트를 어디까지 해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 최소로는 (1) 정상 케이스, (2) 실패 케이스, (3) 동시성/재시도 케이스 3가지는 자동화하는 게 좋다. 운영 이슈의 대부분이 3번에서 나온다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://martinfowler.com/bliki/ParallelChange.html&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Martin Fowler: Parallel Change&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.postgresql.org/docs/current/sql-altertable.html&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;PostgreSQL: ALTER TABLE&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>backend</category>
      <category>database</category>
      <category>expand-contract</category>
      <category>feature-flag</category>
      <category>Migration</category>
      <category>MySQL</category>
      <category>PostgreSQL</category>
      <category>rollforward</category>
      <category>zero-downtime</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/450</guid>
      <comments>https://bigtae1007.tistory.com/450#entry450comment</comments>
      <pubDate>Tue, 7 Apr 2026 20:59:54 +0900</pubDate>
    </item>
    <item>
      <title>TypeScript 런타임 검증 실전: Zod로 API 스키마 드리프트 막기</title>
      <link>https://bigtae1007.tistory.com/449</link>
      <description>&lt;h1&gt;TypeScript 런타임 검증 실전: Zod로 API 스키마 드리프트 막기&lt;/h1&gt;&lt;p&gt;TypeScript는 컴파일 타임에만 안전하다. 런타임에는 API가 바뀌면 그대로 터진다. Zod로 '명확히 실패'하게 만들어 조기 발견을 돕는다.&lt;/p&gt;&lt;h2 id=&quot;schema&quot;&gt;스키마 정의&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { z } from &amp;#x27;zod&amp;#x27;;&lt;br&gt;export const UserSchema = z.object({&lt;br&gt;  id: z.number().int().positive(),&lt;br&gt;  email: z.string().email(),&lt;br&gt;  name: z.string().min(1),&lt;br&gt;  createdAt: z.string().datetime(),&lt;br&gt;});&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;parse&quot;&gt;응답 파싱&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const res = await fetch(&amp;#x27;/api/users/1&amp;#x27;);&lt;br&gt;const json = await res.json();&lt;br&gt;const user = UserSchema.parse(json);&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;safeparse&quot;&gt;safeParse를 쓰는 이유&lt;/h2&gt;&lt;p&gt;parse는 예외를 던진다. 화면에서 우아하게 실패하고 싶거나, 에러 리포트를 풍부하게 남기고 싶으면 safeParse가 운영에 유리하다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const parsed = UserSchema.safeParse(json);&lt;br&gt;if (!parsed.success) {&lt;br&gt;  captureException(parsed.error);&lt;br&gt;  throw new Error(&amp;#x27;Invalid API response schema&amp;#x27;);&lt;br&gt;}&lt;br&gt;return parsed.data;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;apply&quot;&gt;적용 순서(실무 플로우)&lt;/h2&gt;&lt;p&gt;긴 글을 한 번에 다 적용하기보다, 아래 순서대로 '작게' 넣고 관측하면서 키우는 게 실무에서 성공률이 높다.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;현재 상태를 수치로 확인한다(지표/로그/샘플 트래픽).&lt;/li&gt;&lt;li&gt;팀 규칙(키/상태/응답 포맷/설정)을 문서로 고정한다.&lt;/li&gt;&lt;li&gt;핵심 경로 1개(가장 중요한 엔드포인트/잡/토픽)부터 적용한다.&lt;/li&gt;&lt;li&gt;부하/동시성/실패를 재현하는 테스트를 만든다(운영과 비슷하게).&lt;/li&gt;&lt;li&gt;관측(대시보드/알람)을 붙인다: 실패가 '조용히' 넘어가지 않게.&lt;/li&gt;&lt;li&gt;점진적으로 확장한다(적용 범위를 넓히기 전에 효과를 확인).&lt;/li&gt;&lt;li&gt;배포/롤백 계획을 문서화한다(누가, 언제, 어떤 조건에서 되돌릴지).&lt;/li&gt;&lt;li&gt;1~2주 운영 데이터를 보고 규칙/기본값을 재조정한다(처음 값은 대개 틀린다).&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;ops&quot;&gt;운영 체크포인트&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;검증 실패를 Sentry 등에 보내 스키마 드리프트를 조기에 발견한다.&lt;/li&gt;&lt;li&gt;스키마는 가장 깨지기 쉬운 경계(외부 API, 핵심 엔드포인트)부터 적용한다.&lt;/li&gt;&lt;li&gt;parse 예외를 앱 전역 에러 처리 규칙으로 묶는다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;metrics&quot;&gt;운영 지표/알람 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;에러율(4xx/5xx)과 실패 원인 top N&lt;/li&gt;&lt;li&gt;지연(p95/p99)과 타임아웃 비율&lt;/li&gt;&lt;li&gt;재시도 횟수/비율(있다면)&lt;/li&gt;&lt;li&gt;핵심 비즈니스 지표(성공률 등)와 상관관계&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;quick-check&quot;&gt;빠른 점검 명령/쿼리&lt;/h2&gt;&lt;p&gt;장애가 났을 때 '어디부터 볼지'가 정해져 있으면 대응 속도가 빨라진다. 아래는 팀에서 그대로 템플릿으로 쓰기 좋은 최소 목록이다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;# 지표: 에러율/지연(p95/p99)/타임아웃/재시도 비율 확인&lt;br&gt;# 로그: correlation id로 요청 1건을 end-to-end 추적&lt;br&gt;# 설정: 기본값(타임아웃/리밋/락/인덱스)을 문서에서 재확인&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;log-fields&quot;&gt;구조화 로그 필드 추천&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;traceId/requestId/eventId 중 하나는 반드시 포함&lt;/li&gt;&lt;li&gt;endpoint/method/status/latencyMs 같은 기본 필드&lt;/li&gt;&lt;li&gt;실패 이유(reasonCode)와 재시도 횟수(retryAttempt)&lt;/li&gt;&lt;li&gt;민감정보 마스킹(토큰/비밀번호/개인정보)&lt;/li&gt;&lt;/ul&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{&lt;br&gt;  &amp;quot;level&amp;quot;: &amp;quot;INFO&amp;quot;,&lt;br&gt;  &amp;quot;message&amp;quot;: &amp;quot;request completed&amp;quot;,&lt;br&gt;  &amp;quot;requestId&amp;quot;: &amp;quot;...&amp;quot;,&lt;br&gt;  &amp;quot;method&amp;quot;: &amp;quot;POST&amp;quot;,&lt;br&gt;  &amp;quot;path&amp;quot;: &amp;quot;/api/orders&amp;quot;,&lt;br&gt;  &amp;quot;status&amp;quot;: 201,&lt;br&gt;  &amp;quot;latencyMs&amp;quot;: 123,&lt;br&gt;  &amp;quot;userId&amp;quot;: 1004,&lt;br&gt;  &amp;quot;reasonCode&amp;quot;: null&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;test-samples&quot;&gt;테스트 케이스 샘플&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;테스트 케이스(최소 3종):&lt;br&gt;1) 정상: 기대 응답/상태 전이가 맞는지&lt;br&gt;2) 실패: 입력 오류/다운스트림 오류가 &amp;#x27;예상한 에러 포맷&amp;#x27;으로 떨어지는지&lt;br&gt;3) 동시성/재시도: 같은 요청이 2~3번 들어와도 데이터가 깨지지 않는지(멱등성)&lt;br&gt;&lt;br&gt;추가(가능하면):&lt;br&gt;- 지연/타임아웃: 느린 상황에서 서킷/타임아웃이 상한을 지키는지&lt;br&gt;- 재시도 폭주: retry가 장애를 키우지 않는지&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;tradeoffs&quot;&gt;트레이드오프/대안&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;기본값은 팀/서비스에 따라 달라진다: 숫자는 '정답'이 아니라 '출발점'이다.&lt;/li&gt;&lt;li&gt;가용성 vs 보호(fail-open vs fail-closed) 결정을 미루면 장애 때 더 큰 혼란이 온다.&lt;/li&gt;&lt;li&gt;관측 없이 최적화하면: 좋아졌는지 나빠졌는지 판단이 안 된다.&lt;/li&gt;&lt;li&gt;단순한 구현이 항상 좋은 건 아니다: 운영/디버깅 비용까지 합쳐서 판단해야 한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;slo&quot;&gt;성공 기준(SLO) 예시&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;에러율: 5xx 0.1% 이하(서비스 특성에 맞게)&lt;/li&gt;&lt;li&gt;지연: p95 300ms 이하(핵심 API 기준)&lt;/li&gt;&lt;li&gt;타임아웃: 전체 요청의 0.01% 이하&lt;/li&gt;&lt;li&gt;중복 실행(멱등): 0건(또는 '부작용 0건')&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;troubleshooting&quot;&gt;자주 터지는 실수/트러블슈팅&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;as로 타입 단언 후 그대로 사용: 런타임 버그가 조용히 퍼진다.&lt;/li&gt;&lt;li&gt;모든 응답을 과도하게 검증: 성능/복잡도 증가(핵심부터).&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;template&quot;&gt;바로 적용 템플릿&lt;/h2&gt;&lt;p&gt;팀 문서/코드 리뷰에서 바로 복붙할 수 있게 최소 규격을 템플릿으로 남겨두는 게 반복 작업을 줄인다.&lt;/p&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;도입 순서:&lt;br&gt;1) 외부 API 응답 스키마부터 Zod 적용&lt;br&gt;2) safeParse + 에러 로깅&lt;br&gt;3) 테스트에서 mismatch 재현&lt;br&gt;4) 점진 확대&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;verification&quot;&gt;검증 방법&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;필드를 일부러 삭제한 모킹 응답으로도 앱이 '명확히 실패'하는지 확인한다.&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;runbook&quot;&gt;장애 대응 Runbook(초안)&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;현상: 무엇이 깨졌나(에러/지연/중복/누락) 한 문장으로 적기&lt;/li&gt;&lt;li&gt;범위: 언제부터/어느 사용자/어느 엔드포인트/어느 파티션인지&lt;/li&gt;&lt;li&gt;증거: 로그 3줄 + 지표 1개로 재현 가능한 단서 만들기&lt;/li&gt;&lt;li&gt;임시 조치: 제한(레이트리밋), 차단(서킷), 롤백/스위치 등&lt;/li&gt;&lt;li&gt;근본 원인: 키/정렬/락/타임아웃/재시도 등 어떤 규칙이 깨졌는지&lt;/li&gt;&lt;li&gt;재발 방지: 테스트 추가 + 대시보드/알람 + 문서 업데이트&lt;/li&gt;&lt;li&gt;후속 조치: 고객 공지/내부 공유(영향 범위, 원인, 재발 방지) 템플릿으로 남기기&lt;/li&gt;&lt;/ul&gt;&lt;h2 id=&quot;review&quot;&gt;리뷰 체크리스트&lt;/h2&gt;&lt;ol&gt;&lt;li&gt;성공/실패 기준이 수치로 정의돼 있다(지표, 임계값).&lt;/li&gt;&lt;li&gt;입력 검증/에러 처리가 400/409/429 등으로 명확하다(500 남발 금지).&lt;/li&gt;&lt;li&gt;동시성/재시도 상황에서도 데이터가 깨지지 않는다(멱등성/락/유니크).&lt;/li&gt;&lt;li&gt;타임아웃이 상한으로 존재한다(무한 대기 금지).&lt;/li&gt;&lt;li&gt;부하 증가 시 동작이 예측 가능하다(인덱스/큐/풀 고갈 대비).&lt;/li&gt;&lt;li&gt;로그에 상관관계 키(traceId/requestId/eventId)가 있다.&lt;/li&gt;&lt;li&gt;설정 기본값이 문서로 남아 있다(왜 이 값인지).&lt;/li&gt;&lt;li&gt;롤백/비상 조치(runbook)가 준비돼 있다.&lt;/li&gt;&lt;li&gt;테스트에 최소 1개 이상의 실패 케이스가 있다(운영 재현 목적).&lt;/li&gt;&lt;li&gt;참고/출처(공식 문서)로 팀이 더 깊게 확인할 경로가 있다.&lt;/li&gt;&lt;/ol&gt;&lt;h2 id=&quot;doc-template&quot;&gt;팀 문서 템플릿&lt;/h2&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;팀 문서 템플릿(복붙용):&lt;br&gt;1) 목표/배경: 무엇을 해결하나(한 문장)&lt;br&gt;2) 범위: 적용 대상(엔드포인트/잡/토픽/테넌트)&lt;br&gt;3) 규칙: 키/정렬/상태/응답 포맷/기본값&lt;br&gt;4) 예외: 허용하지 않는 케이스(차단/에러 코드)&lt;br&gt;5) 운영: 지표/알람/대시보드 링크&lt;br&gt;6) 장애 대응: 임시 조치 + 롤백 절차&lt;br&gt;7) 변경 이력: 언제/누가/무엇을 바꿨나&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&quot;faq&quot;&gt;FAQ(자주 묻는 질문)&lt;/h2&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 이걸 도입하면 성능이 무조건 좋아지나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 항상 그렇진 않다. 목표는 보통 '장애 반경 축소'와 '예측 가능성'이다. 성능은 인덱스/타임아웃/풀/캐시처럼 병목을 같이 잡아야 체감이 나온다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; &quot;TypeScript 런타임 검증 실전: Zod로 API 스키마 드리프트 막기&quot;를 적용했는데도 문제가 남아요. 어디부터 봐야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 먼저 로그/지표로 실패 지점을 좁히고, 설정/키/인덱스 같은 고정 요소부터 점검한다. 재현 가능한 체크리스트를 먼저 만든다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 운영에서 가장 흔한 실수는요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 규칙을 문서화하지 않고 팀마다 다르게 구현하는 것이다. 같은 이름의 기능이라도 데이터 모델/키/상태 정의가 다르면 장애가 난다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 최소 도입으로 효과를 보려면?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 체크리스트 1~2개만이라도 먼저 적용해 효과가 보이는 변화를 만든 뒤 범위를 넓히는 게 안전하다.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Q.&lt;/b&gt; 테스트를 어디까지 해야 하나요?&lt;br&gt;&lt;b&gt;A.&lt;/b&gt; 최소로는 (1) 정상 케이스, (2) 실패 케이스, (3) 동시성/재시도 케이스 3가지는 자동화하는 게 좋다. 운영 이슈의 대부분이 3번에서 나온다.&lt;/p&gt;&lt;h2 id=&quot;refs&quot;&gt;참고/출처&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=&quot;https://zod.dev/&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;Zod&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API&quot; target=&quot;_blank&quot; rel=&quot;noreferrer&quot;&gt;MDN: Fetch API&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;</description>
      <category>API</category>
      <category>backend</category>
      <category>DX</category>
      <category>frontend</category>
      <category>runtime-validation</category>
      <category>Schema</category>
      <category>testing</category>
      <category>typescript</category>
      <category>Zod</category>
      <author>Just-Do-It</author>
      <guid isPermaLink="true">https://bigtae1007.tistory.com/449</guid>
      <comments>https://bigtae1007.tistory.com/449#entry449comment</comments>
      <pubDate>Tue, 7 Apr 2026 19:59:16 +0900</pubDate>
    </item>
  </channel>
</rss>