Home HTTP 캐시
Post
Cancel

HTTP 캐시


아직 작성 중입니다.




1. 캐시의 생명 주기


HTTP 캐시는 응답을 받은 순간부터 바로 만료되지 않습니다. 응답에 포함된 Cache-Control 헤더에 따라 캐시는 유효 상태(fresh) 와 만료 상태(stale) 를 거칩니다.

  • 캐시가 fresh 상태인 동안 브라우저는 서버나 CDN에 요청을 보내지 않습니다.
  • 캐시가 stale 상태가 되면, 브라우저는 서버에 요청을 보내되
  • 기존 캐시를 그대로 써도 되는지 재검증을 수행합니다.

즉, 캐시는 “있다 / 없다”가 아니라 “지금 써도 되는가 / 확인이 필요한가”의 문제입니다.





2. max-age


max-age는 캐시가 무조건 유효하다고 간주되는 시간을 초 단위로 지정합니다. 이 값이 설정된 리소스는 해당 시간이 지나기 전까지 브라우저에서 fresh 상태로 유지되며, 브라우저는 서버나 CDN에 요청을 보내지 않습니다.

1
Cache-Control: max-age=31536000

위와 같이 max-age가 설정되면, 브라우저는 이 리소스를 디스크 또는 메모리 캐시에서만 사용합니다. 이 기간 동안에는 서버 요청뿐 아니라 CDN 요청도 발생하지 않으며, 네트워크 계층 자체가 완전히 우회됩니다.

이 동작은 서버의 상태와 무관하게 적용됩니다. 서버에서 리소스를 변경했거나 CDN에서 캐시를 무효화했더라도, 브라우저에 저장된 캐시의 max-age가 남아 있는 동안에는 해당 변경을 인지할 수 없습니다. 브라우저는 캐시가 아직 유효하다고 판단하기 때문에, 어떠한 검증 요청도 보내지 않습니다.





3. Revalidation


max-age가 지나면 캐시는 즉시 삭제되지 않습니다. 브라우저는 캐시를 제거하는 대신, 해당 캐시가 여전히 사용 가능한지 확인하는 재검증(Revalidation) 단계로 전환합니다. 이 시점에서 캐시는 stale 상태가 되며, 더 이상 무조건적으로 재사용될 수는 없습니다. 재검증 단계에서 브라우저는 서버에 요청을 보내지만, 리소스 전체를 다시 내려받기 위한 요청은 아닙니다. 기존에 저장된 캐시를 계속 사용해도 되는지 여부만을 판단하기 위한 조건부 요청을 전송합니다.

1
Cache-Control: max-age=0





이 요청에는 이전 응답에서 받은 메타데이터가 함께 포함되며, 서버 또는 CDN은 이를 기준으로 리소스가 변경되었는지만 판단합니다. 재검증 결과 리소스가 변경되지 않았다면, 서버는 본문 없이 상태 코드만을 반환하고 브라우저는 기존 캐시를 그대로 사용합니다.

반대로 재검증 결과 리소스가 변경되었다면, 서버는 새로운 응답 본문을 포함한 정상 응답을 반환합니다. 이 경우 브라우저는 기존 캐시를 폐기하고, 새로 받은 리소스로 캐시를 갱신합니다.

즉, 재검증은 캐시를 다시 받을지, 그대로 유지할지를 결정하는 단계이며, HTTP 캐시는 이 과정을 통해 네트워크 비용과 데이터 전송을 최소화하면서도 최신성을 유지합니다.





4. If-Modified-Since / Last-Modified


If-Modified-Since / Last-Modified는 시간(timestamp)을 기준으로 리소스 변경 여부를 판단하는 조건부 요청 방식입니다. 가장 오래되고 널리 사용되는 재검증 메커니즘으로, HTTP 캐시의 기본적인 동작 원리를 잘 보여줍니다.

서버는 응답을 내려줄 때, 해당 리소스가 마지막으로 수정된 시점을 Last-Modified 헤더로 함께 전달합니다.

1
Last-Modified: Wed, 24 Jan 2026 10:30:00 GMT

브라우저는 이 값을 캐시 메타데이터로 저장해 두었다가, 다음 요청 시 그대로 서버에 전달합니다.

1
If-Modified-Since: Wed, 24 Jan 2026 10:30:00 GMT

이렇게 전달된 요청을 받은 서버나 CDN은 “이 시점 이후에 리소스가 변경되었는가?”만을 판단합니다. 만약 지정된 시점 이후에 변경 사항이 없다면, 서버는 본문 없이 304 Not Modified를 반환하고, 변경되었다면 새로운 본문과 함께 200 OK를 반환합니다. 판단 기준은 오직 시간 비교 하나입니다.

이 방식의 장점은 구현이 단순하고 비용이 낮다는 점입니다. 별도의 해시 계산이나 상태 비교 없이, 수정 시각만 확인하면 되기 때문에 서버와 캐시 모두 부담이 적습니다. 그래서 정적 파일 서버, 단순한 CDN 환경, 파일 시스템 기반 리소스에 널리 사용되어 왔습니다.

하지만 시간 기반 검증에는 명확한 한계가 존재합니다. 서버와 클라이언트 간의 시간 오차, 파일 시스템의 타임스탬프 정밀도 문제, 동일한 시각에 여러 번 수정되는 경우 등으로 인해 실제 내용은 바뀌었지만 변경되지 않은 것처럼 판단되거나, 반대로 내용은 같지만 불필요하게 변경된 것으로 판단되는 상황이 발생할 수 있습니다.





5. 304 Not Modified


304 Not Modified는 클라이언트가 요청한 리소스가 이전에 받은 버전과 비교했을 때 변경되지 않았음을 알리는 응답 코드입니다. 이 응답의 가장 중요한 특징은 본문(body)이 포함되지 않는다는 점입니다.

1
HTTP/1.1 304 Not Modified

304 응답은 아무 요청 없이 발생하는 것이 아니라, 반드시 조건부 요청의 결과로만 발생합니다. 즉, 브라우저나 캐시는 If-None-Match, If-Modified-Since와 같은 헤더를 포함해 요청을 보내고, 서버 또는 CDN이 이를 기반으로 변경 여부를 판단한 뒤에 반환합니다.

이 과정에서 중요한 점은, 요청은 실제로 서버나 CDN에 도달한다는 것입니다. 네트워크 요청 자체가 생략되는 것은 아닙니다. 서버는 리소스의 현재 상태와 요청에 포함된 검증 정보를 비교하고, 변경되지 않았다고 판단하면 응답 본문을 생성하지 않고 304만 반환합니다. 이로 인해 CPU 사용량과 네트워크 전송 비용이 크게 줄어듭니다.

브라우저는 304 응답을 받으면, 자신이 이미 보유하고 있던 캐시를 그대로 사용합니다. 서버가 “그 캐시 그대로 써도 된다”라고 공식적으로 확인해 준 셈이기 때문에, 캐시된 응답은 다시 유효한 상태가 됩니다. 이때 캐시의 만료 시간이나 검증 메타데이터도 갱신될 수 있습니다.

여기서 자주 혼동되는 부분이 있습니다. 304는 캐시 적중(cache hit)이 아닙니다. 캐시 적중은 요청이 네트워크를 타지 않고 로컬 캐시나 CDN에서 즉시 처리되는 경우를 의미합니다. 반면 304는 네트워크 요청이 발생했고, 재검증이 성공한 결과입니다. 즉, 캐시는 있었지만, 그것을 사용해도 되는지에 대한 서버의 확인을 거친 상태입니다.





6. ETag / If-None-Match


ETag(Entity Tag)는 리소스의 현재 상태를 식별하기 위한 버전 식별자입니다. Last-Modified처럼 시간 정보를 기준으로 판단하지 않고, 리소스의 실제 내용이 변경되었는지 여부를 기준으로 캐시 검증을 수행합니다.

1
ETag: "f3a3f82a..."

서버는 응답을 내려줄 때 해당 리소스의 상태를 대표하는 ETag 값을 함께 전달합니다. 이 값은 보통 파일 내용의 해시, 버전 ID, 혹은 내부적으로 관리되는 변경 토큰으로 생성됩니다. 중요한 점은 같은 내용이면 같은 ETag, 내용이 바뀌면 반드시 다른 ETag가 되어야 한다는 것입니다.

브라우저나 캐시는 이후 재요청 시 이 값을 그대로 들고 조건부 요청을 보냅니다.

1
If-None-Match: "f3a3f82a..."

서버는 현재 리소스의 ETag와 요청에 포함된 If-None-Match 값을 비교합니다. 두 값이 같다면, 리소스는 변경되지 않았다고 판단하고 본문 없이 304 Not Modified를 반환합니다. 반대로 값이 다르면, 리소스가 변경된 것이므로 새로운 본문과 함께 200 OK를 반환합니다.

이 방식의 가장 큰 장점은 시간 오차 문제를 완전히 제거한다는 점입니다. If-Modified-Since 기반 검증은 서버와 클라이언트의 시간 차이, 파일 시스템의 타임스탬프 정밀도, 초 단위 반올림 문제 등으로 인해 변경 여부를 정확히 판단하지 못하는 경우가 있습니다. 반면 ETag는 순수하게 “내용이 같으냐, 다르냐”만을 기준으로 판단하기 때문에 훨씬 정확합니다.

그래서 ETag는 자주 변경되지는 않지만, 변경 시에는 반드시 반영되어야 하는 리소스나, 정적 파일이더라도 배포 환경이나 생성 방식에 따라 시간 정보가 신뢰하기 어려운 경우에 특히 유용합니다. CDN과 브라우저 캐시 모두에서 일관된 검증 기준을 제공한다는 점도 큰 장점입니다.

이미 동일한 ETag를 가진 버전을 알고 있다면, 서버는 전체 파일을 다시 내려주지 않고 304 응답만 반환합니다. 이로 인해 네트워크 트래픽은 최소화되면서도 최신성은 정확히 보장됩니다.





7. no-cache


no-cache는 이름과 달리 캐시를 금지하는 옵션이 아닙니다. 의미는 단 하나, “캐시를 사용하기 전에 반드시 서버의 판단을 거쳐라”입니다.

1
Cache-Control: no-cache

이 지시자가 붙은 응답은 캐시에 저장될 수 있습니다. 브라우저 캐시든, CDN 같은 중간 캐시든 모두 저장 자체는 허용됩니다. 다만 차이점은, 캐시에 저장된 데이터를 그대로 재사용하는 것이 금지된다는 점입니다.

클라이언트가 다음 요청을 보낼 때, 캐시는 저장된 응답을 기반으로 조건부 요청을 서버로 보냅니다. 이때 보통 If-None-Match(ETag)나 If-Modified-Since 헤더가 사용됩니다. 서버는 해당 리소스가 변경되지 않았다고 판단하면 304 Not Modified를 반환하고, 변경되었다면 새로운 본문과 함께 200 OK를 반환합니다.

즉, 캐시는 중간 저장소이자 비교 기준으로만 사용되고, 최종 결정권은 항상 서버가 가집니다. 이 때문에 no-cache는 “항상 서버를 거친다”는 의미로 이해하는 것이 가장 정확합니다. 캐시된 데이터가 있더라도, 서버가 “그거 써도 된다”라고 명시적으로 확인해 주지 않으면 사용할 수 없습니다.

이 방식의 장점은 최신성을 매우 강하게 보장하면서도, 완전히 캐시를 포기하지는 않는다는 점입니다. 본문 데이터가 변경되지 않았다면 304 응답만 주고받으면 되므로, 네트워크 비용은 최소화할 수 있습니다. 반대로 매번 서버와 통신해야 하기 때문에, 지연 시간(latency)은 max-age 기반 캐시에 비해 늘어날 수 있습니다.

no-store와 비교하면 차이가 더 분명해집니다. no-store는 캐시 자체를 남기지 않기 때문에 매번 전체 응답을 다시 받아야 하지만, no-cache는 캐시를 유지한 채 검증만 반복합니다. 그래서 no-cache는 보안보다는 정합성과 최신성이 중요한 리소스에 적합합니다.





8. no-store


no-store는 이 응답을 어떤 형태로도 저장하지 말라는 가장 강력한 캐시 지시자입니다. 단순히 캐시를 재사용하지 말라는 수준이 아니라, 아예 기록 자체를 남기지 말라는 의미에 가깝습니다.

1
Cache-Control: no-store

이 헤더가 포함된 응답은 브라우저 캐시에도 저장되지 않으며, CDN이나 리버스 프록시 같은 중간 캐시 계층에도 절대 저장되지 않습니다. 더 나아가, 디스크 캐시뿐만 아니라 메모리 캐시조차 허용되지 않습니다. 즉, 응답은 전송되는 순간에만 존재하고, 응답이 끝나는 즉시 폐기되어야 합니다.

no-store가 no-cache와 혼동되는 경우가 많은데, 둘은 목적이 완전히 다릅니다. no-cache는 “저장할 수는 있지만, 사용하기 전에 반드시 재검증하라”는 의미인 반면, no-store는 “저장 자체가 금지”입니다. 재검증의 대상이 될 캐시조차 남기지 않겠다는 점에서, no-store는 성능보다 보안과 프라이버시를 최우선으로 둔 선택입니다.

이 옵션은 특히 인증 정보나 민감한 개인정보를 포함한 응답에 사용됩니다. 예를 들어 로그인 응답, 토큰 발급 API, 비밀번호 변경 결과, 개인 식별 정보(주민번호, 계좌 정보, 카드 정보 등)가 포함된 응답은 캐시가 남는 것 자체가 보안 사고로 이어질 수 있습니다. 이런 경우에는 브라우저의 뒤로 가기 캐시나 개발자 도구, 프록시 서버 로그 등에 남지 않도록 no-store를 명시적으로 설정해야 합니다.

또한 no-store는 캐시 계층의 동작을 믿지 않겠다는 선언이기도 합니다. 브라우저, CDN, 프록시 구현에 따라 캐시 처리 방식이 달라질 수 있지만, no-store는 모든 구현체가 반드시 지켜야 하는 절대 규칙으로 취급됩니다. 그래서 규격적으로도 가장 보수적인 옵션이며, 금융·결제·인증 시스템에서 사실상 필수로 사용됩니다.





9. public/private


public과 private는 응답을 어디에 저장해도 되는지, 즉 캐시의 저장 위치를 제한하는 지시자입니다. 캐시의 유효 시간(max-age, s-maxage)이 “얼마나 오래 저장할 수 있는가”를 정한다면, public/private는 “누가 저장해도 되는가”를 결정합니다.

1
Cache-Control: public, max-age=86400

public으로 지정된 응답은 모든 캐시 계층에서 저장이 허용됩니다. 사용자의 브라우저 캐시는 물론이고, CDN이나 리버스 프록시 같은 공유 캐시도 해당 응답을 자유롭게 캐싱할 수 있습니다. 일반적으로 정적 리소스나 사용자에 따라 내용이 달라지지 않는 API 응답, 이미지·JS·CSS 파일 등에 사용됩니다. 인증 헤더가 포함되어 있더라도 public이 명시되면 “이 응답은 개인 정보와 무관하니 공유 캐시에 저장해도 된다”는 서버의 의도를 명확히 전달하는 효과가 있습니다.

반대로 private는 개인 사용자 전용 캐시만 허용합니다.

1
Cache-Control: private, max-age=60

이 경우 응답은 브라우저에는 캐시될 수 있지만, CDN이나 프록시 같은 공유 캐시에는 저장되어서는 안 됩니다. 즉, 해당 응답은 사용자마다 다르거나 민감한 정보를 포함하고 있다는 의미입니다. 로그인 상태에 따라 내용이 달라지는 페이지, 사용자 정보가 포함된 API 응답, 세션 기반 데이터 등이 대표적인 예입니다.

private가 중요한 이유는 단순한 성능 문제가 아니라 보안과 데이터 격리에 있습니다. 만약 사용자별로 다른 응답이 CDN에 캐시된다면, 다른 사용자가 잘못된 데이터를 받아보는 치명적인 문제가 발생할 수 있습니다. 따라서 서버는 private를 통해 “이 응답은 절대 공유되면 안 된다”는 강한 의도를 캐시 계층에 전달합니다.

또 하나 중요한 점은, 아무 옵션도 명시하지 않았을 때의 기본 동작은 모호하다는 것입니다. HTTP 스펙상 인증 정보가 포함된 요청에 대한 응답은 기본적으로 공유 캐시가 보수적으로 처리하지만, 이는 구현체마다 다를 수 있습니다. 그래서 의도가 분명한 경우에는 public 또는 private를 명시적으로 지정하는 것이 안전합니다.





10. s-maxage


s-maxage는 브라우저가 아니라 중간 캐시 계층, 대표적으로 CDN이나 리버스 프록시에만 적용되는 캐시 만료 시간(TTL)입니다. 즉, 같은 Cache-Control 헤더 안에 있더라도 max-age와는 적용 대상이 다릅니다. max-age는 최종 사용자 브라우저를 기준으로 동작하는 반면, s-maxage는 요청과 서버 사이에 존재하는 공유 캐시(shared cache)를 기준으로 동작합니다.

1
Cache-Control: s-maxage=31536000, max-age=0

이 설정의 핵심 의도는 캐시 전략을 계층별로 분리하는 것입니다. CDN에서는 해당 리소스를 최대 1년 동안 신뢰하고 캐싱하지만, 브라우저에서는 캐시를 신뢰하지 않고 매 요청마다 원본 서버에 재검증을 수행합니다. 브라우저 입장에서는 항상 최신성을 확인하고 싶지만, 서버 입장에서는 동일한 요청이 매번 서버까지 도달하는 것은 피하고 싶은 상황에 매우 적합한 구조입니다.

이렇게 구성하면 사용자의 브라우저 요청은 매번 네트워크를 타지만, 실제로는 대부분 CDN에서 응답이 처리됩니다. 브라우저는 max-age=0에 따라 캐시된 리소스를 그대로 쓰지 않고 조건부 요청( If-None-Match, If-Modified-Since 등)을 보내지만, 그 요청은 원본 서버가 아니라 CDN에서 처리됩니다. CDN은 자신이 보유한 캐시가 아직 s-maxage 기간 안에 있다면, 원본 서버까지 가지 않고 즉시 응답을 반환합니다.

결과적으로 사용자는 항상 최신 검증 흐름을 거친 것처럼 보이지만, 원본 서버는 보호되고, 트래픽과 부하는 CDN 레벨에서 흡수됩니다. 특히 HTML처럼 사용자별로 달라질 수 있거나, 인증·권한과 연관된 응답의 경우 브라우저 캐시는 보수적으로 가져가되, CDN 캐시는 적극적으로 활용하는 전략에 자주 사용됩니다.

또 하나 중요한 점은, s-maxage가 설정되면 공유 캐시에서는 max-age보다 우선 적용된다는 것입니다. 즉, CDN은 max-age=0을 무시하고 s-maxage 값을 기준으로 캐시를 유지합니다. 이 덕분에 하나의 헤더로 “브라우저는 캐시하지 마라, 하지만 CDN은 캐시해라”라는 상반된 요구를 동시에 만족시킬 수 있습니다.






This post is licensed under CC BY 4.0 by the author.