쿠키와 웹 캐시는 모두 브라우저에 데이터가 남을 수 있지만 목적이 다르다. 쿠키는 서버가 이후 요청에서 돌려받을 상태를 저장하고, HTTP 캐시는 이전 응답을 재사용해 지연과 네트워크 사용량을 줄인다. 둘을 같은 저장소처럼 다루면 인증 정보가 불필요하게 전송되거나 개인화 응답이 공유 캐시에 노출될 수 있다.
쿠키와 캐시는 요청에서 어떻게 다를까
| 구분 | 쿠키 | HTTP 캐시 |
|---|---|---|
| 주된 목적 | 세션 식별자, 사용자 설정 등 상태 전달 | 이전 응답 표현의 재사용 |
| 서버가 설정하는 대표 헤더 | Set-Cookie |
Cache-Control, ETag, Last-Modified |
| 이후 요청 | 조건에 맞는 쿠키를 Cookie 헤더로 보냄 |
신선하면 서버 요청 없이 응답하거나, validator로 재검증 |
| 주요 위험 | 탈취, CSRF, 추적, 과도한 요청 크기 | 오래된 데이터, 개인화 응답의 공유, 잘못된 cache key |
HTTP 자체는 요청을 독립적으로 해석할 수 있는 stateless 프로토콜이다. 서버는 응답의 Set-Cookie로 브라우저에 이름과 값, 적용 범위를 전달하고, 브라우저는 조건이 맞는 다음 요청의 Cookie 헤더로 값을 돌려보낸다.
HTTP/1.1 200 OK
Set-Cookie: session=opaque-id; Path=/; Secure; HttpOnly; SameSite=Lax
GET /account HTTP/1.1
Host: example.com
Cookie: session=opaque-id
쿠키에는 비밀번호나 민감한 원문 데이터를 넣기보다 서버 측 상태를 가리키는 추측하기 어려운 식별자를 두는 편이 안전하다. 쿠키 값은 자동으로 암호화되는 것이 아니며, 브라우저와 중간 시스템의 크기 제한도 고려해야 한다.
쿠키 보안 속성은 각각 막는 위협이 다르다
Secure: HTTPS 연결에서만 쿠키를 전송하도록 제한한다.HttpOnly: 브라우저 스크립트 API에서 쿠키를 읽지 못하게 해 탈취 경로를 줄인다.SameSite: cross-site 요청에서 쿠키를 보낼 조건을Strict,Lax,None으로 제어한다.Domain: 쿠키를 보낼 호스트 범위를 넓힐 수 있다. 생략하면 설정한 호스트에 한정되는 host-only cookie가 된다.Path: 쿠키를 보낼 URL 경로를 고르는 규칙이다. 접근 권한을 나누는 보안 경계로 믿으면 안 된다.Max-Age·Expires: 쿠키 수명을 지정한다. 둘이 없으면 보통 현재 브라우징 세션 범위의 쿠키로 다뤄진다.
Secure는 네트워크 전송을 HTTPS로 제한하지만 서버 측 권한 검사를 대신하지 않는다. HttpOnly도 브라우저가 요청에 쿠키를 붙이는 것은 막지 않으므로 CSRF 대응 전체를 해결하지 않는다. 상태 변경 요청에는 SameSite, CSRF 토큰, Origin 검증과 서버 권한 검사를 서비스 특성에 맞게 조합한다.
현재 쿠키 동작은 RFC 6265와 이를 개정 중인 HTTPWG 6265bis 초안을 함께 확인할 필요가 있다. 초안과 브라우저 기본 정책은 바뀔 수 있으므로 배포 대상 브라우저에서 실제 동작을 검증한다.
캐시는 응답을 언제 재사용할까
캐시는 저장 가능한 응답을 보관하고, 같은 요청에 재사용할 수 있는지 판단한다. 핵심은 저장 가능 여부, 신선도, 재검증이다.
HTTP/1.1 200 OK
Cache-Control: public, max-age=300
ETag: "asset-v12"
Vary: Accept-Encoding
max-age=300: 응답이 생성된 뒤 300초 동안 신선하다고 판단할 수 있다.public: 공유 캐시에도 저장할 수 있음을 명시한다.private: 특정 사용자용 응답이므로 공유 캐시에 저장하지 못하게 한다. 브라우저의 private cache에는 저장될 수 있다.no-cache: 저장 자체를 금지하는 말이 아니다. 재사용 전에 원 서버에 유효성을 확인하라는 뜻이다.no-store: 캐시가 요청이나 응답을 저장하지 말라는 지시다. 이미 다른 곳에 남은 데이터를 지우거나 완전한 개인정보 보호를 보장하는 기능은 아니다.
오래된 응답도 validator가 있으면 전체 본문을 다시 받지 않고 재검증할 수 있다.
GET /app.js HTTP/1.1
If-None-Match: "asset-v12"
표현이 바뀌지 않았다면 서버는 304 Not Modified로 답할 수 있다. ETag는 서버가 정의한 opaque validator이고, Last-Modified와 If-Modified-Since도 시간 기반 재검증에 쓰인다.
Vary와 cache key를 빼놓으면 안 되는 이유
캐시는 기본적으로 요청 메서드와 대상 URI를 중심으로 응답을 찾고, Vary가 지정한 요청 헤더도 cache key 계산에 반영한다. 예를 들어 서버가 압축 형식에 따라 다른 표현을 반환한다면 다음처럼 알린다.
Vary: Accept-Encoding
언어별 응답이면 Accept-Language, API 버전이 요청 헤더에 있다면 해당 헤더가 후보가 될 수 있다. 다만 Vary 항목이 많고 값의 조합이 다양할수록 캐시 적중률은 낮아진다.
특히 Cookie 전체를 Vary에 넣으면 사용자별 조합이 지나치게 많아질 수 있다. 개인화 페이지는 private 또는 적절한 no-store 정책을 쓰고, 공개 정적 자산은 쿠키가 필요 없는 별도 경로나 호스트로 분리하는 설계를 검토한다.
로그인 응답과 정적 자산은 정책이 다르다
모든 응답에 한 가지 Cache-Control을 붙이는 방식은 피한다.
| 응답 | 출발점이 되는 정책 | 확인할 점 |
|---|---|---|
| 해시가 붙은 JS·CSS·이미지 | public, max-age=31536000, immutable |
파일 내용이 바뀌면 URL도 바뀌는가 |
| 자주 갱신되는 공개 문서 | 짧은 max-age와 ETag |
stale 허용 범위와 재검증 비용 |
| 사용자별 계정 화면 | private, no-cache 또는 요구에 따라 no-store |
공유 캐시 유출, 뒤로 가기 UX, 규제 요구 |
| 인증 응답 | 서비스 위험에 맞춘 명시적 정책 | 토큰·개인정보가 캐시되지 않는가 |
immutable을 쓰는 자산은 URL이 그대로인데 내용만 바꾸면 사용자가 오래된 파일을 계속 볼 수 있다. 파일명에 콘텐츠 해시를 넣는 방식과 함께 사용해야 한다.
개발자 도구로 확인할 체크리스트
- 응답의
Set-Cookie가 예상한 Domain, Path, Secure, HttpOnly, SameSite를 갖는지 본다. - 다음 요청에 실제로 어떤
Cookie가 전송됐는지 확인한다. - 응답의
Cache-Control,Age,ETag,Last-Modified,Vary를 본다. - 새로고침 때 조건부 요청과
304가 발생하는지 확인한다. - CDN이나 프록시를 쓴다면 브라우저 캐시와 공유 캐시 결과를 분리한다.
- 로그아웃 뒤 인증 페이지가 과거 캐시에서 노출되지 않는지 별도로 검증한다.
HTTP 메시지와 상태 코드를 먼저 익히려면 HTTP 요청·응답 읽는 법을 함께 보면 흐름이 이어진다.
참고 자료
'배움과 성장 > 네트워크·보안' 카테고리의 다른 글
| OSI 7계층 읽는 법: 캡슐화와 네트워크 장애 구간 찾기 (0) | 2023.03.28 |
|---|---|
| 이메일 전송 과정: SMTP·IMAP·POP3와 MX 레코드 역할 (0) | 2023.03.28 |
| 라우터와 스위치 차이: MAC·IP·브로드캐스트 도메인으로 구분하기 (0) | 2023.03.28 |
| 네트워크 버퍼와 소켓 차이: send·recv 사이에서 데이터가 머무는 곳 (0) | 2023.03.28 |
| HTTP 요청·응답 읽는 법: 메서드·상태 코드·연결 재사용 (0) | 2023.03.28 |
댓글