WEB-CACHE
(TIME-SPACE TRADEOFF)
더 빠른 웹사이트를 위해 저희 개발자들은 눈물겨운 노력을 많이 합니다.
- WebCache
- Code Compressing
- Image Optimizing
- Image Spriting
- Critical Rendering Path
- Webfont Optimizing
- Lazy Loading
- 기타 등등등등등....
위에 나열한 것들은 하나 같이 다 도움이 되는 방법들이지만 이번 저희가 수행하는 웹툰 사이트를 개발하던 중 많은 양의 이미지와 data(json) 등을 서비스 하기 위하여 고민하던 중 Web Cache에 대해 좀 더 깊이 파기 시작하여 알게 된 지식들을 공유해보고자 합니다.
웹 캐쉬
웹 캐쉬란 client가 요청하는 html, image, js, css등에 대해 첫 요청 시에 파일을 내려받아 특정 위치에 복사본을 저장(USING SPACE)하고, 이후 동일한 URL의 Resource요청은 다시 내려 받지 않고 내부에 저장한 파일을 사용하여 더 빠르게 서비스(SAVE TIME)하기 위한 것입니다. 서버를 통해 내려 받는 양이 적어지니 응답 시간이 감소하고 네트워크 트레픽이 감소되니 server와 client 모두가 win-win할 수 있는 최고의 tradeoff 인 셈입니다.
웹 캐쉬의 종류
웹 캐쉬의 종류는 어디에 적용하느냐에 따라 다음과 같이 나뉠 수 있으며 이중 중점적으로 볼 내용은 Browser Cache입니다.
1. Browser Caches
- 브라우저 또는 HTTP요청을 하는 Client Application에 의해 내부 디스크에 캐쉬
- Cache된 Resource를 공유하지 않는 한 개인에 한정된 Cache
- 브라우저의 Back버튼 또는 이미 방문한 페이지를 재 방문하는 경우 극대화
2. Proxy Caches
- Browser Cache와 동일한 원리로 동작하며 Client나 Server가아닌 네트워크 상에서 동작.
- 큰회사나 IPS의 방화벽에 설치 되며 대기시간 & 트래픽 감소, 접근정책 & 제한 우회, 사용률 기록등 수행
- 한정된 수의 클라이언트을 위하여 무한대의 웹서버의 컨텐츠를 캐쉬
3. Gateway Caches (REVERSE OR SURROGATE PROXY)
- 서버 앞 단에 설치되어 요청에 대한 캐쉬 및 효율적인 분배를 통해 가용성, 신뢰성, 성능등을 향상
- Encryption / SSL acceleration, Load balancing, Serve/cache static content, Compression등을 수행
- 무한대의 클라이언트들에게 한정된 수(또는 하나)의 웹서버 컨텐츠를 제공
어떻게 캐쉬를 컨트롤 하나?
브라우저는 한번 요청한 파일은 그 이후부터는 캐쉬를 사용한다고 말했습니다. 하지만 만약 캐쉬되지 않아야 하거나 캐쉬된 내용에 변경이 발생하면 어떻게 될까요? 브라우저가 어떻게 캐쉬를 컨트롤 하는지 알아봅시다. 캐쉬를 컨트롤 하는 방법에는 크게 2가지가 있습니다.
1. HTML Meta Tags
<META HTTP-EQUIV="EXPIRES" CONTENT="Mon, 22 Jul 2002 11:12:01 GMT">
<META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
첫 번째 방법은 위와 같은 HTML Meta Tag를 페이지에 삽입하는 방법입니다. 하지만 이 방법은 과거의 몇몇 브라우저에게만 유효 했으며 지금은 더 이상 사용하지 않는 방법입니다.
2. HTTP Headers
|
HTTP 1.0 (1996년) |
HTTP 1.1 (1999년) |
||
|
REQUEST |
RESPONSE |
REQUEST |
RESPONSE |
validation |
If-Modified-Since |
Last-Modified |
If-None-Match |
Entity Tag (Etag) |
freshness |
Pragma |
Expires |
Cache-Control |
Cache-Control |
두 번째 방법은 HTTP Headers를 사용하는 방법으로 지금 저희가 사용하고 있는 방식입니다.
파일이 이전과 비교하여 변경 되었는가를 체크하는 validation과 캐쉬의 만료 여부를 체크하는 freshness로 구성됩니다. request와 response에 따라 서로 사용될 수 있는 값이 다르며 HTTP1.0에서 HTTP1.1로 넘어오면서 약간의 변화가 있습니다. HTTP 1.1에서는 하위 호환되므로 1.0의 header를 사용하여도 정상 동작하지만 중복으로 선언된다면 1.1에 정의된 것이 우선순위를 가지게 됩니다.
예를 들어 Last-Modified와 Etag가 동시에 있다면 Etag가 우선순위를 가집니다. Expires와 Cache-Control도 마찬가지 입니다.
HTTP 1.1의 Cache-Control은 하나의 값이 아니라 다양한 지시자를 ,를 이용하여 값을 전달할 수 있습니다. 그로 인해 여러가지 컨트롤을 가능하게 만들어 줍니다. ex)Cache-Control:max-age=3600, must-revalidate
지시자 |
설명 |
max-age=[sec] |
Expires 와 동일한 의미지만 고정된 절대 시간 값이 아닌 요청 시간으로부터의 상대적 시간을 표시합니다. 명시된 경우
|
s-maxage=[sec] |
max-age와 동일한 의미지만 shared caches(예:proxy)에만 적용됩니다. 명시된 경우 max-age나 Expires보다 우선순위를 가집니다. |
public |
일반적으로 HTTP인증이 된 상태에서 일어나는 응답은 자동으로 private이 됩니다. public을 명시적으로 설정하면 인증이 된 상태더라도 캐쉬 하도록 합니다.
|
private |
특정 유저(사용자의 브라우저)만 캐쉬 하도록 설정 합니다. 여러 사람이 사용하는 네트워크상의 중간자 (intermediaries)역할을 하는 shared caches (예: proxy) 에는 경우 캐쉬되지 않습니다.
|
no-cache |
응답 데이터를 캐쉬하고는 있지만, 먼저 서버에 요청해서 유효성 검사(validation)을 하도록 강제 합니다. 어느 정도 캐쉬의 효용을 누리면서도 컨텐츠의 freshness를 강제로 유지하는데 좋습니다.
|
no-store |
어떤 상황에서도 해당 response 데이터를 저장하지 않습니다.
|
no-transform |
어떤 프록시들은 어떤 이미지나 문서들을 성능향상을 위해 최적화된 포맷으로 변환하는 등의 자동화된 동작을 하는데 이러한 것을 원치 않는다면 이 옵션을 명시해주는 것이 좋습니다.
|
must-revalidate |
HTTP는 특정 상황(네트워크 연결이 끊어졌을 때 등)에서는 fresh하지 않은 캐쉬 데이터임에도 불구하고 사용하는 경우가 있는데, 금융거래 등의 상황에서는 이러한 동작이 잘못된 결과로 이어질 가능성이 있기 때문에 이 지시자를 통해서 그러한 사용을 방지합니다.
|
proxy-revalidate |
must-revalidate와 비슷하지만 shared caches (예: proxy)에만 적용됩니다.
|
developers.google.com 의 Ilya Grigorik분이 우리가 상황에 맞게 쉽게 선택할 수 있도록 다음의 그림을 제공하고 있습니다.
*Pragma의 인터넷에 떠도는 경우 약간의 오해가 있는데 response에 대한 HTTP명세가 없습니다. 단지 request에서만 사용될 수 있으며 몇몇 캐쉬는 이 헤더에 의해 처리될 수 있지만 대부분은 처리되지 못하므로 사용하지 않는 것이 좋습니다. http1.0에 존재하며 1.1에서는 무시됩니다. 즉, response.setHeader("Pragma","no-cache"); 는 동작하지 않습니다.
어떻게 캐쉬가 동작 하나?
이제 캐쉬가 일어나는 과정에서 위에서 설명한 http header들이 어떻게 사용되는지 살펴보겠습니다.
첫 요청
1. 브라우저는 서버에 index.html 파일을 요청합니다.
2. 서버는 index.html파일을 찾아보고 존재 하는 파일이라면 파일 내용을 브라우저에게 몇 가지 header값과 함께 응답합니다.
3. 브라우저는 응답 받은 내용을 브라우저에 표시하고 응답 헤더의 내용에 따라 캐쉬 정책을 수행합니다.
(응답 헤더에 Last-Modified, Etag, Expires, Cache-Control:max-age 항목이 존재 한다면 복사본을 생성하고 값을 저장 )
재 요청
1. LAST-MODIFIED
1. 브라우저는 최초 응답 시 받은 Last-Modified 값을 If-Modified-Since 라 는 헤더에 포함 시켜 페이지를 요청합니다.
2. 서버는 요청 파일의 수정 시간을 If-Modified-Since값과 비교하여 동일하 다면 304 Not Modified로 응답하고 다르다면 200 OK와 함께 새로운 Last-Modified값을 응답 헤더에 전송합니다.
3. 브라우저는 응답 코드가 304인경우 캐쉬에서 페이지를 로드하고 200이라 면 새로 다운받은후 Last-Modified값을 갱신합니다.
2. ETAG (ENTITY TAG)
1. 브라우저는 최초 응답 시 받은 Etag값을 If-None-Match 라는 헤더에 포함 시켜 페이지를 요청합니다.
2. 서버는 요청 파일의 Etag값을 If-None-Match값과 비교하여 동일하다 면 304 Not Modified로 응답하고 다르다면 200 OK와 함께 새로운 Etag 값 을 응답 헤더에 전송합니다..
3. 브라우저는 응답 코드가 304인경우 캐쉬에서 페이지를 로드하고 200이 라면 새로 다운받은후 Etag값을 갱신합니다.
* Etag는 서버마다 생성하는 값이 다르며 파일마다 고유한 값을 가집니다. 자세한 설명은 뒤에 다시 하겠습니다.
* LAST-MODIFED(1.0) 와 ETAG(1.1)는 validation을 체크 합니다. 이를 체크하기 위해 서버와 한번의 통신이 발생하게 되며 그로 인해 요청과 응답에서 header와 cookie등에 의한 데이터 전송(1KB)이 발생하게 됩니다.
3. Expires
1. 브라우저는 최초 응답 시 받은 Expires 시간을 비교하여 기간 내라면 서버를 거치지 않고 바로 캐쉬에서 페이지를 로드 합니다. 만약 기간이 만료되었다면 위에 설명한 validation 작업을 수행합니다.
4. Cache-Control
1. 브라우저는 최초 응답 시 받은 Cache-Control 중 max-age값(초 단위)를
GMT와 비교하여 기간 내라면 서버를 거치지 않고 캐쉬에서 페이지를 로드
합니다. 만약 기간이 만료되었다면 validation 작업을 수행합니다.
* Expires(1.0)와 Cache-Control: max-age(1.1)는 freshness를 체크합니다. 기간 내라면 서버와 통신을 하지 않고 캐쉬를 사용합니다.
* 시간은 HTTP date 형태이며 로컬 타임이 아닌 GMT를 사용합니다.
* 서버가 Last Modified Time 또는 Last Access Time을 기준으로 하여 일정 시간 이후로 Expires 또는 max-age를 설정 합니다.
어떻게 캐쉬를 설정 하나?
캐쉬는 서버에서 설정 하는데 파일 확장자 명으로 다르게 설정하거나 디렉토리 별로 다르게 설정할 수 있습니다. 또한 Expires나 Etag는 서버 설정에 의하여 사용하지 않을 수도 있습니다. expires가 설정되면 Expires와 max-age가 같이 설정됩니다.
CACHING STRATEGY
캐쉬가 잘 적용되게 하기 위해서 다음과 같은 전략을 새웁니다.
1. 일관된 URL을 사용하라. 동일한 URL은 동일한 사이트라면 다른 페이지에서도 캐쉬되어 사용될 수 있습니다.
2. 자주 바뀌는 파일과 그렇지 않은 파일을 분리합니다. 그래야 각 Resource에 대해 최적의 freshness를 설정할 수 있습니다.
3. 다운가능한 파일의 내용이 바뀌면 이름(URL)을 바꿉니다. 그래야 올바로 수정된 버전을 제공할 수 있습니다.
4. SSL을 최소화 합니다. 암호화된 페이지는 캐쉬되지 않습니다.
RealWorld
캐쉬가 적용되면 웹페이지가 뜨는 속도 자체가 달라집니다. 다음의 동일한 요청에 대해 캐쉬전, validation, freshness 경우 입니다.
캐쉬 전과 후의 Size ( 7.0KB -> 123B -> from cache) 와 Time (69ms -> 11ms -> 1ms) 을 보면 확연하게 차이가 발생하는 것을 알 수가 있습니다. 캐쉬만 적용한다면 사용자들에게 정말 빠르게 서비스할 수 있을 것 같습니다.
하지만 실제 적용하다 보면 여러가지 문제점이 발생합니다. 세상은 그렇게 쉽지 않습니다....
1. freshness를 check 하는 Expires와 Cache-Control: max-age 는 파일이 아직 유효하다고 판단되는 경우 서버에 전송조차 보내지 않고 캐쉬를 사용합니다. 만약 서버에서 CSS또는 JAVASCRIPT가 변경되었다면 유효기간이 만료되기 전까지 또는 사용자가 강제로 캐쉬를 삭제 하기 전까지는 사용자마다 캐쉬 상태에 따라 서로 다른 화면을 볼 수 있습니다.
2. 리얼 환경의 서버 구성은 한대로 구성되지 않고 여러대의 서버로 구성되는 경우가 많습니다. 이 경우 더 큰 문제들이 발생을 합니다.
그림과 같이 구성된 경우 사용자는 L4에 의해 각 상황에 맞게 분배 되게 됩니다. 캐쉬를 컨트롤하는 Etag, Last-Modified 는 파일의 최종 시간과 관련이 있습니다. 하지만 서버가 서로 바라보는 파일이 다르다면 파일의 수정시간이 다를것이고 사용자가 1번 서버에서 캐쉬하면서 받은 Last-Modified 또는 Etag값이 2번 서버와는 다르기 때문에 1번과 2번을 번갈아가며 접속되는 순간마다 캐쉬는 무효화가 되며 다시 처음부터 다운받는 작업을 다시 하게 됩니다. 만약 L4의 분배 알고리즘이 Round robin 이라면 더욱더 이 현상은 심해질 것입니다.
3. Etag는 기본적으로는 정해놓은 몇개의 값을 가지고 MD5 Hash등의 방법으로 digest한 값입니다. 파일의 내용만 가지고 digest를 한다면 서버가 다르다 하여도 아무 문제가 발생하지 않지만 파일 내용을 매번 digest하여 비교 한다는 것은 너무나 비 효율적일 것입니다. 그래서 각 서버마다 설정해놓은 기본값을 가지고 Etag를 생성합니다. 물론 설정을 통하여 값을 바꾸거나 Etag를 사용하지 않을수도 있습니다.
서버 |
기본값 |
Apache |
INode + MTime + Size |
Nginx |
MTime + Size (Size는 제 추측입니다... 정확한 값을 알고 계시다면 알려주시면 감사하겠습니다.) |
IIS |
MTime + ChangeNumber |
Tomcat |
Size + MTime |
표에서 보는 바와 같이 모든 서버는 Modification Time을 공통적으로 사용합니다. 2번과 동일한 문제가 발생할 수 있습니다. 또한 Apache
나 IIS는 서버가 특별히 관리하는 INode값이나 ChangeNumber를 적용하여 생성하지만 마찬가지로 서버마다 그 값이 달라 Mtime이 같다 하여도 만들어 지는 Etag는 값이 서로 다르게 됩니다. 이렇다 보니 Etag를 사용하지 말자 라는 글들도 눈에 많이 띄게 됩니다.
FingerPrint!!
위와 같은 문제점들을 해결하기 위하여 다음과 같은 방식을 사용하는것 같습니다. 바로 fingerprint를 적용하는 것입니다. 하지만 파일명을 계속해서 변경하기는 쉽지 않을 것 같습니다. 파일명을 변경하지 않고 fingerprint를 적용하는 방법은??????
그렇습니다. 바로 url에 ?fingerprint=f8e66dc와 같이 parameter를 추가 합니다. browser는 fingerprint가 변경될 경우 url이 변경되었기 대문에 새로운 resource라고 인식하여 무조건 새로 다운로드 받습니다. 파일의 내용이 변경되었다면 fingerprint값만 변경해주면 됩니다.
jquery같이 한번 적용되면 변경할 일이 없는 library들은 굳이 fingerprint를 적용할 이유는 없습니다. 캐쉬가 잘 적용되는 모습입니다.
아...... 또 문제가 생겼습니다. 파일이 변경될 때마다 모든 페이지를 찾아가며 fingerprint를 변경해야 합니다. 이건 일이 더욱더 커집니다.
JSP 의 CUSTOM TAG를 이용하기로 합니다. 커스텀 태그에서 하는 일은 파일을 설정 시간 단위로 감시하고 있다가 파일의 내용이 변경되면 파일의 내용만을 가지고 md5 digest한값을 fingerprint로 만들어 파일 경로를 넣으면 자동으로 fingerprint를 추가해줍니다. 아..... html은 TAGLIB을 사용할 수 없습니다. 이 부분은 아직 해결하지 못했습니다. 추후 방법이 생각나면 업데이트 하도록 하겠습니다. ㅠㅜ
오오오?!
1.TWO TYPES OF REQUESTS
브라우저는 상황에 따라 2가지 유형의 request를 서버에 날립니다.
- UnConditional (download)
- 브라우저가 캐쉬된 파일을 가지고있지 않은경우
- 유저가 Ctrl + 새로고침(Refresh button or F5)을 하는 경우
- 링크, 이전 & 다음 버튼, 주소창에 입력후 엔터를 치는 경우
(이때 fresh한 캐쉬 아이템이 서버와 통신 없이 캐쉬만으로 처리됩니다.)
- Conditional (validate)
- Cache-Control or Expires이 만료된 경우
- Cache가 저장될때 Vary header와 같이 전달 되었던경우
- META TAG를 이용한 refresh가 발생할 경우
- 자바스크립트의 location을 통해 reload 된 경우
- cross-host HTTPS 를 통해 요청되는 경우
- 유저가 새로고침(Refresh button or F5)을 하는 경우
Conditional Request가 발생되면 요청되는 모든 Resource에 대하여 freshness여부에 상관없이 무조건 revalidation을 요청합니다.
즉, freshness가 유효하다 하여도 서버 통신을 통해 304 또는 200의 응답을 받게 됩니다.
새로고침 (Conditional) 링크클릭,이전버튼, 주소창 (Unconditional)
2.HEURISTIC EXPIRATION
이상하게 분명 Expired 또는 Cache-Control을 설정하지 않았는데 freshness가 유효하다 판단하여 서버에 요청하지 않고 캐쉬되는 경우가 있습니다. 이 것은 Response의 헤더에 expiration times ("Expires" 와 "Cache-Control")가 2개다 명시되어 있지 않지만 Last-Modified는 명시된 경우 브라우저는 heuristic expiration times을 부여합니다. 이는 HTTP/1.1스펙에는 정확한 알고리즘을 제공하지 않으므로 각 브라우저가 각각 따로 구현하고 있습니다. 가능한 한 origin server에서 명확하게 expiration times를 명시하는 것이 좋습니다.
브라우저 |
계산법 |
IE9 |
max-age = (DownloadTime - LastModified) * 0.1 |
Gecko |
now + (now - lastModified)/10 |
Webkit |
(creationTime - lastModifiedValue) * 0.1 |
Chromium |
(date_value - last_modified_value) / 10 |
3.CONTENT-LENGTH IN RESPONSE HEADER
HTTP 1.1 에서는 TCP/IP 커넥션이 끊어지지 않고 유지되는 Keep Alive connection(persistent connection)이 지원됩니다. response header에 content-length항목이 추가 되면 이 기능이 활성화 되어 각 request를 위해 매번 설정하고 연결을 맺고 끊는 과정 없이 하나의 커넥션으로 전부 요청 하기 때문에 페이지의 속도가 더욱 빨라집니다.