From 699454328eb993298e041b0e648c63d52115070e Mon Sep 17 00:00:00 2001 From: midagedev Date: Thu, 27 Aug 2026 19:32:36 +0900 Subject: [PATCH] Stop claiming muted inline video always autoplays on mobile iOS disables autoplay of silent videos in Low Power Mode (WebKit 168985, resolved), so "muted + playsinline plays without a gesture" is not unconditional the way two questions stated it. An animated GIF is an image and sits outside that policy, so it keeps moving in Low Power Mode -- which means the objection those questions dismissed is not entirely wrong. image-gif-autoplay-001 now makes the exception the thing being tested. Its old answer became the one-step-short distractor: still right that sound is what the policy targets, still missing the one case where a poster frame is what the user sees. That replaced a weaker distractor duplicating the vendor-mixup option's axis. video-preview-gif-001 keeps its answer -- replacing preview GIFs with muted H.264 is still the practical call -- but drops the overstatement that nothing is left to justify GIF, and names the poster frame as part of the swap rather than an afterthought. The 2016 WebKit policy post this cited predates the Low Power Mode change by a year, which is why reading it was not enough. Reported-by: RanolP Co-Authored-By: Claude Opus 5 --- data/questions.json | 2 +- data/questions/image.json | 14 +++++++------- data/questions/video.json | 6 +++--- data/sources.json | 5 +++++ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/data/questions.json b/data/questions.json index 06a46ce..855a42f 100644 --- a/data/questions.json +++ b/data/questions.json @@ -1 +1 @@ -[{"id":"caching-no-cache-001","topic":"caching","difficulty":1,"question":"설정 JSON 응답에 Cache-Control: no-cache 를 걸었다. 캐시는 이 응답을 어떻게 다루는가?","options":[{"text":"저장은 하되, 재사용 전에 매번 재검증한다","correct":true,"why":"no-cache는 저장 금지가 아니라 재사용 전 재검증 강제다. 조건부 요청으로 304를 받으면 본문 전송 없이 그대로 쓴다."},{"text":"저장하지 않고 매번 본문을 새로 받는다","why":"그건 no-store의 동작이다. no-cache는 저장 자체를 막지 않는다."},{"text":"공유 캐시만 저장을 건너뛰고 브라우저는 캐시한다","why":"공유 캐시만 제한하는 것은 private이다. no-cache는 캐시 주체를 구분하지 않는다."},{"text":"즉시 만료된 것으로 취급되어 재검증 없이 버려진다","why":"만료 취급까지는 비슷하지만 버리는 게 아니라 재검증한다. 재검증이 성공하면 저장된 응답을 다시 쓴다."}],"explanation":"이름과 동작이 어긋나는 대표적인 디렉티브다. no-cache는 \"쓰기 전에 물어봐라\", no-store는 \"쌓아두지도 마라\"에 가깝다. 그래서 no-cache가 걸린 리소스도 304 응답으로 본문 전송을 아낄 수 있다. 진짜로 어디에도 남기고 싶지 않다면 no-store를 써야 한다.","source":"RFC 9111 §5.2.2.4 (no-cache), §5.2.2.5 (no-store)","sourceLinks":[{"label":"RFC 9111 §5.2.2.4","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.4"}]},{"id":"caching-304-cost-001","topic":"caching","difficulty":2,"question":"이미지 응답의 캐시 히트율이 지표상 높은데도 모바일에서 체감 로딩이 느리다. 확인해 보니 상당수 요청이 304 Not Modified로 끝나고 있었다. 무엇이 문제인가?","options":[{"text":"304도 왕복이 필요해 RTT는 그대로 드는 것이다","correct":true,"why":"304는 바이트를 아낄 뿐 네트워크 왕복은 발생한다. 모바일처럼 RTT가 큰 환경에서는 이 왕복이 체감 지연의 대부분이다."},{"text":"모바일 대역폭이 낮아 이미지 바이트 전송이 오래 걸리는 것이다","why":"304로 끝난 요청에는 본문 전송이 없다. 느림의 원인을 전송 바이트에서만 찾아 재검증 왕복을 놓친 것이다."},{"text":"304 응답 처리가 200보다 비싸 오리진 응답 시간이 늘어난 것이다","why":"재검증 응답은 본문 없이 헤더만 만들므로 200보다 싸다. 304를 추가 작업으로만 보고 왕복 자체를 세지 않은 것이다."},{"text":"304는 캐시 미스로 집계되어 지표가 잘못 읽힌 것이다","why":"집계 방식은 CDN마다 다르지만, 지표를 의심하기 전에 304 자체가 왕복이라는 점이 먼저 설명한다."}],"explanation":"\"캐시 히트\"라는 말이 두 가지를 덮고 있는 것이 함정이다. 저장된 응답을 재검증 없이 바로 쓰는 것과, 재검증해서 304를 받고 쓰는 것은 비용이 다르다. 전자는 0 RTT, 후자는 1 RTT다. 신선도(max-age)를 충분히 주면 재검증 자체가 사라지고, 변경 가능성이 있는 리소스는 immutable + 버전드 URL로 바꾸면 왕복을 없앨 수 있다.","source":"RFC 9111 §4.3 (Validation), RFC 9110 §15.4.5 (304)","sourceLinks":[{"label":"RFC 9111 §4.3","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.3"},{"label":"RFC 9110 §15.4.5","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5"}]},{"id":"caching-stampede-001","topic":"caching","difficulty":2,"question":"인기 이미지의 TTL이 만료되는 순간 같은 객체 요청이 한꺼번에 오리진으로 몰려 부하가 튄다. 코드 변경 없이 헤더로 완화하려면?","options":[{"text":"stale-while-revalidate를 추가한다","correct":true,"why":"만료 직후 낡은 응답을 즉시 내보내면서 뒤에서 갱신한다. 만료 순간의 동시 요청이 오리진까지 가지 않는다."},{"text":"max-age 값을 크게 늘려 만료 시점을 미룬다","why":"스파이크의 간격만 벌어질 뿐 만료 순간에 몰리는 구조는 그대로다."},{"text":"stale-if-error 를 대신 추가한다","why":"그건 오리진이 에러를 낼 때 쓰는 장치다. 정상 만료 상황에서는 발동하지 않는다."},{"text":"no-cache 를 붙여 매 요청마다 재검증하게 한다","why":"모든 요청을 오리진으로 보내게 되어 문제를 악화시킨다."}],"explanation":"캐시 스탬피드는 만료 시각이 한 점에 모여 있어서 생긴다. 해법은 세 갈래다. 만료 순간을 흩는 것(TTL jitter), 동시 미스를 하나로 합치는 것(request coalescing / collapsed forwarding), 만료 직후에도 낡은 것을 내보내는 것(stale-while-revalidate). 헤더만으로 가능한 건 세 번째다. TTL을 늘리는 건 문제를 미루는 것이지 없애는 게 아니다.","source":"RFC 5861 (stale-while-revalidate, stale-if-error)","sourceLinks":[{"label":"RFC 5861","url":"https://www.rfc-editor.org/rfc/rfc5861.html"}]},{"id":"caching-s-maxage-001","topic":"caching","difficulty":2,"question":"엣지에는 하루 동안 캐시시키고 브라우저에는 1분만 캐시시키고 싶다. 표준 Cache-Control 지시자만 쓴다면 어떤 조합인가?","options":[{"text":"max-age=60, s-maxage=86400","correct":true,"why":"s-maxage는 공유 캐시에서만 max-age를 덮어쓴다. 브라우저는 s-maxage를 무시하고 max-age를 따른다."},{"text":"max-age=86400, s-maxage=60","why":"방향이 반대다. 이러면 엣지가 1분, 브라우저가 하루가 된다."},{"text":"max-age=60, public, must-revalidate","why":"브라우저 1분은 맞지만 엣지도 1분이다. 계층별로 값을 나누는 수단이 없다."},{"text":"max-age=60 에 Surrogate-Control 만 추가한다","why":"Surrogate-Control도 계층 분리 수단이지만 모든 CDN이 해석하지는 않는다. 표준 조합은 s-maxage다."}],"explanation":"브라우저 TTL과 엣지 TTL을 분리해야 하는 상황은 흔하다. 엣지에 오래 두면 오리진 부하가 줄고, 브라우저에 짧게 두면 배포 후 사용자가 빨리 새 것을 본다. s-maxage가 그 분리를 담당하고, 벤더에 따라 Surrogate-Control이나 CDN-Cache-Control 계열로 더 세밀하게 나누기도 한다.","source":"RFC 9111 §5.2.2.10 (s-maxage)","sourceLinks":[{"label":"RFC 9111 §5.2.2.10","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10"}]},{"id":"caching-vary-accept-001","topic":"caching","difficulty":3,"question":"이미지 서빙에서 Accept 헤더로 WebP/AVIF를 협상하려고 Vary: Accept 를 붙였다. 실무에서 이 선택이 위험한 주된 이유는?","options":[{"text":"Accept 문자열이 제각각이라 캐시가 잘게 쪼개진다","correct":true,"why":"브라우저·버전마다 Accept 값이 달라 같은 이미지가 수십 개 변형으로 캐시된다. 히트율이 그만큼 떨어진다."},{"text":"Vary는 CDN에서 무시되어 협상이 동작하지 않는다","why":"무시하는 구현이 있기는 하나 일반적으로는 존중된다. 문제는 무시가 아니라 과도한 분화다."},{"text":"Vary: Accept 는 브라우저 캐시에 적용되지 않아 잘못된 포맷이 재사용된다","why":"Vary는 공유 캐시와 브라우저 캐시 양쪽에 적용된다. 위험이 클라이언트 쪽에 있다고 보아 히트율 분화라는 본론을 놓친 것이다."},{"text":"Vary를 쓰면 조건부 요청이 전부 무효화된다","why":"Vary와 ETag 기반 재검증은 함께 동작한다. 무효화되지 않는다."}],"explanation":"협상 자체가 나쁜 게 아니라 협상 축의 카디널리티가 문제다. Accept는 값의 종류가 사실상 무한에 가깝다. 그래서 실무에서는 세 갈래로 피한다. 서버가 지원 여부를 판단해 쿼리 파라미터로 정규화하거나(당근이 2019년에 택한 방식), 엣지에서 Accept를 webp/avif/jpeg 같은 소수 버킷으로 접거나, 클라이언트가 확장자로 명시하게 한다.","source":"RFC 9110 §12.5.5 (Vary), 당근 테크블로그 「Lambda@Edge 실시간 이미지 리사이즈」(2019)","sourceLinks":[{"label":"RFC 9110 §12.5.5","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.5"},{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"caching-heuristic-001","topic":"caching","difficulty":2,"question":"캐시 관련 헤더를 아무것도 붙이지 않은 응답이 CDN에 캐시되어 있는 것을 발견했다. 어떻게 된 일인가?","options":[{"text":"명시 헤더가 없으면 캐시가 휴리스틱으로 신선도를 정한다","correct":true,"why":"Last-Modified 등을 근거로 캐시가 스스로 수명을 추정할 수 있다. 표준이 허용하는 동작이다."},{"text":"헤더가 없으면 캐시되지 않으므로 CDN 설정 오류다","why":"\"설정하지 않으면 동작하지 않는다\"는 직관과 달리, 캐시는 기본적으로 캐시하려 한다."},{"text":"Age 헤더가 없어 캐시가 응답을 신선하다고 판단한 것이다","why":"Age는 캐시에 머문 시간을 알리는 응답 헤더로, 없다고 신선 판정의 근거가 되지 않는다. 신선도는 수명 계산의 결과다."},{"text":"Expires가 없으면 캐시가 무한 TTL로 간주한다","why":"무한이 아니라 추정값이다. 보통 Last-Modified로부터 경과한 시간의 일부를 수명으로 잡는다."}],"explanation":"캐시를 막으려면 명시해야 한다는 것이 요점이다. 개인화된 응답이나 인증이 걸린 응답에 아무 헤더도 없으면, 중간 캐시가 임의로 저장해 다른 사용자에게 노출될 수 있다. 캐시하지 않을 것은 no-store 또는 private을 반드시 명시한다.","source":"RFC 9111 §4.2.2 (Calculating Heuristic Freshness)","sourceLinks":[{"label":"RFC 9111 §4.2.2","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2"}]},{"id":"caching-if-range-001","topic":"caching","difficulty":3,"question":"대용량 파일의 이어받기 요청에 클라이언트가 If-Range: W/\"v42\" 를 실어 보내고 있다. 서버는 요청된 범위 대신 전체 본문을 200으로 내려주고 있다. 범위가 무시되는 까닭은?","options":[{"text":"서버 ETag가 W/ 접두어 없는 strong으로 바뀌어 값이 달라졌기 때문이다","why":"값이 어긋나서 전체를 보내는 것은 If-Range의 정상 동작이다. 다만 여기서 막힌 곳은 값의 비교가 아니라 그 앞이다 — weak 검증자는 비교 대상으로 받아들여지지도 않는다."},{"text":"요청한 범위가 서버의 최소 슬라이스 단위보다 작아 전체로 회귀하기 때문이다","why":"그런 최소 단위 규격은 없다. 200 회귀는 검증자 조건이 맞지 않아서다."},{"text":"If-Range가 받는 검증자는 strong ETag와 HTTP 날짜뿐이기 때문이다","correct":true,"why":"부분 응답은 바이트 단위 동일성이 담보돼야 한다. weak ETag는 의미상 같음까지만 보장하므로 조건 자체가 성립하지 않는다."},{"text":"If-Range는 If-None-Match와 세트로만 동작해서 단독이면 검증이 누락되기 때문이다","why":"If-Range는 단독으로 쓰는 조건부 범위 요청 장치다. 세트여야 한다는 규칙은 없다."}],"explanation":"이어 받은 구간과 새로 받은 구간을 합치려면 둘이 같은 바이트여야 한다. weak ETag는 의미상 같음까지만 보장하므로 If-Range는 strong ETag나 HTTP 날짜만 받고, weak가 오면 서버는 안전한 쪽으로 돌아가 전체 표현을 내준다. 이어받기 대상에는 strong 검증자를 쓰거나, 내용이 바뀌면 URL이 바뀌는 구조로 검증 수요 자체를 없애는 것이 정석이다. 바이트가 다른 조각끼리 섞이는 사고는 이 축을 흐린 채 부분 요청을 설계할 때 터진다.","source":"RFC 9110 §13.1.5 (If-Range), §8.8 (Validator Fields)","sourceLinks":[{"label":"RFC 9110 §13.1.5","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.5"}]},{"id":"caching-must-revalidate-001","topic":"caching","difficulty":3,"question":"오리진 장애 때도 CDN이 낡은 응답으로 서비스를 유지하도록 응답에 must-revalidate 와 stale-if-error=86400 을 함께 넣었다. 장애 시연에서 CDN은 stale을 내주지 않고 504를 뿌렸다. 기대한 동작이 나오지 않는 이유는?","options":[{"text":"stale-if-error는 5xx 응답에만 발동하는데 시나리오가 타임아웃이었기 때문이다","why":"오류의 범주에 게이트웨이 오류와 연결 실패가 포함되는지를 따지기 전에, 조합 자체가 stale 제공을 금지하고 있다."},{"text":"must-revalidate가 있으면 오류 시에도 stale 응답 제공이 금지되기 때문이다","correct":true,"why":"must-revalidate는 재검증 성공 없이는 stale을 재사용하지 못하게 하는 강한 제약이라 stale-if-error의 구명 동작을 덮어버린다."},{"text":"stale-while-revalidate가 함께 있어야 장애 시에도 stale이 풀리기 때문이다","why":"stale-while-revalidate는 정상 상황의 만료 직후를 다루는 지시다. 오리진 장애 상황의 해결책이 아니다."},{"text":"CDN이 stale-if-error 수치를 무시하고 자체 오류 정책을 적용하는 제품이기 때문이다","why":"벤더 동작을 의심하기 전에 표준 규칙상 조합이 이미 막혀 있다. 원인은 정책이 아니라 지시자 충돌이다."}],"explanation":"지시자는 많이 넣는다고 겹치는 게 아니다. must-revalidate는 검증 없는 stale 재사용을 금지하는 상한선이라, 오류 때 낡은 값을 내주는 stale-if-error와는 목적이 정면으로 충돌하고 금지 쪽이 이긴다. 장애 시 무정전이 목표라면 must-revalidate를 빼야 한다. 반대로 오래된 값이 사고가 되는 결제·재고 응답이라면 must-revalidate가 맞고, 그 경우 장애 시 에러가 나가는 것을 받아들이고 다른 완충 설계를 해야 한다.","source":"RFC 9111 §5.2.2.2 (must-revalidate), RFC 5861 §4 (stale-if-error)","sourceLinks":[{"label":"RFC 9111 §5.2.2.2","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.2"},{"label":"RFC 5861 §4","url":"https://www.rfc-editor.org/rfc/rfc5861.html#section-4"}]},{"id":"caching-private-001","topic":"caching","difficulty":1,"question":"로그인 사용자의 개인 데이터를 담은 API 응답에 Cache-Control: private 을 붙였다. 리뷰에서 private이니 CDN에서 아무도 못 보겠다며 추가 조치는 불필요하다는 의견이 나왔다. 이 의견이 놓친 것은?","options":[{"text":"private이면 브라우저 디스크에도 남지 않아 메모리에서만 살아 있다는 점","why":"저장을 아예 막는 것은 no-store다. private 응답은 브라우저 캐시에 디스크로도 저장될 수 있다."},{"text":"private은 Authorization 헤더가 붙은 응답에는 적용되지 않는다는 점","why":"지시의 한계를 벤더 구현 탓으로 돌렸다. 규범을 따르는 공유 캐시는 private을 존중한다."},{"text":"HTTPS만 쓰면 중간 캐시가 본문을 복호화하지 못해 저장이 불가능하다는 점","why":"CDN은 엣지에서 TLS를 종단하고 평문 상태로 저장한다. 전송 암호화와 캐시 저장은 다른 축이다."},{"text":"private은 공유 캐시 저장 금지일 뿐 응답 기밀이나 접근 제어 수단이 아니라는 점","correct":true,"why":"private이 막는 것은 공유 캐시에 남는 것뿐이다. 누가 요청할 수 있는가는 인가가, 전송 보호는 TLS가 담당한다."}],"explanation":"private은 '이 응답은 한 사람의 것'이라고 캐시에게 알리는 지시다. 공유 캐시 저장을 막을 뿐 암호화도 요청 주체 인가도 해주지 않는다. 기밀성은 인가와 전송 보호가 담당하고 캐시 지시는 어디에 남을 수 있는지만 정한다. 또 브라우저 캐시에는 저장되므로, 공유 디바이스의 물리적 접근까지 고려한다면 no-store를 검토해야 한다.","source":"RFC 9111 §5.2.2.7 (private)","sourceLinks":[{"label":"RFC 9111 §5.2.2.7","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.7"}]},{"id":"caching-age-freshness-001","topic":"caching","difficulty":2,"question":"오리진이 max-age=3600 을 준 응답이 중간 캐시를 거쳐 브라우저에 도착했다. 응답의 Age 헤더는 2400이었다. 브라우저 입장에서 이 응답의 남은 신선도는?","options":[{"text":"3600초다 — 브라우저에 도착한 시점에 수명이 새로 시작되기 때문이다","why":"나이가 홉마다 초기화된다고 믿은 것이다. 나이는 오리진으로부터 흐른 시간을 그대로 이어받는다."},{"text":"2400초다 — Age 값이 남은 신선도를 뜻하기 때문이다","why":"이미 경과한 나이와 앞으로 남은 신선도를 바꿔 읽은 것이다. Age는 수명에서 빼야 할 값이다."},{"text":"0초다 — Age가 실린 응답은 이미 재검증 대상이기 때문이다","why":"Age가 0보다 크다고 만료가 아니다. 판정은 Age를 수명과 비교한 결과로만 난다."},{"text":"1200초다 — Age는 오리진에서 경과한 나이를 실어 오기 때문이다","correct":true,"why":"3600에서 이미 경과한 2400을 빼면 1200이 남는다. 나이 계산은 절대 시계가 아니라 전달받은 나이에 자기 체류분을 더하는 구조다."}],"explanation":"캐시의 나이는 오리진 응답이 태어난 순간부터 흐른 시간이고 Age 헤더가 그 값을 다음 계층으로 전달한다. 하위 캐시는 전달받은 Age에 자기 체류분만 더해 판정하므로 3600 − 2400 = 1200초가 남은 것이다. 이 구조 덕분에 중간 계층이 여러 개여도 신선도 계산이 어긋나지 않고, 서버 시계가 틀어져도 응답에 실린 시간 정보의 합산으로 판정이 유지된다. 운영에서는 Age와 TTL만 있으면 이 노드에서 곧 재검증이 나갈지를 진단할 수 있어 자주 읽게 되는 헤더다.","source":"RFC 9111 §4.2.3 (Calculating Age), §4.2.1 (Calculating Freshness Lifetime)","sourceLinks":[{"label":"RFC 9111 §4.2.3","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.3"}]},{"id":"caching-only-if-cached-001","topic":"caching","difficulty":2,"question":"오프라인 구간이 잦은 모바일 앱이다. 캐시에 있는 데이터면 그대로 쓰고, 없으면 오리진까지 나갔다가 긴 타임아웃을 기다리는 대신 곧바로 실패하기를 원한다. 이 요구를 요청 쪽에서 만드는 것은?","options":[{"text":"응답에 Cache-Control: only-if-cached 를 선언하게 하는 것","why":"only-if-cached는 요청 방향 디렉티브다. 응답에 선언할 수 있는 지시가 아니다."},{"text":"요청에 stale-while-revalidate 를 붙여 백그라운드 갱신으로 돌리는 것","why":"stale-while-revalidate는 응답 방향 지시고 만료 직후의 갱신 평탄화가 목적이라 오프라인 실패 제어와 다른 문제를 푼다."},{"text":"요청에 Cache-Control: only-if-cached 를 붙이는 것","correct":true,"why":"캐시 밖으로 나가지 않겠다는 조건을 요청에 싣는다. 조건을 만족하는 저장 응답이 없으면 곧바로 실패가 돌아온다."},{"text":"요청에 no-cache 를 붙여 캐시를 최우선으로 쓰게 만드는 것","why":"no-cache는 캐시 우선이 아니라 재사용 전 재검증 강제다. 원하는 동작과 정반대 방향이다."}],"explanation":"Cache-Control은 요청 방향에서도 쓰인다. only-if-cached는 클라이언트가 '캐시에 없으면 실패해도 좋다'고 선언하는 조건이고, 조건이 안 맞으면 504가 돌아온다. max-age, max-stale, min-fresh도 클라이언트가 지나가는 캐시에 내거는 같은 부류의 제약이다. 오프라인 경험 설계에서는 이 요청 방향 지시와 캐시 가능한 응답 설계가 짝을 이뤄야 한다.","source":"RFC 9111 §5.2.1.7 (only-if-cached)","sourceLinks":[{"label":"RFC 9111 §5.2.1.7","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.7"}]},{"id":"caching-invalidation-001","topic":"caching","difficulty":2,"question":"배포 때마다 CDN 캐시를 purge하는 운영을 이어오고 있다. 멀티 CDN 이관 검토에서 purge 호출 N벌과 전파 확인 부담이 논의됐다. 이 부담을 구조적으로 줄이는 방향은?","options":[{"text":"콘텐츠가 바뀌면 URL이 바뀌게 하고 TTL을 1년까지 올리는 것","correct":true,"why":"바뀐 내용은 새 URL이 되므로 purge가 아니라 참조 전환으로 갱신이 끝난다. 무효화라는 상태ful 운영 자체가 사라진다."},{"text":"TTL을 짧게 낮춰 purge가 필요한 국면 자체를 줄이는 것","why":"오리진 부하와 재검증 지연을 대가로 치르는 방식이고 긴급 회수 요구는 그대로 남는다."},{"text":"TTL을 크게 늘리기만 하면 히트율과 무효화 문제가 함께 풀리는 것","why":"이름이 같은 채 내용이 바뀌는 콘텐츠에 긴 TTL은 오히려 위험하다. 히트율 상한을 가두는 것은 별개의 축이다."},{"text":"s-maxage를 써서 브라우저만 짧게 가져가 purge 대상을 없애는 것","why":"s-maxage는 공유 캐시용 값이라 방향이 반대고, purge 대상인 엣지 캐시는 그대로 남는다."}],"explanation":"이름이 같은 채 내용이 바뀌는 순간 무효화라는 상태ful 운영이 태어난다. 내용 해시를 파일명에 넣으면 바뀐 내용은 새 URL이 되고 참조하는 문서만 갱신하면 된다. 이 구조에서는 TTL을 1년까지 줄 수 있다. immutable(RFC 8246)을 얹으면 리로드 재검증까지 줄지만, 이 지시를 기본 지원하는 주요 브라우저가 사실상 없어 실효는 제한적이다. HTML처럼 이름을 못 바꾸는 자원만 no-cache로 재검증을 맡기는 2층 구조가 정석이고, 이것이 번들러 해시 파일명이 업계 표준이 된 이유다.","source":"RFC 8246 (HTTP Immutable Responses), Google Cloud CDN·AWS CloudFront 공식 무효화 API 문서(벤더별 단위·전파 상이)","sourceLinks":[{"label":"RFC 8246","url":"https://www.rfc-editor.org/rfc/rfc8246.html"},{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"}]},{"id":"caching-cdn-cache-control-001","topic":"caching","difficulty":3,"question":"응답 하나로 브라우저에는 5분, 앞단 CDN에는 하루, 그 뒤 미드티어 프록시에는 10분의 신선도를 주고 싶다. 이 요구를 만족시키는 수단은?","options":[{"text":"s-maxage=86400, max-age=600 조합으로 두 층을 나누는 것","why":"이 조합은 브라우저 600초, 공유 캐시 전체 86400초가 된다. 미드티어까지 하루로 묶여 세 층 요구를 만족하지 못한다."},{"text":"max-age=600은 브라우저 전용 값이니 미드티어에는 s-maxage만 닿는다는 규칙을 쓰는 것","why":"max-age는 브라우저 전용이 아니라 모든 캐시의 기본 수명이다. s-maxage가 있을 때 공유 캐시가 그 값을 덮어 쓸 뿐이다."},{"text":"Surrogate-Control로 CDN 값을 주고 Cache-Control로 나머지 두 층을 덮는 것","why":"Surrogate-Control은 표준이 아니며 해석과 지원은 벤더마다 다르다. 동일 우선순위라는 전제가 성립하지 않는다."},{"text":"앞단 CDN에는 CDN-Cache-Control, 미드티어에는 s-maxage, 브라우저에는 max-age를 주는 것","correct":true,"why":"특정 CDN 계층을 겨냥하는 헤더가 별도로 존재한다. s-maxage가 묶는 공유 캐시 전체에서 한 계층만 값을 따로 갖게 된다."}],"explanation":"브라우저와 엣지의 TTL 요구는 롤백 가능성과 오리진 보호 때문에 정반대로 갈리기 쉽고, 공유 캐시 사이에서도 우리 CDN과 그 뒤 계층을 다르게 하고 싶을 때가 있다. s-maxage는 공유 캐시 전체를 하나의 값으로 묶기 때문에 이런 세 층 요구를 표현하지 못한다. CDN-Cache-Control이나 Surrogate-Control 같은 계층 지정 헤더가 이 간극을 메우며, 일부 벤더는 자체 헤더까지 포함한 우선순위를 문서로 정해 둔다. CDN-Cache-Control은 RFC 9213(2022, Standards Track)으로 표준화됐고 Surrogate-Control은 비표준이다. 다만 계층별 우선순위의 세부 해석은 여전히 대상 CDN 문서 확인이 전제다.","source":"RFC 9213 §2 (Targeted Cache-Control Header Fields), Cloudflare 공식 문서 CDN-Cache-Control(계층별 우선순위), Fastly 문서 Surrogate-Control","sourceLinks":[{"label":"RFC 9213 §2","url":"https://www.rfc-editor.org/rfc/rfc9213.html#section-2"},{"label":"Fastly — Surrogate-Control 헤더","url":"https://developer.fastly.com/reference/http/http-headers/Surrogate-Control/"}]},{"id":"caching-set-cookie-001","topic":"caching","difficulty":2,"question":"세션 관리 정책을 바꾸면서 캐시 대상 HTML 응답 전부에 Set-Cookie 가 실리게 됐다. 그 뒤로 CDN 히트율이 90%대에서 한 자릿수로 떨어졌다. 원인은?","options":[{"text":"쿠키를 발급하는 응답은 주요 프록시·CDN이 기본 정책으로 저장하지 않기 때문이다","correct":true,"why":"쿠키가 얹힌 응답은 개인화된 것으로 취급된다. 표준이 금지하는 것은 아니지만 구현이 보수적으로 회피하므로 히트율이 0에 수렴한다."},{"text":"브라우저가 쿠키가 실린 응답을 캐시하지 않기로 되어 있기 때문이다","why":"저장을 피하는 쪽은 공유 캐시 구현이고 브라우저는 쿠키 응답도 캐시한다. 주체를 뒤집었다."},{"text":"Set-Cookie가 Vary: Cookie 를 암시적으로 만들어 키가 사용자 수만큼 갈라지기 때문이다","why":"쿠키 헤더가 Vary를 유발하지는 않는다. 유명한 카디널리티 문제와 구현의 저장 회피를 섞었다."},{"text":"쿠키 헤더 크기가 CDN의 객체 저장 한도를 넘어 저장이 거부되기 때문이다","why":"저장 회피는 크기와 무관하다. 원인은 용량이 아니라 캐시 구현의 기본 정책이다."}],"explanation":"공유 캐시 입장에서 쿠키가 얹힌 응답은 개인화된 것이다. 표준은 이것을 금지하지 않는다 — RFC 9111 §7.3은 Set-Cookie가 캐싱을 억제하지 않는다고 명시한다. 저장을 피하는 것은 구현의 기본값이며(nginx는 Set-Cookie가 있으면 캐시하지 않고 proxy_ignore_headers로만 해제된다), 결과적으로 히트율이 바닥난다. 벤더마다 기본값이 다르니 우리 앞단의 동작을 문서로 확인해야 한다. 해법은 헤더를 더 붙이는 것이 아니라 캐시 대상 경로에서 쿠키를 아예 내리지 않는 구조다. 세션 발급을 별도 경로로 옮기거나 엣지에서 쿠키 축을 분리하는 식이다. 쿠키를 캐시 키에 넣는 것도 값이 사용자별로 갈라지는 최악의 선택이라 피해야 한다.","source":"nginx 공식 문서 ngx_http_proxy_module(Set-Cookie가 있으면 응답을 캐시하지 않는다); RFC 9111 §7.3 (Caching of Sensitive Information — 표준은 Set-Cookie로 캐싱을 금지하지 않는다)","sourceLinks":[{"label":"RFC 9111 §7.3","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-7.3"},{"label":"nginx — ngx_http_proxy_module","url":"https://nginx.org/en/docs/http/ngx_http_proxy_module.html"}]},{"id":"caching-etag-origin-001","topic":"caching","difficulty":3,"question":"오리진을 두 대로 늘리고 로드밸런서로 분산하기 시작한 뒤, CDN의 재검증 트래픽에서 304 비중이 급감하고 200 재전송이 반복되고 있다. 두 서버가 내려주는 응답 본문은 바이트 단위로 동일하다. 원인은?","options":[{"text":"두 서버의 ETag가 strong과 weak으로 종류가 엇갈려 매칭이 안 되기 때문이다","why":"불일치의 원인은 검증자 종류가 아니라 값 자체가 서버마다 다른 것이다. 종류 혼동으로 진단을 돌렸다."},{"text":"검증자가 인스턴스마다 다르게 생성되어 조건부 요청이 매번 어긋나기 때문이다","correct":true,"why":"mtime 기반 ETag는 배포 시각이 어긋나면 같은 파일에 다른 값을 낸다. 재검증이 오리진 B로 가면 검증은 실패하고 본문이 다시 온다."},{"text":"오리진이 늘어난 구성에서는 304 비중이 원래 떨어지는 것이 정상이기 때문이다","why":"다중 오리진이 곧 재검증 실패라는 구조적 필연은 없다. 검증자 설계의 증상을 자연스러운 것으로 치부했다."},{"text":"서버마다 Date가 달라 만료 판정이 엇갈리면서 재검증이 꼬이기 때문이다","why":"만료 판정 축과 검증자 불일치 축은 다르다. 이 증상은 만료가 아니라 검증 실패의 모양새다."}],"explanation":"mtime 기반 ETag는 배포 시각이 서버마다 어긋나면 같은 파일에 서로 다른 값을 낸다. 엣지가 오리진 A에서 받은 ETag를 실어 재검증했는데 로드밸런서가 오리진 B로 보내면 검증은 실패하고 304 대신 200과 본문이 반복된다. 재검증 왕복 비용은 내면서 본문 절약은 0이 되는 최악의 조합이다. 해법은 내용 기반 ETag로 바꾸거나 배포 시각을 동기화하는 것, 그리고 근본적으로는 내용이 바뀌면 URL이 바뀌는 구조로 재검증 수요 자체를 없애는 것이다. inode 기반 ETag가 같은 문제로 지적되어 기본값에서 빠진 웹서버도 있다.","source":"RFC 9110 §8.8 (Validator Fields), Apache httpd 공식 문서 (FileETag — mtime/size 구성)","sourceLinks":[{"label":"RFC 9110 §8.8","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-8.8"},{"label":"Apache httpd — mod_deflate","url":"https://httpd.apache.org/docs/2.4/mod/mod_deflate.html"},{"label":"Apache httpd — FileETag","url":"https://httpd.apache.org/docs/2.4/mod/core.html#fileetag"}]},{"id":"caching-accept-encoding-001","topic":"caching","difficulty":2,"question":"API 응답에 gzip과 brotli 압축을 적용하면서 Vary: Accept-Encoding 을 붙였다. 히트율이 기대보다 낮아 로그를 보니 클라이언트와 중간 프록시에 따라 Accept-Encoding 문자열이 q값과 순서 차이로 제각각이다. 원인은?","options":[{"text":"Vary를 붙이면 헤더 축만큼 키가 늘어나 압축 응답에서는 피할 수 없기 때문이다","why":"압축 응답에는 Vary: Accept-Encoding이 필요한 올바른 설정이다. 문제는 축 추가 자체이기보다 값이 접히지 않은 것이다."},{"text":"CDN이 Vary를 무시해 서로 다른 인코딩 응답이 하나로 섞이기 때문이다","why":"Vary를 무시하면 히트율은 오히려 오르고 잘못된 변형 재사용 문제가 난다. 관찰된 현상과 방향이 반대다."},{"text":"CDN이 Accept-Encoding 원문을 정규화하지 않은 채 캐시 키에 넣고 있기 때문이다","correct":true,"why":"원문 문자열은 사실상 유니크하다. 지원 인코딩 집합으로 접지 않으면 같은 내용의 변형이 문자열 수만큼 쌓인다."},{"text":"q값 0.9와 1.0이 서로 다른 압축 결과물을 만들어 내기 때문이다","why":"변형의 수는 인코딩 종류의 수지 q값 조합의 수가 아니다. 같은 인코딩이면 바이트도 같다."}],"explanation":"압축된 응답은 요청한 인코딩에 따라 바이트가 달라지므로 Vary: Accept-Encoding은 필요하고 흔한 올바른 설정이다. 문제는 Accept-Encoding 원문이 q값과 순서, 나열까지 브라우저마다 달라 사실상 유니크하다는 점이다. 지원 인코딩 집합으로 접어 키에 넣는 정규화를 CDN이 해 주는지가 히트율을 가른다. 대상 CDN의 정규화 동작을 확인하는 것이 이 설정의 마지막 단계다.","source":"RFC 9110 §12.5.5 (Vary)","sourceLinks":[{"label":"RFC 9110 §12.5.5","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.5"}]},{"id":"caching-ttl-hitrate-001","topic":"caching","difficulty":3,"question":"정적 객체의 TTL을 1시간에서 하루로, 다시 일주일로 올렸다. 히트율은 하루 구간까지 오르다가 그 뒤로는 일주일을 줘도 제자리다. 신규 업로드는 거의 없고 요청 대상 객체 집합도 고정되어 있다. 이 구간의 히트율을 가두고 있는 것은?","options":[{"text":"최초 요청이다 — 남은 미스는 전부 처음 보는 객체의 콜드 미스라 늘어나지 않는 것이다","why":"롱테일은 한 번 저장돼도 밀려났다 다시 요청되는 반복 미스를 낸다. 처음 보는 요청만 남은 것이 아니다."},{"text":"축출이다 — 용량이 롱테일 워킹셋보다 작아 꼬리 객체가 밀려나며 반복 미스되는 것이다","correct":true,"why":"TTL은 신선 축의 이야기일 뿐 저장이 유지됨을 보증하지 않는다. 히트율의 상한은 요청 분포와 캐시 용량에서 나온다."},{"text":"브라우저 캐시가 같은 헤더로 길게 잡혀 엣지 도달 자체를 줄여버리는 것이다","why":"히트율의 분자분모는 엣지 기준인데 브라우저 축을 섞었다. 미스의 원인은 엣지 내부에 있다."},{"text":"캐시 불가 응답이다 — 남은 미스 대상이 no-store 응답이라 애초에 저장이 안 되는 것이다","why":"정적 객체 대부분이 저장 불가라는 전제는 성립하지 않는다. 남은 미스의 실체는 축출이다."}],"explanation":"TTL을 올리면 신선 기간이 늘어나지만, 저장이 유지되는지는 별개다. 히트율의 상한은 요청 분포와 캐시 용량의 관계에서 나온다. 롱테일 전체를 담으려면 워킹셋보다 큰 용량이 필요한데 모자라면 꼬리 객체는 들어왔다 밀려나기를 반복하며 TTL이 남아 있어도 미스가 난다. 이 지점부터는 TTL을 더 올리는 대신 키 정규화로 같은 객체를 합치거나 용량과 축출 정책을 보는 것이 맞는 축이다. 콜드 미스와 축출 미스는 로그를 나눠 봐야 구분된다.","source":"AWS CloudFront·Google Cloud CDN 공식 문서(캐시 용량·축출 정책은 TTL과 무관하게 객체를 밀어냄), RFC 9111 §4.2 (신선도는 재사용 조건이지 저장 보증이 아님)","sourceLinks":[{"label":"RFC 9111 §4.2","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2"},{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"}]},{"id":"caching-authorization-001","topic":"caching","difficulty":2,"question":"요청에 Authorization 이 실리는 개인화 API를 CDN 뒤에 두었다. 응답에는 아무 캐시 헤더도 넣지 않았는데 CDN 히트율이 0으로 나온다. 표준이 이렇게 동작하는 까닭은?","options":[{"text":"Authorization이 실린 요청의 응답은 기본적으로 공유 캐시가 저장하지 못하도록 되어 있기 때문이다","correct":true,"why":"한 사용자의 인증 문맥으로 받은 응답이 다른 사용자에게 재사용되는 일을 막으려는 규범이다. 예외는 응답에 명시적 지시가 있을 때뿐이다."},{"text":"Authorization 헤더가 TLS 종단에서 벗겨져 오리진이 캐시 헤더를 못 붙이기 때문이다","why":"저장 금지는 전송 구간의 문제가 아니라 캐시 저장 규범 자체다. 헤더 전달 경로로 원인을 가져왔다."},{"text":"Authorization이 응답에도 개인화 표시로 실려 나가 캐시가 저장을 꺼리기 때문이다","why":"Authorization은 요청 헤더다. 응답 표시로 작동하지 않는다."},{"text":"CDN이 Authorization을 캐시 키에 넣지 않아 사용자별 엔트리가 아예 안 만들어지기 때문이다","why":"키 문제처럼 보이지만 저장 자체가 규범으로 막힌 것이 먼저고, 인증 값을 키에 넣는 방식도 개인화 캐시의 해법이 아니다."}],"explanation":"공유 캐시가 인증 문맥의 응답을 다른 사용자에게 재사용하는 일을 막으려고 표준은 Authorization이 붙은 요청의 응답을 기본 저장 금지로 둔다. 예외는 응답이 public, s-maxage, must-revalidate 중 하나를 명시할 때뿐인데, 개인화 응답에 이를 붙이면 다른 사용자에게 재사용되는 역방향 사고가 된다. 캐시 헤더가 없다고 휴리스틱이 적용될 여지도 이 저장 금지가 우선해 가로막힌다. 인가와 캐싱을 함께 쓰려면 재사용 가능한 축과 개인 축을 나누고 인가를 URL 서명이나 별도 경로로 옮기는 것이 구조적 해법이다.","source":"RFC 9111 §3 (Storing Responses in Caches — Authorization 저장 규칙)","sourceLinks":[{"label":"RFC 9111 §3","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-3"}]},{"id":"caching-pragma-legacy-001","topic":"caching","difficulty":1,"question":"레거시 대응이라는 이유로 모든 API 응답에 Cache-Control: no-cache 와 Pragma: no-cache 를 함께 실어 보내는 게 오래된 관례로 남아 있다. 접근 로그를 보면 이 트래픽에는 HTTP/1.0 클라이언트가 없다. 이중 지시의 실효는?","options":[{"text":"1.0 세대 프록시 장비가 언제든 끼어들 수 있으니 이중 지시가 안전장치 역할을 한다","why":"과거 예제 코드의 잔재다. 1.0 클라이언트가 없는 트래픽에서는 보호하려는 대상 자체가 존재하지 않는다."},{"text":"현대 캐시는 Cache-Control을 보므로 Pragma 쪽은 사실상 읽히지 않는다","correct":true,"why":"Pragma는 HTTP/1.0 캐시 하위호환용으로 남은 필드고 응답 방향 동작은 규격에 정의조차 없다. Cache-Control이 있는 한 개입할 자리가 없다."},{"text":"두 헤더가 겹치면 캐시가 더 엄격하게 해석해 사실상 no-store와 같아진다","why":"지시가 겹친다고 의미가 강해지지 않는다. 각 필드는 자기 규격대로 해석된다."},{"text":"일부 CDN은 Pragma가 있으면 Cache-Control과 충돌로 간주해 아예 캐시하지 않는다","why":"특정 벤더의 오래된 동작을 일반 규칙처럼 믿은 것이다. 그런 충돌 규칙은 규격에 없다."}],"explanation":"Pragma는 요청 방향으로만 의미가 정의된 HTTP/1.0 잔재고, 응답에 실어도 동작은 구현 마음이다. Cache-Control이 자리 잡은 지금은 1.0 클라이언트가 실제로 존재하는 경로가 확인된 경우에만 남길 가치가 있다. 이중 지시의 실제 비용은 헤더 증가가 아니라 '이 헤더가 뭘 하는지'에 대한 팀의 오해가 누적되는 것이다 — no-cache와 no-store조차 섞어 쓰는 팀이라면 더 그렇다.","source":"RFC 9111 §5.4 (Pragma), MDN Web Docs (Pragma)","sourceLinks":[{"label":"RFC 9111 §5.4","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.4"}]},{"id":"caching-expires-maxage-001","topic":"caching","difficulty":2,"question":"오리진이 우연히 같은 응답에 Cache-Control: max-age=600 과 Expires: (지금부터 1시간 뒤) 를 함께 실어 보내고 있다. 두 값은 어긋난다. 규격을 따르는 캐시의 판정은?","options":[{"text":"수명은 max-age의 600초로 정해지고 Expires는 계산에 쓰이지 않는다","correct":true,"why":"신선 수명 계산은 max-age를 우선하고, 그 값이 없을 때만 Expires에서 Date를 뺀 값으로 대체한다."},{"text":"수명은 Expires로 정해지고 max-age는 보조 값으로만 남는다","why":"구식 헤더가 더 기본처럼 느껴져 우선순위를 뒤집은 것이다. 규격은 정반대로 정한다."},{"text":"두 값이 어긋나면 캐시는 안전 쪽인 짧은 값으로 통일해서 쓴다","why":"'보수적 해석'이라는 일반 직관이 규격에 없는 타협 규칙을 만들어낸 것이다. 우선순위가 정해져 있으므로 타협이 없다."},{"text":"두 값의 중간에 해당하는 수명으로 절충된다","why":"존재하지 않는 절충 규칙이다. 수명 계산은 우선순위에 따라 한 값을 고를 뿐이다."}],"explanation":"신선 수명 계산의 순서는 정해져 있다 — 공유 캐시면 s-maxage, 그다음 max-age, 둘 다 없어야 Expires에서 Date를 뺀 값이 쓰인다. 이 우선순위는 실무에서 자주 걸린다. 중간 장비가 Expires만 다시 쓰는 구성은 아무 효과가 없고, 헤더를 정리할 때도 어느 쪽이 지배하는지 알아야 잘라낼 값을 고를 수 있다. '짧은 쪽이 안전'은 그럴듯하지만 규격이 아니라서, 그렇게 구현된 일부 장비와 규격 준수 캐시가 어긋나는 원인이 되기도 한다.","source":"RFC 9111 §4.2.1 (Calculating Freshness Lifetime), §5.3 (Expires)","sourceLinks":[{"label":"RFC 9111 §4.2.1","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.1"}]},{"id":"caching-inm-precedence-001","topic":"caching","difficulty":3,"question":"캐시된 문서의 재검증 요청에 If-None-Match: \"v2\" 와 If-Modified-Since: (어제 09:00) 이 함께 실렸다. 오리진의 현재 상태는 ETag \"v3\", 마지막 수정은 그저께다. 규격대로 서버가 내놓을 응답은?","options":[{"text":"304 — 마지막 수정 시각이 조건보다 이전이라 변경이 없었다는 조건이 성립한다","why":"두 검증자를 병렬로 평가한다고 믿은 것이다. 규격은 검증자가 있으면 날짜 조건을 아예 보지 않는다."},{"text":"412 — 검증자와 날짜 조건이 서로 어긋나 전제조건 실패로 응답된다","why":"412는 상태 변경 메서드에서 If-Match 계열이 실패할 때의 응답이다. GET 재검증 실패의 모양새가 아니다."},{"text":"304 — 마지막 수정 시각이 검증자보다 신뢰할 수 있는 정보라 날짜 쪽이 우선한다","why":"우선순위의 방향을 뒤집었다. 검증자가 있는 이상 날짜 조건은 평가에서 밀려난다."},{"text":"200 — 검증자 불일치로 조건이 실패해 본문을 내주고, If-Modified-Since는 평가에서 빠진다","correct":true,"why":"If-None-Match가 있으면 If-Modified-Since는 무시된다. 검증자 불일치가 조건 실패를 확정하므로 본문이 다시 나간다."}],"explanation":"조건부 검증자의 우선순위는 규격이 정한다 — If-None-Match가 있으면 If-Modified-Since는 무시된다. ETag가 우선하는 까닭은 날짜 조건이 같은 초 안의 수정을 구분 못 하는 데 비해 검증자는 그 모호함을 덮기 때문이다. 조건 실패의 응답은 메서드에 따라 갈린다. GET/HEAD 재검증이면 200 본문이 다시 가고, 상태 변경 메서드의 조건 실패는 412다. 캐시가 두 헤더를 '보험 삼아' 같이 싣는 일은 흔해서, 이 우선순위를 모르면 '수정한 적 없는데 200이 돌아온다'는 오진이 팀에 생긴다.","source":"RFC 9110 §13.1.2 (If-None-Match), §13.2.2 (Precedence of Preconditions), MDN Web Docs (If-None-Match)","sourceLinks":[{"label":"RFC 9110 §13.1.2","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-13.1.2"}]},{"id":"caching-immutable-scope-001","topic":"caching","difficulty":2,"question":"정적 에셋에 내용 해시 파일명과 Cache-Control: max-age=31536000, immutable 을 붙였다. 리뷰에서 'immutable이니 만료 전까지 브라우저가 재검증하지 않는 게 표준으로 보장된다'는 확인 요청이 왔다. 이 진술에 대한 판단은?","options":[{"text":"주요 브라우저가 모두 같은 의미로 지원하므로 동작이 보장되는 셈이다","why":"지원 편차가 실재하는 지시를 전 브라우저 동질 지원으로 믿은 것이다. 지원하지 않는 쪽은 max-age만 따른다."},{"text":"immutable은 표준이지만 구현은 선택이라 동작 보장은 브라우저 지원에 달려 있다","correct":true,"why":"표준 트랙 문서가 있는 것과 브라우저가 구현한 것은 다른 이야기다. 지원하지 않는 브라우저에서는 그냥 max-age로 동작한다."},{"text":"만료 이후에도 재검증 없이 재사용을 계속 허용하는 것이 immutable의 효과다","why":"효과는 신선한 동안의 리로드 재검증 생략으로 한정된다. 만료 후 동작까지 바꾸지 않는다."},{"text":"immutable을 붙이면 max-age 값과 무관하게 1년짜리 신선도가 부여된다","why":"신선 수명은 max-age가 정한다. immutable은 수명을 늘리는 지시가 아니라 재검증 습관을 줄이는 지시다."}],"explanation":"immutable은 RFC 8246(Standards Track, 2017)으로 표준화된 지시지만 구현은 선택이고, 효과도 한정적이다 — fresh 기간에 사용자가 리로드를 해도 재검증을 생략하게 하는 것뿐이다. 표준 문서가 있다는 것이 곧 동작 보장은 아니다: 이 지시를 기본 지원하는 주요 브라우저가 사실상 없다. 해시 파일명과 1년 max-age 조합의 설계 자체는 여전히 옳지만, '표준 보장'이라고 적어 두면 지원하지 않는 브라우저에서 기대가 어긋났을 때 원인 진단이 늦어진다. 신선 수명을 정하는 것은 어디까지나 max-age라는 점도 함께 못박아 둔다. 문서에는 '지원하는 브라우저에서 리로드 재검증을 줄여 주는 표준 지시' 정도로 정확하게 쓰는 것이 관리 가능하다.","source":"RFC 8246 (HTTP Immutable Responses), RFC 9111 §5.2.2.1 (max-age)","sourceLinks":[{"label":"RFC 8246","url":"https://www.rfc-editor.org/rfc/rfc8246.html"},{"label":"RFC 9111 §5.2.2.1","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.1"}]},{"id":"caching-purge-propagation-001","topic":"caching","difficulty":2,"question":"옥외 광고 소재에 긴급 수정본을 올리고 즉시 CDN purge API를 호출해 200 OK 를 받았다. 5분 뒤에도 몇몇 리전에서 이전 소재가 계속 나오고 있다. 가장 근본적인 원인은?","options":[{"text":"purge 승인은 접수일 뿐이고 실제 무효화는 POP 단위로 비동기 전파돼 수 분이 걸린다","correct":true,"why":"성공 응답과 전파 완료는 다른 사건이다. 리전별로 남은 노출 창은 전파 지연의 정상 범위다."},{"text":"무효화 대상 지정이 캐시 키 정규화 규칙과 어긋나 일부 URL 변형만 지워졌다","why":"purge가 변형을 놓치는 실재하는 사고 유형이지만, 이 증상은 변형이 아니라 리전을 따라 남아 있다."},{"text":"이 CDN은 200을 반환하고도 purge를 수행하지 않는 알려진 결함이 있다","why":"벤더 결함을 먼저 의심하는 습관이다. 접수와 전파의 분리가 정상 동작인데 결함으로 읽었다."},{"text":"TTL이 남아 있는 객체는 purge 대상에서 제외돼 만료 시각까지 유지된다","why":"purge는 즉시 제거고 TTL은 제거와 무관한 신선도 축이다. 두 축을 뒤집어 붙였다."}],"explanation":"purge API의 성공 응답은 '접수됨'이지 '전역 완료'가 아니다. 무효화는 수백 개 POP에 비동기로 퍼지고 전파 창이 수 분 생긴다. 이 창이 사고가 되는 조직은 긴급성 있는 자원을 purge 의존 설계에 올려둔 곳이다 — 내용이 바뀌면 URL이 바뀌는 버전 체계로 옮기면 수정·롤백이 참조 전환만으로 끝나고 무효화라는 상태ful 운영 자체가 사라진다. 멀티 CDN이면 벤더마다 API·대상 단위·전파 SLA가 달라 이 창은 더 커지고 관리 지점도 N벌이 된다.","source":"AWS CloudFront·Google Cloud CDN 공식 무효화 API 문서 (전파 지연·비동기 처리)","sourceLinks":[{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"}]},{"id":"caching-range-key-001","topic":"caching","difficulty":2,"question":"대용량 파일 서빙에서 206 Partial Content 비중이 높고 엣지 히트율이 기대보다 낮다. 팀 회의에서 원인 추정이 갈리고 있다. 범위 요청에 대한 규격상 이해로 옳은 것은?","options":[{"text":"Range 값이 캐시 키에 들어가 요청한 범위 수만큼 엔트리가 갈라진다","why":"쿼리 파라미터가 키에 들어가는 경험을 Range에까지 확장한 것이다. Range는 키 구성 축이 아니다."},{"text":"206 응답은 불완전한 부분 응답이라 캐시 원칙상 저장 대상이 아니다","why":"부분 응답도 저장하고 결합하도록 허용된다. 저장 불가 규칙을 잘못된 자리로 끌어왔다."},{"text":"미스 때 오리진에 요청 범위를 그대로 프록시하는 것이 규격상 의무다","why":"전체를 내려받아 슬라이스할지 범위만 전달할지는 벤더 구현의 영역이다. 규격상 의무가 아니다."},{"text":"Range는 캐시 키가 아니고, 전체 객체가 캐시돼 있으면 엣지가 범위만 잘라 206으로 낸다","correct":true,"why":"키는 메서드와 요청 대상, Vary 축으로 정해진다. 범위는 저장된 응답을 내주는 방식일 뿐이다."}],"explanation":"히트율 저하의 원인을 206에서 찾기 전에 키 구성부터 봐야 한다. 캐시 키는 최소한 메서드와 대상 URI, 그리고 Vary가 지정한 축으로 구성되지 Range가 아니므로, 범위가 아무리 다양해도 엔트리가 갈라지지 않는다. 엣지가 미스 때 오리진을 어떻게 가져올지(전체 저장 후 슬라이스 vs 범위만 프록시)는 벤더마다 다르고, 이 차이가 대용량 파일의 오리진 부하를 좌우한다. 낮은 히트율의 진짜 후보는 같은 객체를 여러 URL로 접근하는 정규화 문제나 축출 쪽이다.","source":"RFC 9111 §2 (Overview of Cache Operation — 캐시 키 구성), AWS CloudFront 공식 문서 (Range 요청 처리)","sourceLinks":[{"label":"RFC 9111 §2","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-2"}]},{"id":"caching-vary-asterisk-001","topic":"caching","difficulty":3,"question":"오리진 팀이 특정 API 응답에 Vary: * 를 붙이기로 했다. 배포 직후 그 경로만 CDN 히트율이 0으로 떨어졌다. 가장 근본적인 이유는?","options":[{"text":"* 는 모든 요청 헤더를 키에 넣으라는 뜻이라 키가 무한히 갈라진다","why":"와일드카드를 '전체 헤더 나열'로 읽은 것이다. 문제는 키 분화가 아니라 재사용 금지다."},{"text":"요청에 실리지 않는 축까지 달라진다는 선언이라 재검증 없이는 재사용 자체가 금지된다","correct":true,"why":"* 값은 어떤 후속 요청과도 변형이 일치하지 않는다. 남은 선택은 매번 재검증뿐이라 히트가 사라진다."},{"text":"이 CDN은 Vary를 지원하지 않아 헤더를 버리고 전부 미스로 처리한다","why":"미지원이면 오히려 저장 응답을 재사용해 히트가 늘어난다. 관찰된 방향과 어긋나는 진단이다."},{"text":"응답에 ETag가 없어 조건부 재검증을 못 하는 것이 직접 계기다","why":"검증자 유무는 재검증의 형태를 정할 뿐이다. 히트가 0이 된 원인은 * 선언 자체다."}],"explanation":"Vary의 값이 *면 '이 응답이 무엇에 따라 달라지는지 특정할 수 없다'는 선언이고, 규격상 어떤 후속 요청과도 다른 변형으로 취급된다. 그래서 캐시는 저장해도 재검증 없이는 꺼내 쓸 수 없고 그 경로는 매 요청이 검증으로 간다. 실제로 협상 축이 특정 가능하다면 헤더 이름을 명시하는 것이 맞고, 정말로 축을 특정할 수 없는 응답이라면 no-store로 정직하게 선언하는 편이 나중 로그 해석에 낫다. 히트율 0은 버그가 아니라 이 선언의 규격적 결과다.","source":"RFC 9110 §12.5.5 (Vary), MDN Web Docs (Vary)","sourceLinks":[{"label":"RFC 9110 §12.5.5","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.5"}]},{"id":"caching-vary-user-agent-001","topic":"caching","difficulty":2,"question":"모바일/데스크톱 마크업 차이를 서버에서 내려주기 위해 HTML 응답에 Vary: User-Agent 를 붙였다. 며칠 뒤 CDN 히트율이 한 자릿수로 떨어졌고 접근 로그의 UA 원문은 수만 종이었다. 대응의 방향으로 옳은 것은?","options":[{"text":"Vary에서 User-Agent를 빼고 응답 선택은 서버가 알아서 하게 둔다","why":"캐시가 다른 UA의 응답을 재사용하게 되어 잘못된 마크업이 나갈 수 있다. 원인을 고친 게 아니라 감춘 것이다."},{"text":"TTL을 크게 올려 늘어난 각 UA 변형이라도 오래 살게 둔다","why":"변형 수 자체는 줄지 않는다. 키 폭발을 상환할 뿐이고 변경 반영 지연까지 커진다."},{"text":"주요 CDN은 UA를 알아서 접어 주니 현재 설정을 유지한다","why":"정규화 여부는 제품마다 다르고 기본 동작으로 가정할 수 없다. 확인 전의 방치는 선택지가 아니다."},{"text":"UA를 소수 버킷으로 접어 주거나, 협상 축을 별도 URL로 분리한다","correct":true,"why":"UA 원문은 사실상 요청별 유니크라 버킷 없이는 키가 폭발한다. 축을 식별 가능한 소수로 접는 것이 구조적 해법이다."}],"explanation":"UA 문자열은 브라우저 빌드·버전·디바이스·패치 수준까지 갈라져 사실상 무한에 가깝고, 원문이 키에 들어가면 요청 단위로 캐시가 갈라진다. 실무의 해법은 축을 접는 것이다 — 엣지에서 모바일/데스크톱 같은 소수 버킷으로 정규화하거나, m.서브도메인이나 별도 경로처럼 URL 축으로 아예 분리하는 쪽이다. Vary에서 UA를 빼는 것은 다른 UA에게 잘못된 변형을 내주는 다른 결함을 여는 것이므로 답이 아니다. 협상 결과가 두세 세트뿐이라면 버킷 수도 그만큼으로 끝난다.","source":"RFC 9110 §12.5.5 (Vary), MDN Web Docs (Browser detection using the user agent)","sourceLinks":[{"label":"RFC 9110 §12.5.5","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.5"}]},{"id":"caching-proxy-revalidate-001","topic":"caching","difficulty":3,"question":"이미지 API에서 엣지에는 만료된 응답을 절대 내주지 않으면서 신선한 동안의 엣지 히트는 그대로 두고, 브라우저는 네트워크가 끊긴 동안 자기 캐시를 계속 쓸 수 있게 하려 한다. 이 요구를 만족시키는 표준 지시는?","options":[{"text":"proxy-revalidate — 공유 캐시에만 만료 후 재검증을 강제한다","correct":true,"why":"재검증 상한이 엣지에만 닿는다. 브라우저의 stale 사용 정책은 규격상 그대로 남는다."},{"text":"must-revalidate — 공유 캐시에만 걸리는 상한이라 브라우저의 stale 사용은 남는다","why":"must-revalidate는 계층을 가리지 않는다. 공유 캐시 한정판인 proxy-revalidate가 따로 있는 이유가 그 차이다."},{"text":"no-cache — 만료 후에만 재검증을 요구하므로 신선한 동안의 엣지 히트는 남는다","why":"no-cache는 만료 여부와 무관하게 재사용 전 재검증을 요구한다. 신선한 동안의 히트가 남지 않는다."},{"text":"s-maxage=0 — 공유 캐시의 만료 후 재검증만 강제하고 신선 기간은 max-age가 정한다","why":"s-maxage=0은 공유 캐시의 신선 기간을 0으로 만든다. s-maxage가 있으면 max-age는 공유 캐시에 닿지 않는다."}],"explanation":"proxy-revalidate는 must-revalidate의 공유 캐시 한정판이다. 만료 후 재검증 강제가 엣지에만 닿아서, 브라우저의 오프라인 재사용이나 이동 사이의 관대한 stale 사용은 그대로 남는다. 이런 계층 비대칭 요구는 실제로 자주 나온다 — 가격이나 재고처럼 엣지가 엄격해야 하는 축과 사용자의 최근 화면처럼 느긋해도 되는 축이 같은 서비스 안에 공존한다. s-maxage=0도 엣지 stale을 막지만 신선 기간을 희생하므로 오리진 검증이 매 요청으로 늘어나는 반대 극단이다.","source":"RFC 9111 §5.2.2.8 (proxy-revalidate), §5.2.2.2 (must-revalidate)","sourceLinks":[{"label":"RFC 9111 §5.2.2.8","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.8"}]},{"id":"caching-collapsing-001","topic":"caching","difficulty":2,"question":"인기 객체 하나의 TTL이 막 지났다. 같은 객체로 200건의 요청이 엣지에 거의 동시에 들어왔다. 이 CDN에는 request collapsing이 켜져 있다. 이 요청들의 운명은?","options":[{"text":"요청 각자가 저장된 낡은 응답을 즉시 받고 갱신은 뒤에서 진행된다","why":"그것은 stale-while-revalidate의 동작이다. collapsing은 재검증 결과를 기다리게 한다."},{"text":"만료 시각에 무작위 지터가 부여돼 재검증이 순차적으로 분산된다","why":"만료 시각을 흩는 것은 TTL 지터의 동작이다. 이미 들어온 요청을 합치는 것과 다른 층이다."},{"text":"동시 미스는 설정과 무관하게 전부 오리진으로 그대로 나간다","why":"동시 미스를 합치는 층의 존재를 모르고 캐시를 단순 프록시로만 상상한 것이다."},{"text":"첫 재검증이 돌아올 때까지 나머지는 엣지에서 대기했다가 같은 결과를 받는다","correct":true,"why":"동시 요청이 하나로 병합되어 오리진에는 재검증 한 건만 나간다. 오리진 보호가 이 기능의 목적이다."}],"explanation":"스탬피드 완화책은 셋이고 각자 바꾸는 대상이 다르다 — collapsing은 동시 요청을 하나로 합치고, TTL 지터는 만료 시각을 흩으며, stale-while-revalidate는 재검증이 돌아올 동안 낡은 것을 내준다. collapsing의 대가는 첫 결과가 돌아올 때까지 뒤 요청의 지연이 늘어나는 것이다. 오리진 입장의 스파이크 높이와 사용자 입장의 꼬리 지연이 오가는 트레이드오프다. 기능 이름과 기본값은 벤더마다 다르므로(Fastly의 request collapsing, CloudFront의 Origin Shield 등) 도입 전에 우리 CDN의 동작 문서를 확인하는 것이 전제다.","source":"Fastly 공식 문서 (Request Collapsing), AWS CloudFront 공식 문서 (Origin Shield)","sourceLinks":[{"label":"AWS CloudFront — Origin Shield","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html"}]},{"id":"caching-no-store-private-001","topic":"caching","difficulty":1,"question":"1회용 인증 링크를 담은 짧은 응답을 설계 중이다. 요구는 두 가지다 — 공유 캐시에 남지 않을 것, 그리고 공용 PC의 브라우저 캐시에도 남지 않을 것. Cache-Control 한 줄로 옳은 선택은?","options":[{"text":"private — 공유 캐시 저장만 막는다","why":"공유 캐시 저장은 막지만 브라우저 캐시에는 디스크로 남을 수 있다. 두 요구 중 하나만 만족한다."},{"text":"no-store — 모든 캐시에서 저장을 금지한다","correct":true,"why":"저장 자체를 금지하는 유일한 표준 지시다. 공유/개인 캐시 구분이 없다."},{"text":"no-cache — 재사용 전 재검증을 강제한다","why":"저장은 일어나고 재사용 전에 검증만 간다. 저장 금지와 다른 지시다."},{"text":"private, no-cache — 저장과 재사용을 동시에 막는 조합이다","why":"no-cache는 저장을 막지 않으므로 조합해도 브라우저에는 남는다. no-store의 효과로 승격되지 않는다."}],"explanation":"private은 '한 사람의 응답' 표시일 뿐 브라우저 저장을 막지 않고, no-cache는 저장 후 재검증을 조건으로 삼을 뿐이다. 어디에도 남기지 않으려면 no-store다. 다만 no-store는 캐시의 힘을 완전히 버리는 선택이므로, 매번 재검증으로 충분한 경우에는 no-cache가 304 절약을 유지하는 더 나은 균형이 될 수 있다. 설계의 첫 단계는 요구가 '재사용 금지'인지 '저장 금지'인지부터 가르는 것이다. 공용 기기의 잔존까지 걱정하는 1회용 링크라면 답은 후자다.","source":"RFC 9111 §5.2.2.5 (no-store), §5.2.2.4 (no-cache), §5.2.2.7 (private)","sourceLinks":[{"label":"RFC 9111 §5.2.2.5","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.5"}]},{"id":"caching-heuristic-oldcopy-001","topic":"caching","difficulty":3,"question":"1년째 수정이 없던 안내 페이지에 문구를 하나 고쳤다. 이 페이지에는 Cache-Control·Expires가 없고 Last-Modified만 내려온다. 배포 2주 뒤에도 일부 사용자에게 옛 문구가 나오고 있다. 가장 그럴듯한 설명은?","options":[{"text":"옛 사본은 배포 전 Last-Modified 기준의 휴리스틱 수명을 안고 있어 아직 신선하다","correct":true,"why":"명시 신선도가 없으면 캐시가 수정 이력 구간의 비율로 수명을 추정한다. 1년짜리 이력이면 수 주짜리 수명이 나온다."},{"text":"신선도 지시가 없는 응답에는 CDN이 Last-Modified와 무관한 고정 기본 TTL을 부여하는데 그 값이 길게 잡혀 있다","why":"기본 TTL을 두는 제품도 있지만 그 값은 대개 시간 단위고 수정 이력과 무관하다. 2주짜리 창을 설명하지 못한다."},{"text":"응답에 검증자가 없어 재검증이 불가능하므로 캐시가 오래 재사용할 수밖에 없다","why":"재검증 불가는 사실이지만 무한 재사용으로 이어지지 않는다. 재사용은 신선도 판정이 허용하는 만큼만이다."},{"text":"인기 페이지라 축출되지 않고 남아 있어 계속 히트하고 있을 뿐이다","why":"오래 남아 있는 것과 신선하게 재사용되는 것은 다른 축이다. 축출은 오히려 다시 받아오게 만든다."}],"explanation":"명시 신선도가 없으면 캐시는 휴리스틱으로 수명을 추정하고, 전통적인 기준은 Last-Modified 이후 경과 구간의 일부(10% 관행)다. 수정 이력이 1년이면 추정 수명이 수 주가 되고, 핵심은 이미 퍼진 옛 사본이 배포 전 기준의 그 긴 수명을 그대로 가진 채 남아 있다는 점이다 — 수정 반영이 밀리는 창은 여기서 온다. 근본 대책은 이력 길이에 수명을 묶지 않는 것이다. 캐시돼도 되는 페이지에는 명시 max-age를, 즉시 반영이 필요하면 no-cache로 재검증에 맡기면 추정에 의존하는 구간이 사라진다.","source":"RFC 9111 §4.2.2 (Calculating Heuristic Freshness)","sourceLinks":[{"label":"RFC 9111 §4.2.2","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.2.2"}]},{"id":"cdn-miss-benefit-001","topic":"cdn","difficulty":1,"question":"인증 결과를 반환하는 API라 캐시가 거의 안 되는 트래픽인데, CDN을 앞에 붙였더니 지연이 줄었다. 히트가 아닌데 줄어든 이유는?","options":[{"text":"CDN이 오리진 응답을 알아서 압축해서 전송량을 줄여 주기 때문이다","why":"본문 재압축은 CDN의 기본 동작이 아니다. 캐시 외 이득의 원리를 본문 가공으로 짚은 인접 개념 치이반이다."},{"text":"TCP·TLS 핸드셰이크와 전송 램프업이 유저 가까운 PoP에서 끝나기 때문이다","correct":true,"why":"캐시와 무관한 종료점 이동 이득이다. 왕복이 짧아지면 혼잡 제어 창도 빨리 자라 소형 객체 전송이 단축된다."},{"text":"TLS 세션 재개로 오리진과 맺은 연결이 재사용되어 왕복이 짧아지기 때문이다","why":"짧아지는 구간은 유저와 엣지 사이지 CDN과 오리진 사이가 아니다. 이득이 나는 위치를 오리진 쪽으로 옮겨 읽었다."},{"text":"CDN이 오리진 응답을 예측해 미리 받아 두는 프리페치가 작동하기 때문이다","why":"저장이 안 되는 인증 응답은 예측 선반입의 대상도 아니다. 캐시형 이득을 저장 불가 트래픽으로 확장한 과잉 일반화다."}],"explanation":"CDN의 이득은 두 개다. 캐시 히트(RTT 0 서빙)와 종료점 이동(TCP/TLS 핸드셰이크와 혼잡 제어 램프업이 유저 근처 PoP에서 끝남)이다. 미스가 나면 전자만 사라지고 후자는 그대로 남는다. 그래서 캐시 불가능한 API 트래픽에도 CDN을 붙이는 구성이 성립하고, 오리진 fetch는 CDN 백본에서 벌어져 유저에게는 첫 바이트부터 순차 전달되는 경우가 일반적이다. \"CDN = 캐시\"라는 등식 하나로 두 이득을 통째로 놓치게 된다.","source":"AWS CloudFront Developer Guide — 동적 콘텐츠 서빙(TLS 종료·연결 재사용)","sourceLinks":[{"label":"AWS CloudFront Developer Guide","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html"}]},{"id":"cdn-shield-math-001","topic":"cdn","difficulty":2,"question":"엣지 캐시 미스율이 10%이고 origin shield 히트율이 80%다. 오리진에 실제로 도달하는 요청 비율은?","options":[{"text":"20% — 1 − 0.8","why":"1 − 0.8 은 실드의 미스율이지 오리진 도달률이 아니다. 엣지 미스율 10% 를 곱하는 단계를 건너뛰었다."},{"text":"8% — 10% × 0.8","why":"10% × 0.8 은 실드가 흡수한 몫이다. 흡수량과 관통량을 뒤집어 계산했다."},{"text":"2% — 10% × (1 − 0.8)","correct":true,"why":"엣지 미스가 실드를 거치고 실드에서 남은 20%만 오리진까지 간다. 계층의 이득은 이렇게 곱으로 쌓인다."},{"text":"10% — 실드 히트율은 오리진 도달률과 무관하다","why":"실드가 있으면 엣지 미스가 오리진으로 직행하지 않는다. 실드 무용론은 키 규칙 불일치로 히트율이 깨진 사례와 혼동한 것이다."}],"explanation":"origin shield의 정량 효과는 '오리진 도달률 = 엣지 미스율 × (1 − 실드 히트율)' 하나로 계산된다. 실드가 없으면 각 PoP의 콜드 미스가 오리진을 직접 때려 객체당 최대 PoP 수만큼 중복 fetch가 일어나고, 실드가 있으면 곱셈(N배)이 덧셈(1배)으로 바뀐다. 단 이 식의 전제는 엣지와 실드의 캐시 키 정규화 규칙이 같다는 것 — 규칙이 어긋나면 실드 히트율 자체가 깨져 계층 이득이 소멸한다. 실드는 만능 스위치가 아니라 히트율의 함수다.","source":"AWS CloudFront Developer Guide — Origin Shield(오리진 요청 감소)","sourceLinks":[{"label":"AWS CloudFront — Origin Shield","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html"},{"label":"AWS CloudFront Developer Guide","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html"}]},{"id":"cdn-unsigned-url-001","topic":"cdn","difficulty":2,"question":"Cloud CDN에서 서명 키 검증을 켠 상태다. 공격자가 Expires·KeyName·Signature 쿼리 파라미터를 전부 지운 URL로 요청했고, 그 URL은 아직 CDN에 캐시되어 있지 않다. 결과는?","options":[{"text":"서명이 없다고 판단한 CDN이 검증을 건너뛰고 오리진까지 전달한다","correct":true,"why":"서명 관련 쿼리 파라미터가 없으면 '서명된 URL이 아니네' 하고 바이패스한다. 응답이 캐시될 수도 있다."},{"text":"Cloud CDN이 서명 파라미터 부재를 확인하고 403 Forbidden으로 거절한다","why":"서명 검증이 전 요청에 적용되는 다른 벤더의 동작이나 상식적 기대를 Cloud CDN에 이식한 것이다."},{"text":"CDN은 통과시키지만 오리진이 서명 부재를 보고 거절한다","why":"오리진에 도달하는 URL은 어차피 서명이 벗겨진 모습이라 정상 요청과 구분되지 않는다. 오리진 검증은 헤더의 원본 URL로 해야 한다."},{"text":"서명이 없으면 공개 콘텐츠 요청으로 간주해 캐시에 있을 때만 응답한다","why":"검증을 건너뛴다는 점은 맞지만 캐시 유무로 갈리지 않는다. 미스면 그대로 오리진까지 도달하고 그 응답이 캐시된다."}],"explanation":"GCP Cloud CDN의 서명 검증은 '서명이 있을 때만' 작동한다. 서명 관련 파라미터(Expires·KeyName·Signature)가 아예 없으면 검증 대상이 아니라고 판단해 바이패스하며, 요청은 오리진까지 도달하고 그 응답이 캐시될 수도 있다. 그래서 'CDN 서명 검증 켬 = 보호 완료'가 아니라, 오리진(애플리케이션)의 2차 검증이 벤더 공식 권장이고, 스토리지 직접 접근 차단이 세 번째 층이 된다. 보안 경계는 한 게이트가 아니라 겹겹이 쌓는 구조로 설계해야 한다.","source":"Google Cloud CDN 공식 문서 — Signed URLs(서명되지 않은 요청은 유효성 검사 생략), 당근 2025 GCP 밋업 발표","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"},{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"},{"label":"Google Cloud CDN — 서명된 URL","url":"https://cloud.google.com/cdn/docs/using-signed-urls"}]},{"id":"cdn-origin-revalidation-001","topic":"cdn","difficulty":3,"question":"CDN 레벨에서 만료된 서명 URL을 40x로 차단하도록 설정했다. 그런데도 오리진에서 서명을 한 번 더 검증해 두어야 하는 이유는?","options":[{"text":"CDN의 시계와 오리진의 시계가 어긋나 만료 판정이 틀릴 수 있어서","why":"시계 오차는 검증 일반의 리스크지만 이 권장의 본체가 아니다. 검증이 아예 생략되는 경로가 존재한다는 것이 핵심이다."},{"text":"서명 키를 CDN과 오리진이 서로 다르게 가져야 해서","why":"키 분리·로테이션은 설계 화제일 뿐 재검증 권장과는 무관한 인접 주제다."},{"text":"CDN 검증은 캐시 미스일 때만 수행되어 히트 요청이 무검증이라서","why":"검증 생략의 트리거는 히트 여부가 아니라 서명 파라미터의 부재다. 생략 조건을 잘못 짚었다."},{"text":"서명 파라미터를 지운 요청은 CDN 검증이 생략되어 오리진까지 관통해서","correct":true,"why":"CDN 검증은 서명이 있을 때만 작동하는 선택적 게이트다. 무서명 우회를 막는 최종 게이트는 오리진뿐이다."}],"explanation":"CDN의 만료 차단은 서명 파라미터가 붙어 있는 요청에만 적용된다. 파라미터를 지우고 오면 검증 자체가 생략되어 오리진까지 관통할 수 있고, 그 응답이 캐시되면 만료 정책과 무관하게 계속 서빙될 수 있다. 그래서 만료·서명의 최종 집행자는 오리진이어야 한다는 것이 벤더 공식 권장이며, 당근도 Cloud Run 애플리케이션에서 시그니처를 한 번 더 검증한다. 'CDN 통과 = 검증됨'이 아니라 'CDN 검증은 선택적 게이트'라는 것이 신뢰 경계 설계의 출발점이다.","source":"Google Cloud CDN 공식 문서 Signed URLs — 서명된 요청은 원본에서 다시 검증할 것 권장, 당근 2025 GCP 밋업 발표","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"},{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"},{"label":"Google Cloud CDN — 서명된 URL","url":"https://cloud.google.com/cdn/docs/using-signed-urls"}]},{"id":"cdn-x-client-url-001","topic":"cdn","difficulty":3,"question":"Cloud CDN이 서명 검증에 성공한 요청을 오리진(Cloud Run)으로 보냈더니 URL에서 서명 파라미터가 사라져 있었다. 오리진에서 서명을 검증하려면?","options":[{"text":"파라미터가 없는 요청은 전부 거절하면 된다","why":"CDN을 정상적으로 통과한 요청도 같은 모습으로 도착하므로 서비스가 전면 중단된다. 무서명 요청과의 구분이 문제의 핵심이다."},{"text":"CDN이 이미 검증했으므로 오리진 검증 자체가 불필요하다","why":"CDN 검증은 서명이 있을 때만 작동하는 선택적 게이트라 무서명 우회를 못 막는다. 오리진 검증이 그 빈틈을 메운다."},{"text":"X-Client-Request-URL 헤더에 담긴 원본 URL로 검증한다","correct":true,"why":"CDN은 서명 파라미터를 지운 URL을 오리진에 보내고 원본은 이 헤더에 담아 전달한다. 오리진 검증의 진실 원본은 헤더다."},{"text":"라우팅 규칙에서 서명 파라미터를 다시 붙여 오리진에 전달한다","why":"서명 파라미터가 LB의 호스트·경로 규칙과 충돌하는 것을 예방하려고 지워 주는 설계다. 재삽입은 그 취지를 되돌린다."}],"explanation":"Cloud CDN은 오리진에 요청을 보낼 때 서명 관련 쿼리 파라미터를 제거한다. 이유는 오리진 앞의 Cloud Load Balancing이 호스트·경로 규칙에 쿼리 파라미터를 포함할 수 있어(공식 문서의 A/B 테스팅 예제) 라우팅 충돌을 예방하는 방어적 설계다. 그 결과 정상 검증된 요청과 파라미터를 지운 악의 요청이 오리진에서 똑같은 모습으로 도착해 구분할 수 없으므로, 오리진은 X-Client-Request-URL 헤더에 담겨 온 원본 URL로 시그니처를 재검증해야 한다. 경유층이 요청을 다시 쓴다는 선택지를 상정하지 않으면 오리진 검증 설계 자체가 성립하지 않는다.","source":"Google Cloud CDN 공식 문서 — Use signed URLs(서명 컴포넌트는 오리진 요청 전에 URL에서 제거되고, 원본 서명 URL은 x-client-request-url 요청 헤더로 전달된다), 당근 2025 GCP 밋업 발표 「대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage」","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"},{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"},{"label":"Google Cloud CDN — 서명된 URL","url":"https://cloud.google.com/cdn/docs/using-signed-urls"},{"label":"Google Cloud Run 문서","url":"https://cloud.google.com/run/docs"}]},{"id":"cdn-hitrate-arithmetic-001","topic":"cdn","difficulty":2,"question":"이미지 CDN의 캐시 히트율을 95%에서 98%로 올렸다. 오리진(동적 리사이즈 컴퓨트)의 요청 관점에서 이 변화는?","options":[{"text":"3%p가량 개선된 수준이라 오리진 부하는 거의 비슷하다","why":"히트율의 표면 차이(3%p)를 오리진 축에 그대로 이식한 것이다. 비용은 h가 아니라 1−h에 비례한다."},{"text":"미스율이 5%에서 2%로 줄어 오리진 요청은 60% 감소한다","correct":true,"why":"오리진 도달 요청은 미스율에 비례한다. 2/5 = 40%가 남으므로 60%가 사라진다."},{"text":"CDN 송신료가 히트율만큼 줄어 요금이 함께 내려간다","why":"egress는 전송 총량에 비례하고 히트율과 무관하다. 줄어드는 것은 오리진 쪽 비용뿐이다."},{"text":"히트율은 통계 지표일 뿐 오리진 부하와는 무관하다","why":"오리진 도달 요청 = 전체 × (1−h)라는 직접 관계식이 있다. 무관이 아니라 비례 관계다."}],"explanation":"히트율 논의는 언제나 미스율(1−h)로 환산해야 정확하다. 95→98%는 미스율 5→2%, 즉 오리진 도달 요청이 60% 사라지는 것이다. 오리진이 컴퓨트(동적 리사이즈)라면 미스 1건이 연산을 수반하므로 비용·안정성 축이 그 비율로 줄어든다. 반면 CDN egress는 전송 총량에 비례하므로 이 개선으로 줄지 않는다. 요청 히트율과 바이트 히트율, CDN 비용과 오리진 비용을 축마다 분리해 말하는 것이 정확한 비용 논의의 조건이다.","source":"AWS CloudFront Developer Guide — Origin Shield(오리진 요청 절감)","sourceLinks":[{"label":"AWS CloudFront — Origin Shield","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html"},{"label":"AWS CloudFront Developer Guide","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Introduction.html"}]},{"id":"cdn-egress-axis-001","topic":"cdn","difficulty":1,"question":"캐시 히트율을 90%에서 96%로 올렸는데 CDN 요금은 줄지 않았다. 가장 먼저 확인해야 할 해석은?","options":[{"text":"CDN egress·요청 과금은 히트율과 무관하므로 정상이다","correct":true,"why":"줄어든 것은 미스율에 비례하는 오리진 쪽 비용이다. CDN 청구 축(전송량·요청 수)은 그대로다."},{"text":"히트율 지표가 잘못 집계되고 있으므로 로그를 먼저 의심한다","why":"'히트율이 오르면 요금도 준다'는 전제가 틀렸다. 지표가 아니라 청구 구조를 먼저 본다."},{"text":"히트가 늘면서 캐시 응답의 전송량이 늘어 요금이 오른 것이다","why":"전송량은 요청 수와 객체 크기로 정해질 뿐 히트 여부로 늘지 않는다. 같은 요청에 같은 바이트가 나간다."},{"text":"축출이 늘어 히트율 개선분이 상쇄된 것으로 보면 된다","why":"축출은 히트율 자체에 영향을 주는 인접 요인이지만 요금 불변을 설명하지 못한다. 청구 축이 다르다."}],"explanation":"비용 모델은 대략 '요청 수 × 요청 단가 + 전송 바이트 × egress 단가 + 미스율 × (오리진 컴퓨트+fetch 비용)'이다. 히트율 h는 셋째 항에만 들어가므로, 히트율을 올려도 CDN 청구(첫째·둘째 항)는 전송 구조가 그대로인 한 그대로다. \"히트율을 올렸는데 CDN 요금이 안 줄었다\"는 이상 징후가 아니라 구조의 정상이다. 진짜로 CDN 요금을 줄이려면 전송 바이트 자체(포맷·해상도)나 요청 수 구조를 건드려야 한다.","source":"AWS CloudFront·Google Cloud CDN 공식 요금 문서(데이터 전송·요청 단위 과금)","sourceLinks":[{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"}]},{"id":"cdn-request-pricing-001","topic":"cdn","difficulty":2,"question":"이미지 CDN이다. 객체당 평균 8KB, 하루 요청 20억 건. 비용 구조에서 가장 먼저 봐야 할 축은?","options":[{"text":"전송량 — 하루 16TB를 내보내니 바이트 단가가 청구를 지배한다","why":"전송량은 과금 축이 맞지만 이 트래픽에서는 지배축이 못 된다. 크기 직관이 건당 과금이 쌓이는 축을 가렸다."},{"text":"히트율 — 캐시가 잘 되면 요청 수 자체가 줄어든다","why":"캐시는 요청 수를 줄이지 않는다. 히트율이 줄이는 것은 오리진 도달뿐이다."},{"text":"오리진 egress — 미스가 나면 원본에서 나가는 요금이 청구를 지배한다","why":"오리진 egress도 실제 비용 항목이지만 히트율이 높은 이미지 서빙에서는 미스분에만 발생한다. 20억 건 전체에 곱해지는 축이 아니다."},{"text":"요청 단가 — 객체가 가벼워도 건당 과금은 요청 수에 그대로 곱해진다","correct":true,"why":"전송량이 작아도 요청 20억 건의 단가는 그대로 곱해진다. 이 트래픽의 비용 축은 요청 수다."}],"explanation":"CDN 과금은 전송량과 요청 건당 단가가 함께 있다. 객체가 작고 요청이 많으면 전송량 축은 작아지지만 요청 축은 그대로 커서, 합산에서 요청 단가가 지배할 수 있다. 캐시 히트율은 요청 수 자체를 줄이지 못하므로(오리진 도달만 줄인다) 이 축의 개선은 매니페스트·목록 API 설계 같은 요청 수 구조에서 나온다. 비용 논의는 \"어떤 트래픽 믹스인가\"부터 축을 정하고 시작해야 한다.","source":"AWS CloudFront 공식 요금 문서(요청당 과금), Google Cloud CDN 가격 문서(요청 비용)","sourceLinks":[{"label":"Google Cloud CDN — 가격","url":"https://cloud.google.com/cdn/pricing"},{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"}]},{"id":"cdn-crosscloud-egress-001","topic":"cdn","difficulty":2,"question":"원본은 GCS에 있고 CDN만 AWS CloudFront로 구성하려 한다. 비용 검토에서 반드시 포함해야 할 항목은?","options":[{"text":"없다 — CDN 요금표와 스토리지 요금표만 보면 된다","why":"원본→CDN 구간이 빠진다. 미스가 발생할 때마다 GCS에서 인터넷으로 나가는 egress가 과금된다."},{"text":"GCS 저장 비용이 CDN 캐시 용량만큼 늘어나는 항목","why":"CDN 캐시는 GCS가 아니라 CDN 쪽에 쌓인다. 스토리지 용량과는 무관한 인접 항목이다."},{"text":"원본(GCS)→CDN 구간의 인터넷 egress 비용","correct":true,"why":"크로스 클라우드 조합에서는 미스 fetch가 인터넷 egress로 과금된다. 같은 클라우드면 내부 처리되는 구간이다."},{"text":"CloudFront 캐시 미스율 예산 — 히트율만 높게 잡으면 된다","why":"히트율 관리도 필요하지만 미스가 남는 이상 원본 구간 과금은 계속된다. 항목 자체를 누락했다."}],"explanation":"원본과 CDN이 클라우드를 달리하면 미스 fetch가 인터넷 egress로 과금된다. 같은 클라우드 안이라면 이 구간이 내부 처리되거나 무료가 되는 경우가 많다 — 당근이 Cloud Run과 Cloud Storage 사이 트래픽이 무료라 밝힌 것, 그리고 원본이 GCP로 이사하면서 CDN도 자연스럽게 GCP가 된 이유가 정확히 이 비용 논리다. 미스율에 비례하는 비용은 오리진 fetch와 오리진 egress까지 포함하므로, 원본 위치 선정은 CDN 선택과 함께 계산되어야 한다.","source":"Google Cloud Storage 공식 요금 문서(인터넷 egress), 당근 2025 GCP 밋업 발표(Cloud Run↔GCS 무료 트래픽)","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"},{"label":"Google Cloud Run 문서","url":"https://cloud.google.com/run/docs"}]},{"id":"cdn-multicdn-cost-001","topic":"cdn","difficulty":2,"question":"가용성을 이유로 멀티 CDN 도입을 검토 중이다. 함께 사야 할 비용 중 구조적으로 가장 큰 것은?","options":[{"text":"캐시가 벤더별로 쪼개져 어느 쪽도 히트율이 오르지 않는 것","correct":true,"why":"벤더마다 캐시가 따로 쌓여 콜드 미스가 재발하고 무효화 API·로그 필드·서명 방식이 갈라진다. 보험의 보험료다."},{"text":"물량 커밋이 두 벤더로 나뉘어도 각각 할인 구간에 들어가 단가가 내려가는 것","why":"트래픽을 나눠도 총량은 같고 커밋 할인은 계약 조건이다. 자동 할인은 성립하지 않는다."},{"text":"벤더별 대시보드가 늘어 모니터링 SaaS 좌석 요금이 추가되는 것","why":"운영 대상이 N벌이 되어 비용은 늘어난다. 가용성과 운영 단순성은 반대 방향으로 움직인다."},{"text":"DNS 레코드와 TLS 인증서를 벤더마다 관리해야 하는 것","why":"DNS는 진입 비용일 뿐이다. 캐시 분할과 일관성 전선이라는 본체를 못 보았다."}],"explanation":"멀티 CDN은 무료 보험이 아니라 히트율과 운영 단순성으로 사는 보험이다. 각 벤더가 자기 캐시를 따로 채우므로 전체 히트율은 벤더별 히트율의 가중합이 되고, 트래픽을 옮기는 순간 콜드 미스가 재발한다. 여기에 무효화 API·서명 방식·로그 필드가 벤더마다 달라 일관성을 위한 별도 설계(이벤트 소싱 무효화, 로그 정규화 스키마)가 필요하다. 히트율이 이미 98%+이거나 무효화 정확성이 생명인 서비스에서는 이 대가가 이득을 넘을 수 있다 — 도입은 계산된 교환이어야 한다.","source":"Google Cloud CDN·AWS CloudFront 공식 무효화 API 문서(벤더별 단위·전파 상이)","sourceLinks":[{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"}]},{"id":"cdn-cold-failover-001","topic":"cdn","difficulty":3,"question":"장애로 트래픽을 예비 CDN으로 전환한 직후 오리진(Cloud Run 리사이즈)에 부하가 폭증했다. 가장 유력한 원인은?","options":[{"text":"예비 CDN의 PoP가 오리진에서 지리적으로 멀어져서","why":"거리는 왕복 지연의 축이지 오리진 요청 수를 늘리지 않는다. 폭주의 원인이 아니다."},{"text":"양쪽 CDN에서 같은 객체를 동시에 받아 이중 요청이 된 것이다","why":"전환은 트래픽을 옮기는 것이지 복제하는 게 아니다. 요청 수는 대체로 그대로다."},{"text":"예비 CDN이 오리진의 Cache-Control을 무시해서","why":"헤더 해석 차이는 실재하는 벤더 간 함정이지만 폭주의 시점(전환 직후)과 맞지 않는다."},{"text":"캐시가 비어 있어 인기 객체 전체가 동시에 미스로 오리진을 때린 것이다","correct":true,"why":"전환 대상 CDN은 콜드 상태다. 히트율 98% 서비스도 빈 캐시 앞에서는 미스 폭주가 된다."}],"explanation":"전환 대상 CDN의 캐시는 비어 있다. 인기 객체 전체가 동시에 미스로 나가면서 오리진에 순간 폭주가 걸리고, 오리진이 컴퓨트(동적 리사이즈)라면 미스 1건이 연산을 수반해 더 치명적이다. 방어는 점진 램프(소량→전량), 인기 상위 객체 사전 워밍, 실드·요청 병합·레이트리밋의 조합이다. 페일오버 설계에서 '전환 자체가 만드는 부하'를 별도 축으로 다루지 않으면, 장애 복구가 두 번째 장애를 만드는 구조가 된다.","source":"당근 2025 GCP 밋업 발표(미스 = Cloud Run 동적 리사이즈 비용, 1% 카나리의 점진 램프 감각)","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"},{"label":"Google Cloud Run 문서","url":"https://cloud.google.com/run/docs"}]},{"id":"cdn-warm-standby-001","topic":"cdn","difficulty":2,"question":"평시 트래픽 100%를 주력 CDN에 두고, 장애 때만 예비 CDN으로 넘기는 계획이다. 이 계획의 구조적 결함은?","options":[{"text":"CDN을 두 벤더로 나누면 캐시가 쪼개져 어느 쪽도 히트율이 안 오르는 것이다","why":"이 계획은 평시 트래픽을 한 곳에만 보내므로 캐시가 쪼개지지 않는다. 멀티 CDN 일반론을 트래픽 분배가 없는 구성에 잘못 적용한 것이다."},{"text":"예비 CDN이 콜드 상태로 있어 전환 시 캐시와 운영 경로가 검증 안 된 것이다","correct":true,"why":"평시 소량 트래픽(웜 스탠바이)이 캐시를 데우고 런북을 상시 검증한다. 실전이 처음이어서는 안 된다."},{"text":"예비 CDN 계약의 최소 사용량 약정 때문에 평시에도 비용이 나가는 것이다","why":"약정이 붙는 계약도 있지만 그것은 조건 협상의 문제다. 이 계획이 구조적으로 사는 것은 비용이 아니라 검증되지 않은 전환 경로다."},{"text":"평시에 예비 CDN으로 트래픽을 조금 흘리면 주력 히트율이 떨어지는 것이다","why":"방향을 뒤집었다. 소량 상시 분배는 히트율을 조금 내주고 전환 경로의 검증을 사는 거래이며, 이 계획의 결함을 닫는 표준 처방이다."}],"explanation":"페일오버는 실전이 처음이 아니어야 한다. 평시 100/0 배분은 예비 CDN의 캐시를 항상 콜드로 두고, 전환 절차·계약 커밋·모니터링 경로도 검증되지 않은 채 둔다. 그래서 평시에도 제2벤더에 소량 트래픽을 흘려 두는 웜 스탠바이가 정석이다 — 캐시가 데워지고, 런북이 상시 리허설되며, 인기 객체의 콜드 기간이 압축된다. '장애 때만 쓰는 벤더'는 장애 때 쓸 수 없는 벤더가 되기 쉽다.","source":"AWS Route 53 헬스체크·DNS 페일오버 공식 문서(웜 스탠바이 구성)","sourceLinks":[{"label":"AWS Route 53 — DNS 페일오버","url":"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html"}]},{"id":"cdn-dns-ttl-001","topic":"cdn","difficulty":2,"question":"DNS 기반 분배 멀티 CDN에서 벤더 A 장애를 감지하고 즉시 가중치를 반전했다. 사용자 트래픽이 B로 넘어가는 속도를 지배하는 것은?","options":[{"text":"감지 알고리즘의 속도 — 감지만 빠르면 전환도 빨라진다","why":"감지와 전환은 별개 축이다. 감지된 뒤에도 DNS 응답이 퍼지는 속도가 상한이다."},{"text":"헬스체크 주기 — 프로브 간격이 곧 전환 지연이다","why":"헬스체크는 감지 축이다. 감지된 뒤의 트래픽 수렴은 다른 메커니즘에 묶인다."},{"text":"DNS 레코드의 TTL과 리졸버 캐시가 상한이다","correct":true,"why":"권한 DNS 응답이 바뀌어도 리졸버와 OS 캐시가 TTL만큼 붙잡는다. 실제 수렴은 분 단위다."},{"text":"CDN 간 로드밸런서가 트래픽을 즉시 재분배한다","why":"DNS 기반 구성에는 그런 중간 계층이 없다. 프록시 계층 구성과 혼동했다."}],"explanation":"DNS 기반 분배의 제어 루프는 분 단위다. 권한 DNS의 응답을 바꿔도 재귀 리졸버와 OS·브라우저 캐시가 TTL만큼 옛 값을 붙잡고 있어, 실제 사용자 트래픽의 수렴은 TTL + 캐시 초과분까지 늘어진다. TTL을 낮추면 페일오버는 빨라지지만 DNS 조회량과 권한 DNS 의존이 커진다 — 페일오버 속도와 DNS 비용의 트레이드오프다. 즉시 전환이 요구사항이면 분배 방식 자체를 클라이언트 사이드로 바꿔야 한다.","source":"RFC 1035 §4.1.3(TTL — 리소스 레코드를 캐시할 수 있는 시간), AWS Route 53 DNS 페일오버 공식 문서","sourceLinks":[{"label":"RFC 1035 §4.1.3","url":"https://www.rfc-editor.org/rfc/rfc1035.html#section-4.1.3"},{"label":"AWS Route 53 — DNS 페일오버","url":"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html"}]},{"id":"cdn-client-side-switch-001","topic":"cdn","difficulty":3,"question":"동영상 서비스가 두 CDN을 운영한다. 한 벤더에 장애가 나면 재생 중인 사용자가 버퍼를 소진하기 전에 다른 벤더로 옮겨야 한다는 요구사항이다. 맞는 분배 방식은?","options":[{"text":"DNS 기반 가중치 — 벤더 CNAME 응답을 바꿔 주면 충분하다","why":"DNS는 TTL만큼 지연된다. 요청 단위 전환 요구와 맞지 않는다."},{"text":"CNAME 체이닝 — 앞단 프록시 계층이 업스트림 벤더를 골라 준다","why":"즉시성은 있지만 그 계층이 새로운 단일 장애점·지연 홉이 된다. 요구사항의 주체(플레이어)와 맞지 않는다."},{"text":"지역별 GSLB 상품으로 바꿔 지연 기반 라우팅을 적용한다","why":"GSLB도 결국 DNS 응답을 바꾸는 방식이라 TTL 상한은 그대로다."},{"text":"클라이언트 사이드 선택 — 플레이어가 매니페스트의 세그먼트 호스트를 바꾼다","correct":true,"why":"ABR 플레이어는 어차피 매니페스트를 해석해 세그먼트를 고른다. 호스트만 바꾸면 DNS 없이 요청 단위 전환이 된다."}],"explanation":"전환 지연 요구가 '요청 단위'이면 클라이언트가 선택권을 가져야 한다. ABR 스트리밍은 플레이어가 매니페스트를 해석해 세그먼트를 고르는 구조라, 세그먼트 호스트만 벤더별로 바꿔 주면 DNS 개입 없이 다음 요청부터 전환이 가능하다. 여기에 타임아웃·재버퍼링 같은 실측(RUM) 신호를 얹어 벤더 성능을 관측하고 반영할 수도 있다. 웹 브라우저에는 이런 훅을 걸 곳이 적어 DNS 기반이 현실적이다 — 플랫폼별로 정답이 다르다는 것 자체가 분배 방식 설계의 내용이다.","source":"RFC 8216(HTTP Live Streaming — 매니페스트 기반 세그먼트 URL 선택)","sourceLinks":[{"label":"RFC 8216","url":"https://www.rfc-editor.org/rfc/rfc8216.html"}]},{"id":"cdn-partial-outage-001","topic":"cdn","difficulty":3,"question":"전역 5xx 알럿은 잡히지 않았는데 한국 지역 사용자의 이미지 로딩 실패만 급증했다. 감지 체계의 무엇을 고쳐야 하나?","options":[{"text":"지역별로 쪼갠 자체 RUM 계측을 갖춘다","correct":true,"why":"실제 장애는 특정 리전·PoP만 나쁜 부분 장애다. 전역 집계에 희석되지 않게 차원을 나눠야 보인다."},{"text":"벤더의 지역별 상태 API를 폴링해 리전 이상을 알럿으로 받는다","why":"벤더가 스스로 이상으로 인정한 것만 올라온다. 사용자에게만 보이는 부분 실패는 그 API에 끝까지 나타나지 않는다."},{"text":"전역 알럿의 임계를 낮춰 작은 이탈도 잡히게 한다","why":"임계를 낮추면 노이즈 알럿이 폭주한다. 문제는 임계가 아니라 층화 부재다."},{"text":"한국 리전 오리진 서버의 리소스 알럿을 새로 단다","why":"끊김의 원인이 엣지 국부에 있으면 오리진 지표에는 나타나지 않는다. 감지 계층을 오리진으로 옮겨 짚은 착오다."}],"explanation":"실제 CDN 장애의 모습은 '벤더 전체 다운'이 아니라 특정 리전·PoP·경로만 나쁜 부분 장애다. 전역 집계에 섞이면 이탈이 희석되어 임계를 넘지 않고, 벤더 statuspage도 늦고 불완전할 수 있다. 그래서 지역×캐시상태×오류코드 차원을 로그에 남기고 쪼개서 보는 자체 계측이 원본이며, 임계는 절대값보다 동일 시간대 베이스라인 대비 이탈로 잡는 것이 노이즈에 강하다. 당근이 LB 로그를 BigQuery에 쌓고 서비스별 대시보드로 보는 것도 같은 구조 — 관측의 차원 설계가 감지 능력의 상한을 정한다.","source":"당근 2025 GCP 밋업 발표(LB 로그→BigQuery→대시보드, 지역·서비스별 관측)","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"}]},{"id":"cdn-synthetic-rum-001","topic":"cdn","difficulty":3,"question":"합성 모니터링에서 예비 벤더가 30% 빨라 주력을 교체했다. 캐시가 충분히 웜해진 뒤에도 실사용자 p95는 나빠진 상태다. 가장 유력한 설명은?","options":[{"text":"교체 직후 캐시가 콜드해서 일시적으로 나쁘게 측정된 것이다","why":"콜드 효과는 실재하지만 시간이 지나도 회복되지 않는다면 설명이 되지 않는다."},{"text":"소수 뷰포인트의 합성 점수가 실사용자 분포를 대표하지 못한 것이다","correct":true,"why":"합성은 통제된 재현성 있는 측정이지 대표 표본이 아니다. 벤더 비교 기준은 층화된 RUM이어야 한다."},{"text":"실사용자 지표가 노이즈라서 합성 점수가 진짜 성능이다","why":"RUM은 편차가 크지만 진짜 경험이다. 합성을 진실로 삼는 순간 측정 지점 편향을 못 본다."},{"text":"신규 벤더의 TLS 인증서 검증이 느려 첫 연결이 느린 것이다","why":"핸드셰이크 축의 가능성은 있으나 p95 전반 악화의 주된 설명으로는 근거가 없다."}],"explanation":"합성 모니터링은 내가 통제하는 소수 뷰포인트·조건에서 재현성 있게 재는 도구다. 그 강점이 곧 한계라, 실사용자의 지역·ISP·디바이스 분포를 대표하지 못한다. 벤더 비교의 기준값은 RUM에 두되 CDN 식별 태그·지역 층화·동시간대 비교를 갖춰야 공정하다. 합성은 회귀 감지와 페일오버 판정 입력으로, RUM은 사용자 경험의 진실로 — 역할 분담을 정해 두지 않으면 '점수로 갈았는데 실제는 나빠졌다'는 함정에 빠진다.","source":"Google Cloud Synthetic Monitoring 공식 문서(지정된 위치에서 수행)","sourceLinks":[{"label":"Google Cloud Monitoring — 가동 시간 확인","url":"https://cloud.google.com/monitoring/uptime-checks"}]},{"id":"cdn-auto-failover-001","topic":"cdn","difficulty":2,"question":"CDN 벤더 장애 시 자동 페일오버를 도입하려 한다. 자동화가 만드는 가장 큰 리스크는?","options":[{"text":"자동화 스크립트의 버그 — 사람이 했으면 안 생겼을 오류","why":"버그는 자동·수동 모두에 있다. 자동화 고유의 리스크는 판단의 결여에서 나온다."},{"text":"페일오버 대상 벤더의 단가가 평시보다 비싸 전환할 때마다 비용이 튀는 것","why":"전환 비용이 튀는 것은 청구서에서 끝나는 문제다. 자동화가 새로 만드는 리스크는 그보다 가용성 쪽에 있고, 거기까지 한 걸음이 남았다."},{"text":"계측 오류·국부 장애를 전체 장애로 읽고 정상 벤더를 버리는 것","correct":true,"why":"자동 판정은 신호 품질에 묶인다. 다수결·지속 조건·점진 램프·원상복구로 블래스트 레디어스를 제한한다."},{"text":"알럿이 자주 울려 on-call의 대응 감각이 무뎌지는 것","why":"알럿 피로는 실재하는 운영 문제지만 페일오버 자동화의 고유 리스크는 아니다."}],"explanation":"자동 전환은 복구 시간을 줄이지만 판단을 내리는 주체가 신호 품질에 종속된다. 계측 오류나 특정 리전의 국부 장애를 전체 장애로 읽으면 살아 있는 벤더를 버리게 되고, 전환 자체가 콜드 캐시 폭주라는 새 사고를 만들 수 있다. 실무 타협은 짧은 윈도의 다수결·지속 조건, 점진 램프, 자동 원상복구로 블래스트 레디어스를 제한하는 것이다. 조직의 운영 성숙도에 맞춰 '자동 판정 + 램프'와 '자동 알럿 + 수동 런북' 사이를 고르는 것이 설계자의 답이다.","source":"AWS Route 53 헬스체크·DNS 페일오버 공식 문서","sourceLinks":[{"label":"AWS Route 53 — DNS 페일오버","url":"https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover.html"}]},{"id":"cdn-query-key-default-001","topic":"cdn","difficulty":3,"question":"이미지 CDN에서 리사이즈 옵션을 쿼리 파라미터(w=300&f=webp)로 내고, 캐시 키에 쿼리 전체를 포함하는 기본값을 그대로 쓴다(당근 구조). 이 선택이 성립하는 근거는?","options":[{"text":"쿼리 전체 키가 업계 표준이라 커스터마이징이 불필요해서","why":"기본값은 표준이 아니라 벤더 기본일 뿐이다. 근거가 아니라 관성이다."},{"text":"트래킹 파라미터는 트래픽이 적어 미스 영향을 무시할 수 있어서","why":"임의 파라미터의 미스·오염 리스크는 실재하고 완화는 화이트리스트다. 이 설계가 성립하는 근거가 아니다."},{"text":"쿼리가 키에 있으면 전부 히트되어 오리진이 쉴 수 있어서","why":"쿼리 축이 늘수록 키는 잘게 쪼개진다. 히트가 아니라 분열의 방향이다."},{"text":"파라미터가 변형을 정의해 값이 바뀌면 미스→오리진이 동적 생성하는 것이 의도라서","correct":true,"why":"키 공간 = 변형 공간이 성립한다. w=300&f=webp는 곧 '300px WebP 변형'이라는 비즈니스 객체다."}],"explanation":"캐시 키 설계의 원칙은 '키 공간 = 실제 변형 공간'이다. 리사이즈 파라미터가 변형을 정의하는 구조에서는 쿼리 전체 키가 오히려 정합적이다 — 값이 바뀌면 미스가 나고, 오리진(Cloud Run)이 그 변형을 동적으로 만들어 채운 뒤 캐시되는 것이 의도한 동작이다(당근의 기본 키 선택). 남는 리스크는 변형이 아닌 파라미터(utm·추적 코드)가 키에 섞이는 것이고, 그 완화는 화이트리스트다. '쿼리는 캐시의 적'이라는 경구는 트래킹 파라미터 맥락에서만 참이지 변형 파라미터 맥락에서는 거짓이다.","source":"당근 2025 GCP 밋업 발표(기본 캐시 키 — 쿼리 매개변수가 바뀌면 미스, Cloud Run 동적 리사이즈)","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"},{"label":"Google Cloud Run 문서","url":"https://cloud.google.com/run/docs"}]},{"id":"cdn-param-order-001","topic":"cdn","difficulty":2,"question":"CloudFront 앞의 이미지 서비스에서 오리진 요청이 눈에 띄게 늘었다. 로그를 보니 w=300&f=webp 로 저장된 엔트리 옆에 f=webp&w=300 miss가 이미지마다 나란히 쌓인다. 원인은?","options":[{"text":"TTL이 짧아 같은 변형의 엔트리가 만료를 반복해 재검증이 늘어난 것이다","why":"만료로 인한 재검증은 같은 문자열 안의 이야기다. 신선도 축을 카디널리티 축에 겹쳐 읽었다."},{"text":"요청이 서로 다른 PoP로 흩어져 캐시가 아직 각 PoP에 퍼지지 않은 것이다","why":"콜드 PoP의 중복 fetch는 실재하는 현상이지만 같은 쌍이 이미지마다 반복되는 규칙성은 못 만든다."},{"text":"쿼리 문자열이 하나라도 다르면 CloudFront는 다른 키로 캐싱하기 때문이다","correct":true,"why":"같은 의미의 URL이라도 문자열이 다르면 별도 엔트리가 된다. 순서·대소문자가 대표 사례다."},{"text":"CloudFront가 파라미터를 알파벳 순으로 정렬해 정규화하는데 그 기능이 꺼져 있는 것이다","why":"CloudFront에 파라미터 정렬 기능은 없다. 캐시 키에 포함할 파라미터를 고를 수는 있어도 순서를 맞춰 주지는 않는다. 정규화는 URL을 만드는 쪽의 책임이다."}],"explanation":"CloudFront는 쿼리 파라미터의 순서·대소문자에 따라 다르게 캐싱한다(당근이 2019년에 실측해 명시한 동작). 같은 변형을 가리키는 요청이 철자만 다르면 별도 캐시 엔트리가 되어 히트율이 분열한다. 해법은 CDN을 바꾸는 것이 아니라 URL을 만드는 쪽에서 파라미터 순서와 대소문자를 고정하는 생성 규칙이거나, CDN의 캐시 키 정책으로 정규화·화이트리스트를 명시하는 것이다. 정규화는 캐시 위생의 최소 단위이며 두 자릿수 % 포인트를 움직이는 축이다.","source":"당근 테크블로그 「AWS Lambda@Edge에서 실시간 이미지 리사이즈 & WebP 형식으로 변환」(2019) — CloudFront 쿼리 파라미터 순서·대소문자 캐싱","sourceLinks":[{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"cdn-key-hygiene-first-001","topic":"cdn","difficulty":3,"question":"히트율 88% 이미지 CDN의 개선 예산이 한 가지뿐이다. 어디에 쓰는 것이 가장 큰 효과를 내나?","options":[{"text":"축출 정책 교체 — LRU에서 LFU로 전환하는 작업","why":"같은 용량에서 정책 간 차이는 한 자릿수 % 포인트다. 유의미하지만 가장 큰 축이 아니다."},{"text":"캐시 키 정규화 — 파라미터 화이트리스트 적용 작업","correct":true,"why":"가짜 키 축(utm·순서·쿠키)이 만드는 분열은 두 자릿수 % 포인트를 움직인다. 키 위생이 먼저다."},{"text":"캐시 용량을 지금의 두 배로 늘리는 증설 작업","why":"롱테일 분포에서 추가 용량이 흡수하는 꼬리의 기여는 작다. 비례 개선이 되지 않는다."},{"text":"전 객체의 TTL을 1년으로 일괄 상향하는 작업","why":"변경되는 객체에서 무효화 문제가 커지고 만료 집중 구조도 그대로다. 정규화와는 다른 축이다."}],"explanation":"같은 용량에서 축출 정책 간 히트율 차이는 보통 한 자릿수 % 포인트지만, 키 정규화·화이트리스트는 두 자릿수 % 포인트를 움직인다. 트래킹 파라미터·순서 차이·쿠키 같은 가짜 축은 실제 변형이 아닌 엔트리를 무한히 만들어 트래픽을 분열시키기 때문이다. 용량 증설은 롱테일 특성상 수익이 체감되고, TTL 상향은 무효화 문제를 다른 곳으로 미룰 뿐이다. 개선 순서는 키 위생 → 용량·계층 → 정책 튜닝이며, 원인 규명 없는 파라미터 조정은 이 우선순위를 흐린다.","source":"당근 테크블로그(2019) — 쿼리 순서·대소문자 캐시 분열 실측, AWS CloudFront 캐시 정책 공식 문서","sourceLinks":[{"label":"AWS CloudFront — 캐시 키와 캐시 정책","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/controlling-the-cache-key.html"},{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"cdn-serverless-neg-001","topic":"cdn","difficulty":1,"question":"Google Cloud에서 Cloud CDN의 오리진을 Cloud Run 애플리케이션으로 지정하려 한다. 필요한 구성은?","options":[{"text":"Cloud Run을 백엔드 서비스로 LB에 직접 등록하면 된다","why":"Cloud Run은 LB 백엔드로 직접 붙지 못한다. 다른 클라우드의 인스턴스 그룹 감각을 이식한 것이다."},{"text":"Cloud Run URL로 리다이렉트하도록 LB 규칙을 만들면 된다","why":"리다이렉트는 왕복을 추가하고 CDN 캐시 구조도 깨진다. 오리진 지정이 아니다."},{"text":"Serverless NEG로 Cloud Run을 LB 백엔드에 매핑하면 된다","correct":true,"why":"캐싱 가능 콘텐츠를 원본으로 내리려면 부하분산기가 필요하고 Cloud Run은 서버리스 NEG로 매핑된다 — 공식 권장이다."},{"text":"Cloud Run 인그레스를 내부 전용으로 바꿔 직접 이으면 된다","why":"인그레스 정책은 접근 경계의 문제다. LB·NEG 없이는 CDN의 오리진이 될 수 없다."}],"explanation":"GCP에서 캐싱 가능한 콘텐츠를 Cloud Run으로 서빙하려면 Cloud CDN → Cloud Load Balancing(L7) → Serverless NEG → Cloud Run의 연결이 공식 권장 구성이다. Cloud Run은 LB 백엔드로 직접 등록되지 않고 반드시 서버리스 NEG로 매핑되어야 하며, TLS 인증서는 Certificate Manager가 프론트엔드에 주입한다. 이 그림은 그냥 외울 게 아니라 '오리진이 컴퓨트일 때 검증 지점이 어디로 가는가'의 사례로 소화할 것 — 앞의 서명 검증 문제들이 정확히 이 구조 위에서 터진다.","source":"Google Cloud 공식 문서 — Cloud Run 캐싱 가능한 콘텐츠 서빙(부하분산기 + 서버리스 NEG 매핑), 당근 2025 GCP 밋업 발표","sourceLinks":[{"label":"당근 2025 GCP 밋업 — 대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage","url":"https://www.youtube.com/watch?v=aFauN9cs8_I"},{"label":"Google Cloud Run 문서","url":"https://cloud.google.com/run/docs"}]},{"id":"cdn-vary-key-split-001","topic":"cdn","difficulty":2,"question":"개인화 미들웨어를 켠 뒤 이미지 CDN의 히트율이 89%에서 40%대로 떨어졌다. 오리진 응답에는 Vary: Cookie 헤더가 새로 붙었다. 원인은?","options":[{"text":"쿠키 값이 응답과 함께 저장되어 객체가 커지고 축출이 빨라진 것이다","why":"Vary 값은 응답에 복사되지 않는다. 용량·축출 축이 아니라 키 카디널리티 축의 문제다."},{"text":"쿠키 값이 캐시 키에 추가 축으로 들어가 방문자 수만큼 같은 이미지가 쪼개져 저장된 것이다","correct":true,"why":"Vary는 지정한 요청 헤더의 값마다 별도 변형을 저장하라고 명령한다. Cookie 축의 카디널리티는 사용자 수다."},{"text":"Vary는 응답 부가정보일 뿐 캐싱 동작을 바꾸지 않고, 미들웨어가 응답 본문을 수정해 무효화가 일어난 것이다","why":"Vary는 부가정보가 아니라 캐시 키 정의의 일부다. 원인을 본문 변경으로 옮겨 놓았다."},{"text":"쿠키가 붙은 요청은 개인화 트래픽으로 분류되어 CDN이 캐시를 우회시키는 것이다","why":"쿠키가 있으면 무조건 우회하는 건 특정 구성·벤더의 동작이다. 여기선 캐시는 되지만 방문자 수만큼 쪼개진다."}],"explanation":"Vary는 캐시 키에 사실상 축을 추가한다. Vary: Accept-Encoding이라면 축의 값이 gzip/br/identity 몇 개로 끝나지만, Vary: Cookie는 값이 사용자 수만큼 있어 같은 이미지가 방문자별로 따로 캐시된다. 히트율 89%가 40%대로 무너지는 전형적인 패턴이고, 캐시 키 위생 검토의 첫 확인 항목이 'Vary에 뭐가 붙었나'다. 미들웨어가 넣는 실험·개인화 헤더는 값의 종류가 통제 불가능하므로, 이미지처럼 정적 자원에는 애초에 붙지 않아야 한다.","source":"RFC 9111 §4.1(Calculating Cache Keys with the Vary Header Field)","sourceLinks":[{"label":"RFC 9111 §4.1","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1"}]},{"id":"cdn-byte-hit-rate-001","topic":"cdn","difficulty":3,"question":"VOD CDN의 요청 기준 캐시 히트율이 99%다. 오리진 송신(egress) 트래픽은 한 달째 예산의 3배를 맴돈다. 가장 유력한 설명은?","options":[{"text":"미스가 고화질 대용량 세그먼트에 몰려 있어, 바이트 기준 미스 비중이 요청 기준 1%보다 훨씬 크다","correct":true,"why":"히트율의 분모는 요청 수다. 오리진 트래픽은 바이트 축이라 희소한 미스의 '무게'가 청구를 지배한다."},{"text":"요청의 99%가 캐시에서 나오니 오리진 egress도 총량의 1% 수준이어야 하고, 3배라는 수치는 계량 오류다","why":"요청 축의 비율을 바이트 축으로 그대로 이식했다. 미스가 큰 객체에 몰리면 바이트 비중은 수 배가 된다."},{"text":"만료된 세그먼트 재검증이 조건부 요청을 계속 만들어 오리진 왕복이 늘어난 것이다","why":"재검증 왕복(304)은 수십 바이트짜리 응답이다. 대역폭을 지배하는 건 풀 객체를 다시 받는 미스다."},{"text":"매일 올라오는 신규 에피소드의 초기 미스가 콜드 스파이크로 누적된 것이다","why":"신규 콘텐츠의 콜드 미스는 실재하는 요인이지만 예산 3배를 한 달 유지하는 구조적 설명이 되지 못한다."}],"explanation":"캐시 히트율은 요청 축 지표고, 오리진 egress는 바이트 축 현상이다. 두 축은 미스의 분포가 어디에 무게를 두느냐에 따라 크게 벌어진다 — 4K 세그먼트 하나의 미스는 썸네일 수백 개의 미스와 같은 바이트다. 요청 히트율 99%는 미스된 1%가 대용량 콘텐츠에 몰려 있으면 오리진 트래픽 관점에서 훨씬 나쁜 숫자다. 그래서 VOD·대용량 배포에서는 요청 히트율과 바이트 히트율을 나눠 보는 게 기본이고, 예산은 미스 바이트 분포 기준으로 잡아야 한다.","source":"AWS CloudFront 공식 문서 — 모니터링 및 메트릭(캐시 히트율은 요청 기준 지표)","sourceLinks":[{"label":"CloudFront 메트릭 보기","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/viewing-cloudfront-metrics.html"}]},{"id":"cdn-log-sampling-001","topic":"cdn","difficulty":2,"question":"드물게 터지는 5xx 계열 오류의 실제 발생 규모를 잡고 싶다. 표준 액세스 로그로 세는 운영 대시보드에서 가장 먼저 확인할 일은?","options":[{"text":"로그 전달 지연을 흡수하도록 집계 창을 하루 미뤄 반영한다","why":"전달 지연은 추이를 흔들지만 건수 자체의 계통적 축소를 고치지 못한다. 병목을 잘못 짚은 조치다."},{"text":"오리진에 도달한 요청만 남도록 로그 필터를 좁혀 잡음을 없앤다","why":"엣지 액세스 로그의 기록 단위를 오리진으로 옮겨 읽었다. 엣지에서 난 5xx가 오히려 사라진다."},{"text":"표준 로그의 샘플링 정책을 확인하고, 필요하면 전량 스트리밍 로그로 옮긴다","correct":true,"why":"샘플에서 희귀 이벤트는 놓치거나 과소계상된다. 집계의 전제인 기록 정책을 먼저 검증해야 한다."},{"text":"로그 설정에서 전체 요청 기록 토글을 켜면 표준 로그도 전량 기록이 된다","why":"표준 로그에는 그 토글이 없다. 전량을 원하면 상품(실시간 로그) 자체를 바꿔야 한다. 다른 설정을 이식한 믿음이다."}],"explanation":"많은 CDN의 표준 액세스 로그는 전수 기록을 보장하지 않는다. 베스트 에포트로 전달되고 샘플링될 수 있어, 발생 빈도가 낮은 이벤트는 표본에서 계통적으로 과소 계상된다. 희귀 오류·보안 이벤트처럼 꼬리를 세는 계측이라면 로그 상품의 기록 정책부터 확인하고, 필요하면 샘플링 비율을 정할 수 있는 실시간(스트리밍) 로그로 올려야 한다. '로그 = 전수 조사'라는 등식이 대시보드의 정밀도를 결정하는 숨은 전제다.","source":"AWS CloudFront 공식 문서 — 액세스 로그(전체 요청 기록 미보장) 및 실시간 로그(샘플링 비율 설정)","sourceLinks":[{"label":"CloudFront 표준 로그","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html"},{"label":"CloudFront 실시간 로그","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/real-time-logs.html"}]},{"id":"cdn-cache-status-expired-001","topic":"cdn","difficulty":1,"question":"캐시 동작 점검 중 같은 URL을 반복 요청했더니 응답 상태가 처음엔 Miss, 한동안 뒤엔 Expired로 표시됐다. Expired가 말해주는 것은?","options":[{"text":"시간이 지나는 동안 캐시가 꽉 차 엔트리가 밀려나 다시 받아온 것이다","why":"밀려남은 축출(eviction)의 이야기다. Expired는 존재하되 신선도가 지난 상태다."},{"text":"그 URL은 캐시된 적 없이 매번 오리진까지 간 것이다","why":"매번 오리진 직행은 Miss의 의미다. Expired는 저장 이력을 전제하는 상태다."},{"text":"캐시에 있던 엔트리의 TTL이 지나 오리진 재검증을 다녀온 것이다","correct":true,"why":"Expired는 '있었는데 오래됐다'는 뜻이다. 재검증 뒤 갱신되거나 그대로 다시 서빙된다."},{"text":"오리진이 무효화 신호를 보내 캐시에서 내보낸 것이다","why":"무효화는 운영자가 CDN API로 호출하는 것이지 오리진이 자동으로 시그널을 보내는 게 아니다."}],"explanation":"캐시 상태 값은 각각 다른 사건을 가리킨다. Miss는 저장된 적 없는 첫 요청, Expired는 저장돼 있던 엔트리의 TTL이 지나 재검증을 다녀왔다는 뜻이고, 둘 다 캐시가 정상 동작한다는 증거다. 밀려남은 용량 문제(축출), 지워짐은 운영자 호출(무효화)의 영역이라 상태 표기로 구분된다. 상태 헤더를 읽을 줄 알면 배포 직후 '캐시가 깨졌다'는 오진을 내리지 않고, TTL과 재검증 주기가 의도대로 걸려 있는지도 확인할 수 있다.","source":"Cloudflare 개발자 문서 — 캐시 상태 응답 헤더 값 정의(EXPIRED 등), RFC 9111 §4(신선도 및 재검증)","sourceLinks":[{"label":"RFC 9111 §4","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4"}]},{"id":"cdn-compression-variant-001","topic":"cdn","difficulty":3,"question":"오리진이 Vary: Accept-Encoding을 붙여 gzip 표현을 보냈고 CDN에 캐시해 두었다. 여기에 브라우저가 Accept-Encoding: br만 담아 요청하면 CDN은?","options":[{"text":"캐시된 gzip을 CDN이 그 자리에서 br로 재압축해 히트로 서빙한다","why":"캐시는 보통 저장된 표현을 즉석 변환하지 않는다. 인코딩별 표현은 오리진이 내려주는 값이다."},{"text":"br은 별도 변형이라 미스가 나고, 오리진에서 br 표현을 받아 따로 캐시한다","correct":true,"why":"Vary는 인코딩마다 별도 엔트리를 만든다. gzip 엔트리는 br 요청을 만족시키지 못한다."},{"text":"미지원 인코딩 요청엔 저장된 gzip으로 응답하는 게 하위 호환 관행이라 gzip을 내준다","why":"옛 서버·프락시 시절의 관행을 현재로 가져왔다. 클라이언트가 받을 수 없는 인코딩을 강제하면 안 된다."},{"text":"이 CDN은 인코딩과 무관하게 단일 표현만 저장하므로 gzip이 그대로 나간다","why":"Vary를 존중하는 캐시는 축마다 엔트리를 나눈다. 단일 표현 정책은 다른 설정의 동작이다."}],"explanation":"Vary: Accept-Encoding은 '이 객체는 인코딩별로 다른 변형이며, 요청의 Accept-Encoding 값이 캐시 키에 들어간다'는 선언이다. 그래서 br 요청은 gzip 엔트리에 히트하지 못하고 미스가 나며, 오리진이 br 표현을 내려주면 그것이 별도 엔트리로 쌓인다. 히트율 관점에서 이 축은 gzip/br/identity 정도로 값이 한정되어 Vary: Cookie 같은 폭발은 없지만, 변형별로 웜업이 필요하다는 점은 그대로다. 즉석 재압축을 해 주는 CDN 기능도 있지만 그것은 캐시 동작이 아니라 별도 설정이며, 기본 가정은 '표현은 오리진이 정의한다'다.","source":"RFC 9110 §12.5.3(Accept-Encoding), RFC 9111 §4.1(Vary와 캐시 키)","sourceLinks":[{"label":"RFC 9110 §12.5.3","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.3"},{"label":"RFC 9111 §4.1","url":"https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1"}]},{"id":"cdn-h2-termination-001","topic":"cdn","difficulty":1,"question":"유저 구간을 HTTP/3로 서빙하는 CDN인데, 오리진 웹서버 접근 로그에는 요청이 HTTP/1.1로 기록된다. 가장 유력한 설명은?","options":[{"text":"CDN이 유저 연결을 엣지에서 끊고, 오리진과는 별도로 맺은 연결로 요청하기 때문이다","correct":true,"why":"CDN은 프로토콜 종료점이다. 양쪽 구간의 협상은 서로 독립적으로 일어난다."},{"text":"유저의 h3 연결을 오리진까지 이어주려 했으나 오리진이 지원하지 않아 그 구간만 폴백된 것이다","why":"오리진이 h3를 못 쓰는 건 사실일 수 있지만 연결은 애초에 분리돼 있다. 하나의 연결이 강등되는 그림이 아니다."},{"text":"협상된 프로토콜이 유저부터 오리진까지 그대로 이어지는 것이고, 로그 표기 오류일 뿐이다","why":"표기 오류가 아니다. 중간에 반드시 연결이 종료되고 다시 시작되는 구조다."},{"text":"h3가 아직 실험 단계라 중간 장비들이 자동으로 구버전으로 강등하는 것이다","why":"HTTP/3는 RFC 9114로 표준화됐다. 실험 프로토콜 시절의 감각이 남은 것이다."}],"explanation":"CDN은 유저 세션의 종료점이다. 유저가 h3로 붙어도 CDN은 오리진과 자신이 관리하는 별도 커넥션 풀(h1.1/h2)로 요청하므로, 오리진 로그에 남는 프로토콜은 그 구간의 협상 결과다. 이 구조는 결함이 아니라 CDN의 설계다 — 오리진 연결을 재사용해 핸드셰이크를 아끼고, 유저 구간은 PoP에서 끊어 혼잡 제어를 유저 가까이서 시작한다. 그래서 '앞단 프로토콜'은 유저 경험 축이고, '오리진 프로토콜'은 커넥션 관리 축으로 따로 다뤄야 한다.","source":"AWS CloudFront 개발자 안내서 — 동적 콘텐츠 서빙(엣지 연결 종료·오리진 연결 최적화), RFC 9114(HTTP/3)","sourceLinks":[{"label":"RFC 9114","url":"https://www.rfc-editor.org/rfc/rfc9114.html"}]},{"id":"cdn-request-collapse-001","topic":"cdn","difficulty":2,"question":"만료된 인기 이미지에 수천 개 요청이 같은 순간 몰렸는데, 그 시점 오리진 접근 로그에는 1건만 기록돼 있다. 이 동작을 만든 메커니즘은?","options":[{"text":"오리진 앞 방화벽이 중복 요청을 429로 차단해 1건만 통과시킨 것이다","why":"오리진에 도달하기 전에 CDN이 같은 키의 동시 미스를 하나로 모았다. 차단 주체를 오리진 쪽으로 옮겨 읽었다."},{"text":"나머지 요청도 오리진에 갔지만 로그 샘플링 때문에 1건만 보이는 것이다","why":"샘플링은 무작위라 특정 순간에 딱 1건만 남기지 않는다. 병합을 관측 왜곡으로 해석한 것이다."},{"text":"CDN이 인기 객체를 미리 예측해 선반입해 둔 덕분에 오리진이 쉴 수 있었던 것이다","why":"선반입은 이 순간의 설명이 아니다. 첫 요청만 오리진에 가고 나머지는 그 응답을 기다렸다 받는다."},{"text":"동일 키 동시 미스 병합 — 첫 요청만 오리진으로 가고 나머지는 그 응답을 기다려 받는 것이다","correct":true,"why":"request collapsing이다. 만료 순간의 동시 재검증 폭주를 오리진에 그대로 전달하지 않는 표준 방어다."}],"explanation":"캐시가 같은 키에 대한 동시 미스를 하나로 병합하는 것은 널리 쓰이는 방어다. nginx의 proxy_cache_lock처럼 잠금으로 구현되기도 하고 CDN마다 request collapsing이라 부르기도 한다. 첫 요청이 오리진에서 응답을 받아 오는 동안 나머지 요청은 대기하고, 응답이 캐시에 들어오면 그대로 히트로 서빙된다. TTL 만료 순간과 무효화 직후에 인기 객체로 요청이 몰리는 상황에서 오리진을 지키는 첫 번째 안전장치다. 다만 병합은 동시에 몰린 요청에만 작동하므로, 각기 다른 시간에 흩어져 도착하는 미스까지 막아주지는 않는다 — 그건 TTL과 계층의 몫이다.","source":"nginx 공식 문서 — proxy_cache_lock(같은 키의 동시 요청은 1건만 프록시), AWS CloudFront 공식 문서 — Origin Shield(동시 요청 병합)","sourceLinks":[{"label":"AWS CloudFront — Origin Shield","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html"}]},{"id":"cdn-shield-key-mismatch-001","topic":"cdn","difficulty":3,"question":"origin shield를 켠 지 한 달인데 오리진 요청은 엣지 미스율(8%)과 거의 같고, 실드 캐시 히트율 지표는 5%에 머물러 있다. 가장 유력한 원인은?","options":[{"text":"실드 캐시의 용량이 작아 인기 객체가 밀려나 히트율이 오르지 못하는 것이다","why":"실드가 받는 건 엣지 미스뿐이라 요청 집합이 작고 상대적으로 여유롭다. 용량이 먼저 막히는 계층이 아니다."},{"text":"엣지와 실드의 캐시 키 규칙이 달라 같은 객체가 실드에서 별도 키로 쪼개져 히트율이 깨진 것이다","correct":true,"why":"계층의 이득은 두 층의 키가 일치할 때만 성립한다. 오리진 도달률은 엣지 미스율 × (1−실드 히트율)이다."},{"text":"히트 응답까지 실드를 경유해 지표의 분모가 부풀어 있어 낮아 보이는 것이다","why":"실드로 가는 요청은 미스뿐이다. 히트는 엣지에서 끝나므로 분모가 부풀 일이 없다."},{"text":"실드 히트율은 별도 콘솔 지표로 집계되는 벤더 설정이라 현재 값은 그대로 믿으면 안 되는 것이다","why":"지표 혼합을 의심하는 그림은 다른 벤더의 콘솔 체계를 이식한 것이다. 지표는 정상이고 값이 낮은 것이 본체다."}],"explanation":"origin shield의 절감은 '오리진 도달률 = 엣지 미스율 × (1 − 실드 히트율)' 하나로 계산된다. 엣지 미스율 8%에서 실드 히트율 5%라면 절감은 거의 0이고, 이 실드 히트율이 깨지는 대표 원인이 두 계층의 캐시 키 규칙 불일치다 — 엣지가 계산한 키와 실드가 계산한 키가 어긋나면 같은 객체가 실드에서 계속 다른 엔트리로 들어가 히트가 성립하지 않는다. 실드는 스위치가 아니라 키 위생의 연장이고, 켜기만 해서 절감이 보장되지 않는 이유가 여기에 있다. 계층을 늘리기 전에 '두 층이 같은 키를 계산하는가'부터 확인해야 한다.","source":"AWS CloudFront 공식 문서 — Origin Shield(캐시 키 정규화 규칙이 계층 간 히트율을 결정)","sourceLinks":[{"label":"AWS CloudFront — Origin Shield","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html"}]},{"id":"cdn-stale-if-error-001","topic":"cdn","difficulty":2,"question":"오리진이 5xx를 반환하기 시작했는데도 CDN은 만료된 지 한참 된 콘텐츠를 200으로 계속 내려주고 있다. 작동한 메커니즘은?","options":[{"text":"재검증 실패 시 오래된 캐시를 내려주도록 설정된 serve-stale(예: stale-if-error) 동작이다","correct":true,"why":"오리진 장애를 캐시 계층에서 흡수하는 표준 수단이다. 오류보다 만료 응답이 낫다는 운영 판단의 구현이다."},{"text":"백그라운드 재검증(stale-while-revalidate)이 갱신 중이라 그동안 옛 응답을 내주는 것이다","why":"SWR의 stale 창은 재검증이 성공하는 동안 짧게 열린다. 오류를 조건으로 한 지시자와는 다른 축이다."},{"text":"오류가 나면 CDN이 갖고 있던 마지막 정상 응답으로 폴백하는 것이 별도 설정 없는 기본 동작이다","why":"오래된 것을 내려주는 건 조건부 설정이다. 기본 동작은 만료 응답을 버리고 오류를 전파하는 쪽이다."},{"text":"장애 중에는 TTL 판정이 멈춰 모든 객체가 만료 전 상태로 돌아가 있는 것이다","why":"TTL 시계는 오리진 장애와 무관하게 흐른다. 만료를 무릅쓰고 내려주도록 '설정된' 동작이다."}],"explanation":"stale-if-error(또는 벤더의 serve-stale 설정)은 재검증하러 간 요청이 5xx 등으로 실패했을 때, 만료된 캐시 응답을 지정한 시간만큼 계속 내려주는 지시자다. 오리진 장애가 곧 서비스 장애로 번지는 것을 캐시 계층에서 끊는 표준 완화 수단이고, 무효화·배포 지연 창을 흡수하는 용도로도 쓰인다. 주의할 구분은 두 가지다 — stale-while-revalidate는 재검증 '동안' 잠시 오래된 것을 주는 것이고, 기본 동작은 오류를 그대로 전파한다는 것. '만료 = 못 쓴다'가 아니라 '만료 = 설정이 허용하면 쓸 수 있다'가 이 기능의 세계관이다.","source":"RFC 5861(stale-if-error), Fastly 개발자 문서 — stale 콘텐츠 서빙(stale_if_error 설정)","sourceLinks":[{"label":"RFC 5861","url":"https://www.rfc-editor.org/rfc/rfc5861.html"}]},{"id":"cdn-invalidation-propagation-001","topic":"cdn","difficulty":2,"question":"배포 파이프라인은 '오리진 반영 → 태그 무효화 API 호출'까지를 성공 처리하고, 무효화 응답 200이 돌아오면 배포 완료로 표시한다. 이 완료 판정의 위험은?","options":[{"text":"무효화는 오리진의 원본을 고치는 호출이라 원본 갱신이 늦으면 200이어도 새 콘텐츠가 없는 것이다","why":"무효화가 지우는 건 CDN의 사본이지 원본이 아니다. 동작 방향을 오리진 쪽으로 뒤집었다."},{"text":"태그 purge는 비동기지만 경로 purge는 동기로 즉시 반영되니, 태그를 경로 무효화로 바꾸면 위험이 사라진다","why":"경로 무효화도 전파 시간이 필요하다. 동기식 무효화라는 믿음은 다른 체계에서 가져온 것이다."},{"text":"무효화는 전 PoP 전파에 시간이 걸리는 비동기 작업이라, 200 직후에는 옛 응답이 나올 수 있다","correct":true,"why":"200은 접수 확인이지 전역 완료가 아니다. 즉시성이 필요한 자산은 짧은 TTL·버전 URL로 설계해야 한다."},{"text":"무효화 호출은 실패해도 알려주지 않으므로 완료 판정에 재호출 확인 단계를 더해야 한다","why":"재시도 검토도 필요하지만 지금 관찰되는 위험은 실패가 아니라 전파 지연이다. 병목을 앞당겨 짚었다."}],"explanation":"무효화 API의 200은 '요청을 접수했다'는 응답이지 '전 세계 PoP에서 사라졌다'는 보증이 아니다. 실제 제거는 비동기로 전파되어 완료까지 시간이 걸리고, 그 창 동안 캐시 히트는 옛 응답을 계속 내준다. 그래서 실무 교리는 무효화를 비상 수단으로 쓰고 상시 정합성은 '짧은 TTL + 내용이 바뀌면 URL이 바뀌는 버전 체계'로 확보하는 것이다. 배포 완료 판정을 무효화 접수 시점에 두려면, 그 뒤에 따르는 전파 지연 창을 수용 가능한 이탈로 명시해야 한다.","source":"AWS CloudFront 공식 문서 — 파일 무효화(전파 시간·비동기 처리)","sourceLinks":[{"label":"CloudFront 파일 무효화","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html"}]},{"id":"cdn-purge-all-risk-001","topic":"cdn","difficulty":3,"question":"릴리스할 때마다 전체 캐시 무효화(/*)를 자동 호출하도록 파이프라인을 구성했다. 이 관행이 매 배포마다 만드는 결함은?","options":[{"text":"무효화 요청이 오리진에 대한 대량 조건부 요청으로 바뀌어 배포 순간 오리진 커넥션이 고갈되는 것이다","why":"무효화는 엣지의 엔트리를 무효로 표시하는 제어 동작이고 오리진으로 요청을 쏘지 않는다. 오리진이 맞는 것은 그 뒤에 들어오는 사용자 요청이다."},{"text":"웜 캐시를 통째로 버려 전 객체가 콜드에서 출발하고 인기 콘텐츠의 미스가 오리진에 동시에 몰리는 것이다","correct":true,"why":"워밍된 캐시는 서비스 자산인데 이 관행이 그걸 매번 통째로 버린다. 무효화는 변경된 대상만 지우는 도구다."},{"text":"purge가 사용자 브라우저 캐시까지 전파되어 단말의 사본이 함께 버려지고 재다운로드가 폭증하는 것이다","why":"purge가 닿는 범위는 벤더의 엣지까지다. 브라우저 사본은 자기 max-age가 끝날 때까지 그대로 남는다."},{"text":"무효화가 진행되는 동안 해당 경로의 요청이 일시적으로 5xx로 떨어지는 것이다","why":"무효화 중에도 요청은 정상 처리된다 — 미스로 돌아 오리진을 다녀올 뿐이다. 실패로 바뀌는 구간은 없다."}],"explanation":"전체 무효화는 웜 캐시 전체를 버린다. 배포 직후 모든 객체가 콜드 상태에서 출발해 인기 콘텐츠의 미스가 오리진에 동시에 몰리고, 트래픽이 클 때는 이것 자체가 장애가 된다. 무효화의 설계된 용도는 변경된 객체만 선별적으로 지우는 비상 수단이고, 상시 정합성은 짧은 TTL과 내용이 바뀌면 URL이 바뀌는 버전 체계로 확보하는 게 교리다 — CloudFront 문서도 되도록 무효화 대신 버전이 바뀐 파일명 사용을 권고한다. 무효화 자체가 오리진으로 요청을 쏘거나 브라우저 사본까지 지우거나 요청을 실패시키지는 않는다. 비용은 전부 그 뒤의 콜드 미스로 나타난다.","source":"AWS CloudFront 공식 문서 — 파일 무효화(버전이 바뀐 파일명 사용 권고)","sourceLinks":[{"label":"CloudFront 파일 무효화","url":"https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html"}]},{"id":"cdn-region-pricing-001","topic":"cdn","difficulty":2,"question":"글로벌 커뮤니티의 CDN 비용이 지난달의 1.25배다. 총 트래픽은 비슷한데 액세스 로그에 남미·아프리카 발신 비중이 눈에 띄게 늘었다. 가장 유력한 설명은?","options":[{"text":"히트율이 떨어져 오리진 fetch 트래픽이 늘어 요금이 커진 것이다","why":"미스 비용은 실재하는 축이지만 로그가 보여주는 믹스 이동과는 다른 이야기다. 지배 축을 잘못 짚었다."},{"text":"리전별 전송 단가 차이는 ±10% 수준이라 이 변화로 요금이 1.25배가 될 수 없으니 계량 오류다","why":"실제 격차는 벤더에 따라 1.3~2배다. 남미·아프리카는 요금표에서 비싼 리전에 속한다."},{"text":"CDN 요금은 전송량 단일 축으로 계산되므로 리전 구성은 청구에 영향을 주지 않는다","why":"요금표는 리전별로 나뉘어 있다. 같은 바이트라도 발신 리전이 다르면 단가가 달라진다."},{"text":"리전별 전송 단가가 다른데 남미·아프리카는 북미·유럽보다 비싸, 비중 이동이 청구를 끌어올린 것이다","correct":true,"why":"총량이 같아도 단가 높은 리전의 비중이 늘면 청구는 커진다. 로그의 믹스 변화가 바로 그 신호다."}],"explanation":"주요 CDN의 전송 단가는 리전 스키마로 쪼개져 있고, 남미·아프리카 등은 북미·유럽보다 비싸다 — CloudFront 기준 $0.110 대 $0.085로 약 1.3배이고 GCP는 격차가 더 크다. 단가 격차가 상한이므로 총량이 같을 때 청구가 그 배수를 넘을 수는 없다. 그래서 같은 트래픽 총량이어도 발신 리전의 믹스가 바뀌면 청구가 크게 달라진다 — 바이럴 확산으로 특정 지역 트래픽이 뛰는 커뮤니티 서비스에서 자주 보이는 패턴이다. 비용 추정은 '기준 리전 단가 × 총량'이 아니라 '리전별 단가 × 리전별 트래픽'으로 계산해야 하고, 다국적 서비스라면 예산을 리전 믹스 시나리오로 감싸야 한다. 요금표의 리전 구분은 벤더마다 스키마가 다르다는 점도 비교 검토 때 함께 봐야 한다.","source":"AWS CloudFront 공식 요금표(리전별 데이터 전송 단가), Google Cloud CDN 가격 문서(리전별 차등)","sourceLinks":[{"label":"Google Cloud CDN — 가격","url":"https://cloud.google.com/cdn/pricing"},{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"}]},{"id":"image-quality-scale-001","topic":"image","difficulty":2,"question":"사진 서빙을 JPEG quality 75에서 WebP quality 75로 바꿨더니 용량이 줄었다. 품질이 유지됐는지 판단하려면 무엇을 해야 하는가?","options":[{"text":"같은 75이므로 양자화율이 같아 추가 확인이 필요 없다","why":"quality는 인코더마다 내부 정책이 다른 관례 값이라 같은 숫자가 같은 품질을 보장하지 않는다."},{"text":"WebP의 quality는 1~10 척도라 75는 상한으로 잘려 무손실에 가깝다","why":"WebP 손실 품질은 0~100 척도다. 척도 자체를 틀리게 기억하고 있다."},{"text":"인코더마다 quality 척도가 달라 지각 품질 지표로 다시 비교해야 한다","correct":true,"why":"비교의 정석은 지각 품질을 고정하고 그 지점의 바이트를 재는 것이다. 숫자 동일성은 품질 동일성이 아니다."},{"text":"WebP는 JPEG보다 작게 나오므로 용량만 확인하면 된다","why":"콘텐츠와 품질점에 따라 편차가 있고 '같은 품질' 전제 없이는 비교가 성립하지 않는다."}],"explanation":"quality는 인코더가 양자화 정책을 스케일하는 관례 값이지 표준 단위가 아니다. 그래서 포맷은 물론 같은 포맷에서 인코더를 바꾸기만 해도 같은 숫자의 결과가 달라진다. 포맷 비교의 정석은 지각 품질 지표로 품질을 고정하고 그 지점의 바이트를 재는 것이다. WebP 전환 보고서가 '같은 75'에 기대고 있다면 그 비교는 처음부터 성립하지 않았다.","source":"Google cwebp 문서(-q 파라미터), IJG libjpeg 문서 — quality는 인코더 도구의 관례 파라미터","sourceLinks":[{"label":"Google — cwebp 인코더 문서","url":"https://developers.google.com/speed/webp/docs/cwebp"},{"label":"Independent JPEG Group","url":"https://www.ijg.org/"}]},{"id":"image-chroma-text-001","topic":"image","difficulty":2,"question":"흰 배경에 빨간 텍스트가 있는 사용자 스크린샷을 썸네일로 만들었더니 글자 가장자리가 번져 흐려졌다. 원인은?","options":[{"text":"리사이즈 커널의 링잉 아티팩트다. Lanczos3를 bicubic으로 바꾼다","why":"커널 아티팩트는 경계의 진동이지 색 번짐이 아니다. 축소 품질의 축과 색상의 축을 섞었다."},{"text":"사진용 기본값인 색차 서브샘플링(4:2:0)을 텍스트 이미지에 적용한 것이다","correct":true,"why":"4:2:0은 색차 해상도를 절반으로 줄여 날카로운 색 경계가 번진다. 텍스트·UI류는 4:4:4로 내보내야 한다."},{"text":"quality가 낮아서다. 값을 올리면 번짐이 사라진다","why":"품질을 올리면 완화되지만 색차를 절반으로 저장한 원인 자체는 남는다."},{"text":"리사이즈 축소에서 색 채널이 먼저 축소되어 생긴 것이다","why":"방향이 반대다. 압축을 세게 할수록 색 번짐은 심해진다."}],"explanation":"크로마 서브샘플링은 사람이 색차 해상도에 둔감한 점을 이용해 색 정보를 공간적으로 절반으로 줄이는 기법이다. 자연 사진에서는 색이 완만해 티가 나지 않지만, 빨간 텍스트처럼 색 경계가 날카로운 콘텐츠에서는 가장자리가 번진다. 그래서 정책은 콘텐츠 클래스별로 갈라진다: 사진은 4:2:0, 스크린샷·UI·다이어그램은 4:4:4 또는 무손실. quality를 올리는 것은 완화일 뿐 색차 절반이라는 원인을 고치지 않는다.","source":"JFIF 사양(서브샘플링 옵션), RFC 6386 §2(VP8은 8비트 YUV 4:2:0 전용 — 손실 WebP의 부호화)","sourceLinks":[{"label":"RFC 6386 §2","url":"https://www.rfc-editor.org/rfc/rfc6386.html#section-2"}]},{"id":"image-exif-orientation-001","topic":"image","difficulty":1,"question":"스마트폰 세로 사진의 썸네일만 옆으로 누워 보인다. 원본은 모든 뷰어에서 정상이다. 가장 먼저 확인할 것은?","options":[{"text":"EXIF orientation 태그를 픽셀에 굽는 처리가 빠졌는지","correct":true,"why":"카메라는 회전하지 않은 픽셀과 태그로 저장하고 뷰어가 태그를 읽어 돌려 보여준다. 태그를 버리고 리사이즈하면 누운 파생물이 된다."},{"text":"리샘플링 커널이 세로 이미지에 부적합한지","why":"커널은 축소·확대의 품질 문제지 회전과 무관하다."},{"text":"ICC 프로파일이 벗겨지며 회전 메타데이터까지 사라졌는지","why":"색 프로파일과 EXIF 회전 태그는 서로 다른 메타데이터다. 색의 축과 회전의 축을 섞었다."},{"text":"원본을 다시 업로드하면 뷰어가 알아서 바로잡을 것이다","why":"뷰어 보정은 원본 보기에만 작동한다. 태그 없이 나가는 파생물에서는 그대로 재발한다."}],"explanation":"스마트폰 카메라는 센서 방향 그대로 픽셀을 저장하고 촬영 방향을 EXIF orientation 태그(값 1~8)로 기록한다. 뷰어는 이 태그를 읽어 회전해 보여주므로 원본은 정상으로 보인다. 그런데 파생물을 만들 때 태그를 버리고 픽셀만 리사이즈하면 누운 썸네일이 나온다. 해결은 변환 전에 태그를 픽셀에 굽는 것(bake)이고, 원본은 태그째 보관하며 파생물에만 굽는다. '리사이즈 후 이미지가 누웠다'는 장애의 1순위 원인으로 두는 것이 맞다.","source":"EXIF 사양 CIPA DC-008(Orientation tag)","sourceLinks":[{"label":"CIPA DC-008 — Exif 사양","url":"https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E"}]},{"id":"image-icc-profile-001","topic":"image","difficulty":2,"question":"iPhone 원본(Display P3)에서 만든 파생물이 웹에서 색이 빛바래 보인다. 파이프라인은 ICC 프로파일을 제거하고 있다. 색 관리를 지원하지 않는 뷰어까지 포함해 모든 환경에서 안전해야 한다면, 올바른 수정은?","options":[{"text":"제거하지 말고 원본 P3 프로파일을 파생물에 그대로 보존한다","why":"보존은 광색역을 명시적으로 지원할 때의 유효 경로지만, 프로파일을 이해하지 못하는 클라이언트에서는 여전히 색이 틀어진다."},{"text":"지금처럼 벗기는 게 맞고 빛바램은 오래된 브라우저의 버그다","why":"변환 없이 태그만 벗기면 P3 픽셀이 sRGB로 해석돼 채도가 낮아지는 예측된 결과다. 버그가 아니다."},{"text":"P3를 sRGB로 변환한 뒤 태그를 최소화한다. 순서가 핵심이다","correct":true,"why":"변환 전에 태그를 벗기면 색 손실은 되돌릴 수 없다. 변환이 먼저, 태그 최소화가 나중이다."},{"text":"모든 원본을 16비트로 재인코딩하면 색역이 보존된다","why":"비트 깊이는 표현 단계 수지 색역 자체가 아니다. 색 공간 변환 문제를 해결하지 못한다."}],"explanation":"색 공간 태그는 픽셀 값을 어떤 절대 색으로 해석할지의 약속이다. P3 원본에서 변환 없이 태그만 벗기면 웹은 그 픽셀을 sRGB로 읽어 채도가 낮아 보인다. 웹 서빙용 파생물의 기본 정책은 sRGB로 변환한 뒤 태그를 최소화하는 것이고, 이 순서가 바뀌면 손실은 되돌릴 수 없다. 프로파일을 원본 그대로 보존하는 경로는 광색역 지원을 명시적으로 제공할 때 별도 파라미터로 연다.","source":"ICC 사양(ICC.1), IEC 61966-2-1(sRGB)","sourceLinks":[{"label":"ICC.1:2022 사양","url":"https://www.color.org/specification/ICC.1-2022-05.pdf"}]},{"id":"image-query-canonical-001","topic":"image","difficulty":2,"question":"이미지 CDN 히트율이 60%에서 40%로 떨어졌다. 로그에는 같은 이미지·같은 규격의 요청인 w=640&q=75 와 Q=75&w=640 쿼리가 함께 잡혀 있다. 대응은?","options":[{"text":"캐시 키 설정에서 파라미터 이름의 대소문자를 무시하도록 켠다","why":"로그에 보이는 차이의 절반만 닫는다. 대소문자를 맞춰도 파라미터 순서와 생략된 기본값은 여전히 별개 키로 갈린다."},{"text":"변형마다 서브도메인을 배정해 캐시를 나눠 담는다","why":"캐시를 더 잘게 나누는 방향이라 악화다. 문제의 반대 방향을 선택했다."},{"text":"CDN 설정에서 쿼리 전체를 캐시 키에서 제외한다","why":"리사이즈 파라미터가 키에서 빠지면 서로 다른 규격이 같은 응답으로 충돌한다."},{"text":"요청을 표준형 URL로 재작성하는 계층을 CDN 앞단에 둔다","correct":true,"why":"CDN은 문자열이 다르면 다른 키로 본다. 표준형을 정하고 그 변환을 한 곳에서 강제해야 분열이 닫힌다."}],"explanation":"CDN의 캐시 키는 URL 문자열에 충실하다. CloudFront는 쿼리 파라미터의 순서와 대소문자가 다르면 별도로 캐싱하고, Cloud CDN도 쿼리가 조금이라도 다르면 miss다. 그래서 정규화 — 순서 고정, 소문자 통일, 기본값 생략 — 는 URL을 생성하는 서비스의 책임이 된다. 정규 표준 URL을 한 번 만들어 캐시 키·서명·로그가 전부 그 문자열만 보게 하면 이 클래스의 히트율 누수는 사라진다.","source":"당근 테크블로그 「AWS Lambda@Edge에서 실시간 이미지 리사이즈 & WebP 형식으로 변환」(2019) — 파라미터 순서·대소문자별 캐싱 경고","sourceLinks":[{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"image-size-whitelist-001","topic":"image","difficulty":2,"question":"리사이즈 엔드포인트가 w=<아무 정수> 쿼리를 검증 없이 받고 있다. w=3, w=99999 같은 값으로 수천 종의 변형 생성 시도가 관측됐다. 올바른 대응은?","options":[{"text":"서비스가 쓰는 크기 집합만 허용하고 밖의 값은 거부(400)한다","correct":true,"why":"화이트리스트가 캐시 변형 수와 변환 CPU 양쪽의 상한을 함께 만든다. 거부가 원칙이다."},{"text":"상한(예: 2048)으로 클램프해 정상 응답한다","why":"클램프는 요청과 다른 콘텐츠를 조용히 서빙하고 변형 수도 그대로 남긴다."},{"text":"원본보다 큰 요청만 원본으로 대체하고 나머지는 수용한다","why":"업스케일 축만 막을 뿐 작은 값들이 만드는 카디널리티 폭발은 그대로다."},{"text":"CDN 앞에서 비정상 파라미터를 레이트리밋한다","why":"완화일 뿐, 허용 집합을 정의하지 않으면 변형은 계속 늘어난다."}],"explanation":"검증 없는 임의 크기 수용은 두 가지 폭발을 만든다. 캐시 변형이 무한히 늘어나 히트율이 무너지고, 임의 크기 변환의 반복 요청이 변환 CPU를 태운다. 방어선은 이산 화이트리스트다. 서비스가 실제로 쓰는 크기 집합만 허용하고 밖의 값은 400으로 거부한다. 클램프는 요청과 다른 콘텐츠를 조용히 서빙하므로 거부가 원칙이며, 프론트엔드가 요구하는 규격 집합과 서버의 화이트리스트가 일치해야 캐시도 공유된다.","source":"Cloudflare Images 공개 문서(Variants — 변형은 고정 집합으로 정의)","sourceLinks":[{"label":"Cloudflare Images 문서","url":"https://developers.cloudflare.com/images/"}]},{"id":"image-decode-budget-001","topic":"image","difficulty":2,"question":"변환 워커가 2MB짜리 PNG 업로드 하나를 처리하다 OOM으로 죽었다. 업로드에는 10MB 파일 크기 상한이 이미 걸려 있었다. 예산 변수는?","options":[{"text":"파일 크기 상한을 더 낮춰야 한다","why":"압축비가 몇 배든 디코드 결과는 같을 수 있다. 상한을 걸 축 자체가 틀렸다."},{"text":"디코드 후 픽셀 수다. 헤더 선검사로 w×h 상한을 건다","correct":true,"why":"메모리는 픽셀 수로 결정된다. 본문을 디코드하기 전에 치수만 읽어 거부한다."},{"text":"메모리 상한이 문제니 상한을 없애고 재시작으로 회복한다","why":"상한은 폭탄을 워커 안에 가두는 장치다. 없애면 워커가 아니라 파이프라인이 죽는다."},{"text":"PNG를 받지 않고 JPEG만 허용하면 된다","why":"JPEG도 픽셀 폭탄이 가능하다. 포맷 차단이 아니라 픽셀 수 상한이 방어선이다."}],"explanation":"decompression bomb의 예산 변수는 파일의 메가바이트가 아니라 디코드 후 픽셀 수다. RGBA 기준 1억 픽셀은 약 400MB라 2MB짜리 PNG 하나가 워커를 죽일 수 있다. 방어는 본문을 디코드하기 전에 헤더만 읽어 w×h 상한을 검사하는 것, 그리고 디코드를 격리된 워커에서 수행하는 것이다. 이미지 디코더는 신뢰할 수 없는 입력을 파싱하는 오래된 네이티브 코드라 공격면 취급이 원칙이다.","source":"ImageMagick policy.xml 공식 문서(width/height/area 상한), NVD CVE-2023-4863(libwebp 디코더 취약점)","sourceLinks":[{"label":"ImageMagick — 보안 정책(policy.xml)","url":"https://imagemagick.org/script/security-policy.php"},{"label":"NVD — CVE-2023-4863 (libwebp)","url":"https://nvd.nist.gov/vuln/detail/CVE-2023-4863"}]},{"id":"image-avif-cost-001","topic":"image","difficulty":3,"question":"코퍼스 벤치마크에서 AVIF가 WebP보다 평균 20% 작았고, Accept 헤더 분포로 클라이언트 지원 비율도 이미 충분함을 확인했다. on-the-fly 파이프라인의 기본 출력을 AVIF로 전환하자는 제안이다. 가장 먼저 검토할 것은?","options":[{"text":"AVIF가 애니메이션을 지원하지 않아 GIF 대체 경로가 막힌다는 우려","why":"지원은 Accept 헤더 분포로 확인하는 축이고 이미 높다. 지원 논점으로 다른 축을 덮고 있다."},{"text":"AVIF 디코딩은 가벼우니 클라이언트 비용은 안 봐도 된다는 믿음","why":"AVIF 디코드는 JPEG보다 비싸다. 저가 기기의 배터리·스크롤 성능에 영향한다."},{"text":"인코딩 CPU 비용이 miss 지연과 변환 단가에 미치는 영향","correct":true,"why":"고품질 AVIF 인코딩은 크게 느려 miss p95와 비용 구조를 바꾼다. 이중 전략 검토로 이어진다."},{"text":"전송 바이트가 줄면 총비용도 내려간다는 가정","why":"대역폭 절감과 신규 변환 CPU 비용은 다른 축이라 상쇄 계산이 필요하다."}],"explanation":"포맷 선택은 압축 효율 이전에 인코딩 CPU 비용을 어느 계층에서 누가 지불할지의 아키텍처 문제다. 고품질 AVIF 인코딩은 WebP보다 크게 느려 on-the-fly의 miss 지연과 변환 단가를 바꾼다. 그래서 실무는 인기 콘텐츠는 사전 생성, 롱테일은 WebP/JPEG로 즉시 응답하는 이중 전략이나 빠른 인코더 백엔드와의 품질 트레이드오프로 간다. 디코드 비용도 클라이언트가 내므로 저가 기기 배터리·스크롤 성능까지가 검토 범위다.","source":"libavif 공개 문서(speed 파라미터), 당근 테크블로그(2019) — 변환 비용 실측(평균 0.492초, 월 $2,400)","sourceLinks":[{"label":"libavif — AVIF 인코더/디코더","url":"https://github.com/AOMediaCodec/libavif"},{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"image-negotiation-001","topic":"image","difficulty":3,"question":"같은 URL에서 AVIF/WebP/JPEG를 클라이언트별로 주고 싶다. Vary: Accept 는 공유 캐시의 변종을 폭발시킨다. 클라이언트가 실제로 지원하는 포맷을 정확히 반영하면서 히트율까지 지키는 실무적 방식은?","options":[{"text":"Accept를 포맷 소수값으로 정규화해 URL 파라미터로 굽는다","correct":true,"why":"캐시 키의 카디널리티가 포맷 수(2~3)로 고정된다. URL 생성 시점에 정규화하는 방식이 실증돼 있다."},{"text":"Vary: Accept 를 유지한 채 CDN 캐시 TTL만 짧게 잡는다","why":"변종 수가 줄지 않는다. 신선도는 카디널리티 문제의 해법이 아니다."},{"text":"User-Agent로 판별해 클라이언트별 URL을 본문에 심는다","why":"UA 문자열은 Accept보다 카디널리티가 더 크고 불안정하다. 협상 축을 잘못 골랐다."},{"text":"협상을 포기하고 모든 클라이언트에 JPEG를 준다","why":"회피는 히트율을 지키지만 압축 효율을 버린다. 문제의 해결이 아니다."}],"explanation":"Vary: Accept의 문제는 논리가 아니라 카디널리티다. Accept 문자열은 브라우저 빌드 수준으로 다양해 같은 이미지가 수십 개 변종으로 쪼개진다. 해법은 협상 축을 정규화하는 것이다. URL을 만드는 시점에 서버가 지원 여부를 판단해 쿼리 파라미터로 굽거나, 엣지에서 Accept를 webp/avif/jpeg 버킷으로 접는다. 어느 쪽이든 캐시 키의 카디널리티가 포맷 수로 고정되고, 캐시는 불변 URL만 보게 된다.","source":"RFC 9110 §12.5.5(Vary), 당근 테크블로그(2019) — 쿼리 파라미터 정규화 방식 채택","sourceLinks":[{"label":"RFC 9110 §12.5.5","url":"https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.5"},{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"image-vmaf-still-001","topic":"image","difficulty":3,"question":"이미지 파이프라인의 회귀 게이트에 VMAF 임계값을 걸었다. 코드 리뷰에서 지적될 문제는?","options":[{"text":"VMAF는 0~10 점수라 95 같은 임계가 잘못됐다","why":"VMAF는 대략 0~100 스케일이다. 스케일 논점이지 이 리뷰의 본질이 아니다."},{"text":"VMAF는 PSNR보다 지각 상관이 낮아 게이트 지표로 부적절하다","why":"반대다. PSNR은 오차 에너지 총량만 보고 지각 상관이 가장 낮은 축이다."},{"text":"VMAF 계산이 이미지마다 수 초 걸려 게이트가 파이프라인 병목이 된다","why":"계산 비용은 실재하지만 표본 축소·캐시로 다룰 운영 문제다. 지표가 이 대상에 맞는지와는 다른 축이다."},{"text":"VMAF는 모션 특징을 학습한 비디오용 지표라 정지 이미지에 부적합하다","correct":true,"why":"이미지 회귀 게이트엔 저렴한 PSNR/SSIM 감시선과 지각 지표의 조합이 맞다."}],"explanation":"VMAF는 모션 특징을 포함해 비디오용으로 학습된 지표라 정지 이미지의 품질 임계로 쓸 수 없다. 이미지 회귀 게이트의 정석은 저렴한 PSNR/SSIM을 매 커밋의 감시선으로 걸고, 여기서 튀면 지각 지표로 정밀 분석하는 이층 구조다. 그리고 어떤 단일 지표도 '사람 눈에 같다'의 증명이 아니므로 지표는 둘 이상 쓰고 최종 판단은 사람 스팟 체크로 남긴다.","source":"Netflix VMAF 공개 문서·저장소(비디오용 지표), Wang 외(2004) SSIM 논문","sourceLinks":[{"label":"Netflix VMAF — 저장소와 문서","url":"https://github.com/Netflix/vmaf"}]},{"id":"image-onthefly-choice-001","topic":"image","difficulty":3,"question":"UGC 이미지 서비스: 하루 업로드 50만 장, 하루 이미지 요청 2억 건, 사용 규격 4종에 2종 추가 예정. 사전 생성과 on-the-fly 중 어느 쪽이 유리한가?","options":[{"text":"사전 생성 — 응답이 즉시라 규격이 늘어도 유리하다","why":"규격 추가마다 기존 전량 재생성이 필요하다. 읽기 대 쓰기 비율을 보지 않았다."},{"text":"on-the-fly — 변환 비용은 첫 요청자만 내고, 파생물을 저장할 필요가 없다","correct":true,"why":"파생물 저장이 0이고 규격 변경이 즉시 반영된다. miss는 첫 요청자만 지불한다."},{"text":"사전 생성 — miss 지연이 없으니 사용자 경험에서 앞선다","why":"첫 응답 축만 보면 맞지만 저장·재생성 비용과 규격 유연성을 버린 것이다."},{"text":"사전 생성 — 2억 건의 요청을 실시간으로 변환하면 컴퓨트가 따라가지 못하니 미리 만들어 두는 것이다","why":"변환은 첫 miss에만 일어나고 이후 캐시가 흡수한다. 전 요청이 변환기를 지난다고 상상한 것이다."}],"explanation":"두 아키텍처는 비용을 지불하는 시점이 다르다. 사전 생성은 쓰기 시점에 규격 수만큼 미리 지불하고 읽기를 싸게 만들고, on-the-fly는 첫 miss마다 지불하고 쓰기를 가볍게 만든다. 읽기가 쓰기보다 400배 많고 규격이 늘어날 예정이면 on-the-fly가 파생물 저장 0과 규격 변경 즉시 반영으로 이긴다. miss는 첫 요청자만 지불하고 이후 캐시가 흡수한다는 구조가 이 계산의 전제다.","source":"당근 테크블로그(2019) — 하루 50만 업로드 vs 2억 요청, 사전 생성 방식의 4가지 문제","sourceLinks":[{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"image-gif-loop-001","topic":"image","difficulty":2,"question":"게시판의 루프 애니메이션이 대부분 GIF라 이미지 트래픽이 크다. 알파 투명은 후보 포맷이 모두 지원한다고 전제할 때, 대안을 고르는 기준이 되는 축은?","options":[{"text":"품질 기준이다. GIF가 무손실이니 대체 후보도 무손실 포맷으로 좁힌다","why":"GIF는 256색 팔레트 제한이 있어 그라데이션에서 손실이 드러난다. 무손실 전제부터 어긋난다."},{"text":"루프는 짧든 길든 MP4로 바꾸는 것이 크기 면에서 가장 유리하다","why":"수 초를 넘는 콘텐츠엔 맞지만, 짧은 스티커성 루프는 인터프레임 이득이 작다."},{"text":"루프 길이다. 짧으면 animated WebP/AVIF, 길면 비디오다","correct":true,"why":"프레임 독립 압축과 인터프레임 압축의 이득이 루프 길이에 따라 갈린다."},{"text":"파일 크기 상한이다. 상한 이하면 GIF를 유지하고 넘으면 비디오로 보낸다","why":"용량 상한은 결과를 재는 자이지 포맷을 가르는 축이 아니다. 같은 용량에서도 짧은 루프는 이미지 계열이, 긴 루프는 비디오가 이긴다."}],"explanation":"GIF의 문제는 두 겹이다. 256색 팔레트 제한은 그라데이션에서 밴딩을 만들고, 프레임 독립 압축은 길이에 비례해 비효율이 쌓인다. 대체재 선택의 기준은 루프 길이다. 짧은 스티커성 루프는 animated WebP/AVIF가 같은 품질에 수 분의 1 크기고, 수 초를 넘는 부드러운 루프는 이전 프레임과의 차이만 부호화하는 비디오가 이긴다. '이미지 포맷'과 '비디오'의 경계를 루프 길이로 긋는 것이 설계 판단이다.","source":"GIF89a 사양(256색 팔레트), RFC 9649(WebP 애니메이션)","sourceLinks":[{"label":"RFC 9649","url":"https://www.rfc-editor.org/rfc/rfc9649.html"},{"label":"GIF89a 사양(W3C 보관본)","url":"https://www.w3.org/Graphics/GIF/spec-gif89a.txt"}]},{"id":"image-sign-recheck-001","topic":"image","difficulty":3,"question":"CDN 서명 URL로 비공개 이미지를 서빙한다. 서명 파라미터를 아예 지운 요청이 오리진까지 도달해 이미지가 응답됐고, 그 응답이 CDN에 캐시까지 됐다. CDN 계층의 요청 필터는 이미 캐시된 응답에는 적용되지 않는다. 구조적 해법은?","options":[{"text":"오리진 앱이 원본 URL(서명 포함)을 별도 헤더로 받아 재검증한다","correct":true,"why":"CDN 서명은 캐시 보호, 앱 재검증은 권한 보호다. 두 계층이 각자 책임을 진다."},{"text":"CDN에 서명 없는 요청을 차단하는 규칙을 추가한다","why":"완화는 되지만 우회 가능한 필터다. 계층 분리라는 구조적 닫힘이 없다."},{"text":"CDN이 검증을 이미 했으니 오리진은 응답 속도만 보면 된다","why":"반대다. CDN을 통과했다고 오리진 권한까지 증명되지 않는다."},{"text":"서명 만료 시간을 짧게 줄여 노출 창을 최소화한다","why":"만료는 통과된 요청의 수명만 줄인다. 우회 자체를 막지 못한다."}],"explanation":"Cloud CDN은 서명 관련 쿼리 파라미터가 없으면 '서명된 URL이 아니네' 하고 검증 없이 통과시키며, 오리진으로 보낼 때는 서명 파라미터를 떼 버린다. 그래서 오리진 입장에서 정상 서명 요청과 서명을 지운 요청이 똑같이 보인다. 해법은 CDN이 원본 URL을 별도 헤더로 전달하게 하고 오리진 앱이 시그니처를 재검증하는 것이다. 'CDN 서명 = 캐시 보호, 애플리케이션 재검증 = 권한 보호'라는 계층 분리로 일반화된다.","source":"당근 「대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage」(2025 GCP 밋업 공개 발표) — X-Client-Request-URL 기반 시그니처 재검증","sourceLinks":[{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"},{"label":"Google Cloud Run 문서","url":"https://cloud.google.com/run/docs"}]},{"id":"image-webp-basis-001","topic":"image","difficulty":1,"question":"WebP 전환 검토 회의에서 '레거시 브라우저 때문에 아직 이르다'는 반대 의견이 나왔다. 판단의 근거가 되는 것은?","options":[{"text":"WebP의 주류 브라우저 기본 지원 여부","why":"시점 의존 사실이라 기억이나 낡은 인용으로는 판단할 수 없다. 근거축이 아니다."},{"text":"포털·경쟁사가 WebP를 쓰고 있다는 사실","why":"남의 트래픽 분포는 내 사용자 분포와 다르다. 근거가 되지 못한다."},{"text":"폴백 JPEG를 함께 둔다면 즉시 전환해도 무방하다는 점","why":"폴백은 전환의 안전장치일 뿐 실제 지원 비율을 대신하지 않는다."},{"text":"자사 트래픽의 Accept 헤더에서 webp 지원 비율 실측","correct":true,"why":"사용자 분포가 유일한 판단 근거다. 실측 위에서 전환 범위를 정한다."}],"explanation":"브라우저 지원률은 시점 의존 사실이라 기억이나 낡은 인용으로 판단할 수 없다. 근거는 자사 트래픽의 Accept 헤더 분포다. 당근도 2019년에 클라이언트의 80% 이상이 WebP를 지원하는 Android이라는 자체 분포 실측을 근거로 전환을 결정했다. 벤치마크로 효과를 확인하고, 자사 분포를 재고, 단계적으로 롤아웃하는 것이 정석 순서다.","source":"당근 테크블로그(2019) — 클라이언트 80% 이상이 WebP 지원 Android이라는 자체 분포 기반 결정","sourceLinks":[{"label":"당근 테크블로그 — Lambda@Edge 실시간 이미지 리사이즈 & WebP 변환","url":"https://medium.com/daangn/lambda-edge%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-on-the-fly-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A7%95-f4e5052d49f3"}]},{"id":"image-edge-origin-001","topic":"image","difficulty":2,"question":"엣지 함수로 on-the-fly 리사이즈를 하고 있다. 요구 사항이 '서명 재검증·권한 확인 같은 비즈니스 로직을 처리하며 서빙'하는 것으로 바뀌었다. 구조적 판단은?","options":[{"text":"엣지가 사용자에게 가장 가까우니 로직도 엣지에 넣는다","why":"지연 축만 보고 엣지 런타임의 제약(본문 크기·언어·API)을 보지 않았다."},{"text":"변환을 오리진 앱 런타임으로 옮기고 엣지는 캐시로 남긴다","correct":true,"why":"풀 런타임에서 검증·비즈니스 로직·인코더 선택이 자유롭다. 캐시는 여전히 엣지가 담당한다."},{"text":"검증 코드를 엣지와 오리진에 복제해 이중으로 돌린다","why":"같은 코드가 두 계층에서 각자 진화해 어긋난다. 단일 소유자가 아니다."},{"text":"이미지 서빙과 비즈니스 로직은 분리하니 현행을 유지한다","why":"서명 재검증은 서빙 경로 자체의 요구다. 분리로 안 되는 것을 분리로 치웠다."}],"explanation":"엣지 변환은 POP에서 변환해 오리진 왕복이 없지만 런타임 제약을 안는다. 본문 크기 상한, 단일 언어, 제한된 API가 그것이다. 요구가 서명 재검증·권한 확인 같은 비즈니스 로직을 포함하는 순간 풀 런타임이 필요해지고, 변환을 오리진 앱으로 옮기는 것이 구조적 답이다. 엣지는 캐시 계층으로 남는다. 실제로 이 요구 사항이 이미지 서비스를 엣지 함수에서 오리진 앱으로 옮긴 사례의 핵심이었다.","source":"당근 「대규모 이미지 서비스 with Cloud CDN, Cloud Run, Cloud Storage」(2025 GCP 밋업 공개 발표) — Cloud Run 오리진에서 비즈니스 로직·서명 재검증","sourceLinks":[{"label":"Google Cloud CDN — 캐싱 개요","url":"https://cloud.google.com/cdn/docs/caching"},{"label":"Google Cloud Run 문서","url":"https://cloud.google.com/run/docs"}]},{"id":"image-fixed-quality-001","topic":"image","difficulty":2,"question":"모든 이미지를 quality 75 한 값으로 인코딩하는 정책이다. 평균 바이트는 목표치를 유지하는데 CS 문의가 늘고 있다. 이 정책의 실질적 문제는?","options":[{"text":"인코더마다 quality 75의 척도가 달라 교체할 때 평균 바이트가 흔들린다","why":"척도가 인코더마다 다른 것은 사실이지만 지금 문제는 인코더를 바꾼 적이 없는데 생겼다. 고정값 정책의 결함인 클래스별 편차는 그대로 남는다."},{"text":"고정 quality는 알파 채널이 있는 이미지에서만 링잉 아티팩트를 남긴다","why":"링잉은 알파와 무관하게 고주파 경계에서 생긴다. 결함을 특정 채널 구성 탓으로 좁히면 텍스트·일러스트 전반의 과압축을 놓친다."},{"text":"quality 75는 대다수 콘텐츠에 이미 과잉이라 바이트만 낭비된다","why":"방향을 뒤집었다. 낭비도 함께 일어나지만 CS 문의를 만드는 쪽은 반대편 — 텍스트·평면 일러스트에서의 과압축이다."},{"text":"클래스별 결과 편차가 커서 한쪽은 과압축, 반대쪽은 과잉 품질이 된다","correct":true,"why":"콘텐츠 클래스별 품질 계단을 정하거나 목표 지표 방식이 해법의 방향이다."}],"explanation":"고정 quality는 품질 정책을 포기한 것이다. 노이즈 사진은 같은 값에서도 바이트가 크고 디테일이 먼저 무너지며, 플랫 그래픽은 과잉 품질로 낭비가 된다. 규모가 커지면 콘텐츠 클래스별로 오프라인 실험으로 품질 계단을 정하는 캘리브레이션, 또는 지각 지표를 목표로 이미지별로 탐색하는 방식으로 간다. 평균 바이트가 목표치 안이어도 분포의 최악이 무너지면 그게 사용자 항의로 나타난다.","source":"Google Research Guetzli 발표(2017) — 지각 지표를 목표로 인코딩 파라미터를 탐색하는 접근","sourceLinks":[{"label":"Google Research — Guetzli JPEG 인코더","url":"https://github.com/google/guetzli"}]},{"id":"image-progressive-interlace-001","topic":"image","difficulty":2,"question":"이미지 목록 화면의 로딩 체감이 나쁘다는 CS가 있다. 서빙되는 이미지는 JPEG 사진과 PNG 다이어그램이 섞여 있다. '인터레이스를 켜면 위에서부터 조금씩 뜨니 체감이 좋아진다'는 팀원의 제안에 대한 올바른 판단은?","options":[{"text":"둘 다 인터레이스 옵션이 있으니 전부 켜는 것이 무조건 이득이다","why":"PNG의 인터레이스(Adam7)는 필터 예측 효율을 해쳐 파일이 오히려 커지는 경우가 많다. '무조건 이득'인 기능이 아니다."},{"text":"progressive JPEG는 느린 모뎀 시대의 기술이라 지금 광대역 환경에서는 효과가 사라졌다","why":"완전한 그림이 먼저 보이는 시점이 앞당겨지는 효과는 대역폭과 무관하게 유효하다."},{"text":"progressive 인코딩은 평균 15% 작아지니 체감보다 용량 이득이 주효과다","why":"progressive는 바이트를 크게 줄이지도 늘리지도 않는다. 15% 절감은 지어낸 수치고 주효과는 체감이다."},{"text":"JPEG는 progressive로 켜고 PNG 인터레이스는 끈다 — PNG의 용량은 포맷 교체로 푼다","correct":true,"why":"progressive JPEG는 바이트 손해가 거의 없지만 PNG의 Adam7은 크기 손해가 커서 권장되지 않는다."}],"explanation":"progressive JPEG는 스캔을 여러 번 나눠 담아 저해상도 전체가 먼저 뜨게 한다. 바이트 관점에서 대체로 중립적이라 '공짜 체감 개선'으로 불린다. PNG의 인터레이스(Adam7)는 이름이 같은 다른 트레이드오프다 — 필터가 예측에 쓰는 이전 패스 데이터가 멀어져 압축 효율이 나빠지고 크기가 오히려 커지는 경우가 많아 권장되지 않는다. PNG를 작게 만드는 길은 인터레이스가 아니라 무손실 WebP/AVIF 같은 포맷 교체다. 같은 단어 '인터레이스'가 포맷마다 다른 비용 구조를 감춘다.","source":"ITU-T T.81(JPEG — progressive DCT 모드), ISO/IEC 15948(PNG — Adam7 인터레이스)","sourceLinks":[{"label":"ITU-T T.81 (JPEG)","url":"https://www.itu.int/rec/T-REC-T.81"},{"label":"PNG 사양 (W3C — ISO/IEC 15948과 동일 내용)","url":"https://www.w3.org/TR/png-3/"}]},{"id":"image-upscale-request-001","topic":"image","difficulty":2,"question":"원본이 1200px인 아바타에 대해 클라이언트 팀이 w=4096 변형을 요청하고 있다. 변환 API의 올바른 대응은?","options":[{"text":"4096 변형을 만들지 않고 원본 소스를 응답한다 — 업스케일은 품질 이득 없이 비용만 만든다","correct":true,"why":"업스케일은 새 디테일을 만들지 못한다. '그 이상 없음'을 원본 응답으로 표현하는 것이 이미지 API의 정석이다."},{"text":"요청값을 그대로 만들어 준다 — 고해상도 디스플레이 사용자를 위한 친절함이다","why":"'요청한 값을 준다'가 서비스 품질처럼 읽히지만 실제로는 뭉개진 픽셀과 변환 비용·캐시 변형만 늘어난다."},{"text":"원본보다 큰 요청은 아예 404로 거부해 버린다","why":"거부가 아니라 '그 크기의 의미가 없음'을 원본 응답으로 처리하는 것이 관례다. 불필요한 오류 경로를 새로 만드는 것도 부담이다."},{"text":"2배까지의 업스케일은 화질 손실이 체감되지 않으니 2400px까지만 만들어 준다","why":"손실이 체감되지 않는 안전 상한 같은 값은 존재하지 않는다. 업스케일의 품질 이득은 배율과 무관하게 0이다."}],"explanation":"업스케일은 픽셀을 보간해 늘릴 뿐 새 정보를 만들지 않는다. 그래서 원본보다 큰 요청에는 원본을 그대로 주는 것이 관례이고, sharp의 withoutEnlargement 같은 스위치가 표준 옵션으로 존재한다. 이와 별개로 허용 크기 집합은 이산 화이트리스트로 제한해야 캐시 변형 수와 변환 CPU의 상한이 같이 생긴다. '고해상도 요청 = 고해상도 대응'이라는 직관은 뭉개진 화질과 변형 폭발만 남긴다.","source":"sharp 공개 문서(resize — withoutEnlargement 옵션)","sourceLinks":[{"label":"sharp — resize","url":"https://sharp.pixelplumbing.com/api-resize"}]},{"id":"image-downscale-moire-001","topic":"image","difficulty":2,"question":"체크무늬 셔츠 상품의 썸네일에 원본 사진에는 없는 지그재그 줄무늬가 나타난다. 가장 유력한 원인은?","options":[{"text":"JPEG 압축 아티팩트다 — 품질값을 올리면 사라진다","why":"압축 아티팩트는 8×8 블록 얼룩 형태지 규칙적인 줄무늬가 아니다. 압축의 축과 리샘플링의 축을 섞었다."},{"text":"축소할 때 고주파를 미리 제거하지 않은 리샘플링 — aliasing이다","correct":true,"why":"축소는 샘플링 레이트를 낮추는 연산이라 사전 필터 없이 픽셀을 건너뛰면 남은 고주파가 낮은 주파수처럼 접혀 보인다."},{"text":"촬영 원본 해상도가 낮아서 생긴다 — 더 큰 원본을 받아 다시 만들면 된다","why":"방향이 반대다. 모아레는 고주파 정보가 있는 원본을 제대로 필터링하지 않고 축소할 때 생긴다."},{"text":"썸네일 포맷 문제다 — WebP로 내보내면 이런 무늬가 없어진다","why":"모아레는 인코딩 이전의 리샘플링 단계 결함이다. 같은 축소기라면 포맷을 바꿔도 그대로 재현된다."}],"explanation":"썸네일의 지그재그 무늬 정체는 대부분 aliasing이다. 축소는 샘플링 레이트를 낮추는 연산인데, 낮추기 전에 표현 못 할 고주파를 걸러내지 않으면 그 에너지가 낮은 주파수 대역으로 접히면서 줄무늬로 보인다. 체크무늬·벽돌 담·섬세한 격자가 단골 피해자고 '원본이 원래 그런가 보다'로 넘어가는 경우가 많다. 제대로 된 축소는 적분 기반·사전 필터 방식이며 사진 축소의 관례 커널은 Lanczos3다. 커널 선택은 취향이 아니라 품질 정책이다.","source":"sharp 공개 문서(resize — kernel, 이미지 축소용 기본 lanczos3)","sourceLinks":[{"label":"sharp — resize","url":"https://sharp.pixelplumbing.com/api-resize"}]},{"id":"image-placeholder-cls-001","topic":"image","difficulty":2,"question":"피드 이미지에 블러 자리표시자(blurhash)를 적용했다. 로딩 체감은 좋아졌다는 평가인데 레이아웃 점프(CLS) 지표는 그대로다. 이유는?","options":[{"text":"자리표시자의 블러 강도가 약해서다 — 강도를 올리면 점프도 잡힌다","why":"블러는 '무언가 있다'는 시각 피드백일 뿐 공간 예약과는 다른 축이다. 강도는 점프에 영향하지 않는다."},{"text":"자리표시자가 본체보다 99% 작아 그만큼 로딩이 앞당겨지는데 측정 오차로 안 보이는 것이다","why":"자리표시자는 본체 로딩을 빠르게 하지 않는다. 속도를 만드는 계층이 아니라 체감을 채우는 계층이다."},{"text":"점프는 웹폰트 스왑이 만드는 것이므로 font-display 설정만 고치면 된다","why":"폰트 스왑도 CLS 원인이지만 이 화면의 점프는 이미지 영역에서 일어나고 있다. 축을 옮긴 진단이다."},{"text":"자리표시자는 공간을 예약하지 않는다 — 점프는 종횡비 예약(width/height)으로 잡는다","correct":true,"why":"CLS의 근본 해결은 컨테이너가 종횡비를 미리 갖는 것이고, 자리표시자는 그 위에 얹히는 체감 계층이다."}],"explanation":"CLS는 늦게 도착한 콘텐츠가 이미 그린 레이아웃을 밀어내는 현상이라, 해결은 이미지 도착 전에 공간을 점유시키는 것이다. width/height 속성이나 aspect-ratio CSS로 컨테이너에 종횡비를 예약하면 점프가 사라진다. LQIP·BlurHash·dominant color는 그 위에 시각적 연속성을 얹는 계층이지 공간을 만들지도 속도를 만들지도 않는다. 순서가 지켜지면 블러가 뜨는 동안에도 레이아웃은 이미 확정되어 있다.","source":"web.dev «Optimize Cumulative Layout Shift»(이미지 치수 예약), BlurHash 공식 문서","sourceLinks":[{"label":"web.dev — CLS 최적화","url":"https://web.dev/articles/optimize-cls"},{"label":"BlurHash","url":"https://github.com/woltapp/blurhash"}]},{"id":"image-original-metadata-001","topic":"image","difficulty":2,"question":"저장 비용 절감 회의에서 '필요한 파생물을 모두 만들었으니 원본은 지우고, 파생물은 지금처럼 메타데이터 포함해 그대로 서빙하자'는 제안이 나왔다. 올바른 판단은?","options":[{"text":"방향을 뒤집는다 — 원본은 무손실로 보존하고 파생물에서 EXIF를 스트립해 서빙한다","correct":true,"why":"원본은 다음 규격·정책 변경 때 재변환의 유일한 원천이고 파생물의 메타데이터는 개인정보 유출면이다. 제안과 정확히 반대로 가야 한다."},{"text":"제안대로 원본을 지우되, 파생물 응답에서 GPS 좌표만 골라 지운다","why":"GPS만 지우는 선별 필터는 촬영시각·기기식별 같은 다른 민감 항목과 원본 영구 손실을 그대로 남긴다."},{"text":"원본을 오브젝트 스토리지에 올려 두면 업로드 시점에 EXIF가 자동으로 제거되니 걱정 없다","why":"S3 같은 오브젝트 스토리지는 바이트를 그대로 저장한다. 업로드 시 메타데이터를 고쳐 주는 기능이 없다."},{"text":"EXIF는 이미지당 수십 바이트 수준이라 용량·개인정보 어느 축에서도 의미 있는 문제가 아니다","why":"GPS 좌표와 임베디드 썸네일을 포함한 EXIF는 KB급이 될 수 있고, 크기와 무관하게 위치 정보 노출은 그 자체로 사고다."}],"explanation":"원본 보존과 파생물 스트리핑은 한 쌍의 정책이다. 원본은 다음 규격·포맷·정책 변경 때 재변환의 유일한 원천이라 메타데이터 포함 무손실로 남기고, 서빙되는 파생물에서는 EXIF를 스트립한다 — 용량보다 GPS 좌표 등 개인정보 노출 방지가 본체다. 오브젝트 스토리지는 업로드된 바이트를 수정하지 않으므로 '올려 두면 정리된다'는 믿음은 성립하지 않는다. 방향이 반대인 이 제안은 저장 비용은 아끼고 재변환 자산과 사용자 신뢰를 태운다.","source":"CIPA DC-008(EXIF 사양 — GPS IFD), Amazon S3 개발자 안내(객체는 업로드된 그대로 저장)","sourceLinks":[{"label":"CIPA DC-008 — Exif 사양","url":"https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E"}]},{"id":"image-webp-subsampling-001","topic":"image","difficulty":3,"question":"흰 배경 텍스트 스크린샷의 가장자리 번짐 CS가 이어지자 회의에서 'JPEG의 색 압축 때문이니 이미지를 전부 손실 WebP로 전환하자'는 제안이 나왔다. 이 제안을 평가하면?","options":[{"text":"전환에 찬성한다 — 전환하면서 quality를 90으로 올리면 색 경계도 선명해진다","why":"quality 상향은 완화일 뿐 색차 해상도를 절반으로 저장하는 구조 자체는 그대로 남는다."},{"text":"무손실 WebP로 전환하면 번짐도 사라지고 사진보다 바이트도 줄어든다","why":"무손실 WebP는 사진 계열에서 손실 JPEG보다 크기가 크다. 텍스트에 맞는 정책과 사진의 바이트 비교를 한 문장에 섞었다."},{"text":"손실 WebP도 색차 4:2:0으로 부호화한다 — 번짐은 그대로, 텍스트류는 4:4:4·무손실로 갈라야 한다","correct":true,"why":"크로마 서브샘플링은 특정 인코더 옵션이 아니라 손실 부호화의 일반 구조다. 해결 축은 포맷이 아니라 콘텐츠 클래스별 색차 정책이다."},{"text":"4:2:0은 JPEG 고유의 옵션이라 포맷을 바꾸는 것만으로 이 문제류는 전부 사라진다","why":"서브샘플링은 색 공간 압축의 일반이다. 손실 WebP도 같은 축을 쓴다는 사실을 놓친 것이다."}],"explanation":"크로마 서브샘플링은 JPEG의 전유물이 아니라 색 공간 압축의 일반이다. 손실 WebP도 Y'CbCr 4:2:0으로 부호화하므로 '포맷을 바꾸면 색 축이 사라진다'는 전제부터 성립하지 않는다. 번짐을 잡는 정책 축은 콘텐츠 클래스별 색차 결정이다 — 사진은 4:2:0, 텍스트·UI·다이어그램은 4:4:4 또는 무손실. 포맷 전환은 같은 지각 품질에서의 바이트 비교로 별도로 평가할 문제지 이 번짐의 해법이 아니다.","source":"RFC 6386 §2(VP8은 8비트 YUV 4:2:0 전용 — 손실 WebP의 부호화)","sourceLinks":[{"label":"RFC 6386 §2","url":"https://www.rfc-editor.org/rfc/rfc6386.html#section-2"}]},{"id":"image-sizes-attr-001","topic":"image","difficulty":2,"question":"제품 목록 카드에 srcset으로 480/800/1600px 소스를 두고 sizes도 함께 넣었다. 375px 뷰포트에서 개발자 도구로 확인하니 1600px 소스를 내려받고 있다. 가장 유력한 원인은?","options":[{"text":"srcset의 w 디스크립터는 서버로 전달되지 않아 CDN이 임의로 가장 큰 변형을 골라 응답한 것이다","why":"소스 선택은 전적으로 브라우저가 하고 CDN은 브라우저가 요청한 URL만 응답한다. 선택 주체를 뒤집었다."},{"text":"sizes가 실제 렌더 폭보다 크게 선언돼 있다(예: 100vw 고정) — 브라우저는 이 값으로 소스를 계산한다","correct":true,"why":"sizes는 힌트가 아니라 선택 계산의 입력이다. 크게 쓰면 그만큼 큰 소스를 내려받는다."},{"text":"브라우저는 화면에 선명하게 보이려고 DPR 1에서도 항상 가장 큰 소스를 고른다","why":"브라우저는 sizes×DPR로 계산한 값에 맞는 소스를 고른다. '항상 최대'는 선택 규칙에 없다."},{"text":"sizes가 없어도 브라우저가 DOM에서 렌더 크기를 읽어 알아서 고른다 — 원인은 다른 곳에 있다","why":"sizes가 없으면 뷰포트 폭을 가정한다. 렌더된 DOM 크기를 다시 보고 소스를 갈아타지는 않는다."}],"explanation":"srcset과 w 디스크립터 조합에서 브라우저는 'sizes에 적힌 렌더 폭 × 기기 픽셀 밀도'로 필요한 픽셀 수를 계산하고 그에 맞는 소스를 고른다. 이 계산은 문서 파싱 시점에 sizes 선언으로 끝나며 렌더 후 실제 DOM 크기와 무관하다. 그래서 카드 이미지인데 100vw로 두는 식의 sizes 부정확은 좁은 화면에서도 큰 소스를 내려받는 과다 전송으로 나타난다. sizes는 참고 정보가 아니라 브라우저가 사용하는 계산 입력이다.","source":"HTML 표준(img 요소 — sizes 속성), MDN «Responsive images»","sourceLinks":[{"label":"HTML 표준 — sizes 속성","url":"https://html.spec.whatwg.org/multipage/images.html#sizes-attributes"},{"label":"MDN — 반응형 이미지","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Responsive_images"}]},{"id":"image-art-direction-001","topic":"image","difficulty":1,"question":"메인 배너를 모바일에서는 인물 클로즈업, 데스크톱에서는 전체 구도로 보여주고 싶다. 적절한 구현은?","options":[{"text":"두 구도를 srcset의 w 디스크립터로 등록한다 — 브라우저가 화면 크기에 맞게 골라 간다","why":"srcset은 같은 내용의 다른 해상도를 고르는 축이다. 구도가 다른 이미지를 섞으면 해상도 변형으로 오해해 예측과 다르게 고른다."},{"text":"picture 요소는 아직 브라우저 지원이 성숙하지 않아 JS 폴백 라이브러리를 함께 써야 한다","why":"picture는 주류 브라우저 전면 지원이 오래 끝난 표준 요소다. 시점 의존 사실을 현재 근거로 쓰고 있다."},{"text":"데스크톱 구도 하나를 내려받게 하고 모바일에서는 CSS object-position으로 초점을 옮겨 보여준다","why":"화면에서는 그렇게 보여도 모바일이 데스크톱 크기 이미지를 통째로 내려받는다. 크롭 흉내가 아니라 전송 낭비다."},{"text":"picture 요소의 source에 media 조건으로 구도별 소스를 지정한다","correct":true,"why":"내용이 다른 이미지를 뷰포트별로 갈라 내는 것이 picture+source media의 용도다."}],"explanation":"반응형 이미지에는 두 가지 다른 문제가 있다. 같은 내용의 해상도 분배(srcset)와 내용 자체가 다른 구도 분배다. 후자는 어느 쪽이 좋은지 브라우저가 판단할 수 없으므로 마크업이 명시적으로 조건을 갈라야 하고, 그 용도가 picture 요소의 source media다. 크롭을 CSS로 흉내 내면 모바일에 불필요하게 큰 소스를 전송하게 되어 반응형의 본래 목적과 어긋난다. picture는 주류 전면 지원이 오래 된 표준 요소라 별도 폴백은 필요 없다.","source":"HTML 표준(picture 요소), MDN «Using the picture element»(아트 디렉션)","sourceLinks":[{"label":"HTML 표준 — picture 요소","url":"https://html.spec.whatwg.org/multipage/embedded-content.html#the-picture-element"},{"label":"MDN — picture","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/picture"}]},{"id":"image-svg-upload-001","topic":"image","difficulty":3,"question":"사용자 아바타 업로드에 SVG 형식 허용을 검토 중이다. 서빙 파이프라인의 필수 조치는?","options":[{"text":"업로드 시점에 새니타이즈·재인코딩하고, 서빙 도메인을 본 서비스와 분리한다","correct":true,"why":"SVG는 문서 맥락에서 열리면 스크립트가 실행될 수 있는 콘텐츠다. 저장 전 제거와 실행 맥락 격리가 함께 가야 한다."},{"text":"화면에서는 img 태그로만 삽입하면 스크립트가 실행되지 않으니, 업로드 검증 없이 허용해도 된다","why":"img 맥락의 미실행은 사실이지만 URL 직접 접근 같은 문서 맥락이 남아 있다. 한 맥락의 안전성으로 전체를 닫았다."},{"text":"SVG는 실행 코드를 담을 수 없는 벡터 포맷이라 이미지 확장자 화이트리스트에 넣는 것으로 충분하다","why":"SVG는 XML 문서라 script·이벤트 핸들러·외부 참조를 포함할 수 있다. '실행 불가'는 방향이 반대다."},{"text":"응답에서 script 태그 문자열만 걸러내는 필터를 붙인다","why":"이벤트 핸들러·foreignObject·외부 리소스 참조 같은 우회 경로가 많다. 태그 이름 블랙리스트는 소독이 아니다."}],"explanation":"SVG는 이미지이면서 동시에 XML 문서다. img로 로드되면 스크립트가 실행되지 않지만 그 URL을 직접 열면 문서로 취급되어 포함된 스크립트가 실행될 수 있다. 사용자 업로드를 받는 순간 이는 '이미지 확장자 하나 늘리기'가 아니라 신뢰할 수 없는 문서 입력을 받는 문제가 된다. 대응은 이중이다 — 업로드 경로에서 허용 요소만 남기는 새니타이즈·재인코딩, 그리고 실행 맥락을 끊는 서빙 도메인 분리. 'img로만 쓰면 안전하다'는 통제는 한 렌더링 지점에만 성립한다.","source":"MDN «SVG as an image»(이미지 맥락의 제한과 문서 맥락의 차이 — 직접 열면 스크립트 실행)","sourceLinks":[{"label":"MDN — 이미지로서의 SVG","url":"https://developer.mozilla.org/en-US/docs/Web/SVG/Guides/SVG_as_an_image"}]},{"id":"image-gif-autoplay-001","topic":"image","difficulty":1,"question":"마케팅 페이지의 3초 루프 배너가 GIF 4MB라 모바일 트래픽 상위 1위다. 비디오 교체 제안에 '모바일 브라우저에서는 재생이 안 될 것'이라는 반대가 나왔다. 이 반대를 평가하면?","options":[{"text":"반대가 맞다 — iOS Safari는 playsinline 옵션조차 무시하므로 iOS에서는 GIF가 유일한 선택이다","why":"iOS Safari는 음소거·인라인 비디오의 제스처 없는 재생을 허용한다. iOS 정책을 반대 방향으로 기억하고 있다."},{"text":"음소거에 인라인 속성을 붙인 비디오는 모바일 포함 제스처 없이 재생된다 — 차단 대상은 소리 있는 재생이다","correct":true,"why":"autoplay 제한의 본체는 소리다. 무음 루프 배너는 정확히 허용되는 범주에 있다."},{"text":"autoplay 정책은 모든 모바일 브라우저에서 사용자 제스처 이후에만 풀린다 — 반대가 일반적으로 옳다","why":"제스처 요건은 소리 있는 재생에 붙는 조건이다. 음소거 재생을 함께 묶은 과잉 일반화다."},{"text":"GIF는 이미지라 브라우저 캐시에 남지만 비디오는 캐시되지 않아 반복 노출에는 GIF가 이득이다","why":"비디오도 HTTP 캐시 대상이다. 미디어 종류와 캐시 가능성은 연결되어 있지 않다."}],"explanation":"모바일 autoplay 정책의 설계는 '소리 없는 재생은 허용한다'는 방향이다. 음소거(muted)에 playsinline을 붙인 비디오는 iOS Safari를 포함한 주류 모바일 브라우저에서 제스처 없이 재생된다. iOS가 인라인 재생을 제한하던 시절의 기억이 '모바일은 GIF여야 한다'는 믿음으로 남아 있는 것이다. 소리 없는 루프 콘텐츠라면 GIF 대신 비디오로 내는 것이 프레임 압축 구조상 수십 퍼센트 단위의 절감으로 이어진다.","source":"WebKit 공식 블로그 «New