Egloos | Log-in


브라우저가 JS, CSS 등 정적 자원 캐시 잘하도록 힌트 주기

웹어플리케이션이 화려해지면서 JavaScript와 CSS를 많이 쓴다. 예전에는 JavaScript와 CSS 등 클라이언트 기술은 웹어플리케이션에서 주연이 아니고 조연이었기 때문에 별도의 파일로 두기 보다는 HTML에 삽입해서 HTML과 섞어서 사용했다. 지금은 클라이언트 기술의 비중이 커졌고 어플리케이션 자체도 커져 관리의 편의성 등을 위해 HTML에 CSS나 JavaScript를 섞지 않고 별도의 파일로 관리한다.

HTML에서 CSS와 JavaScript가 별도의 파일로 분리되어서, 브라우저는 HTML에서 참조되는 자원을 가지고 오기 위해 별도로 추가적인 요청을 한다. 매 요청마다 다른 내용이 되는 HTML과는 달리 CSS나 JavaScript 등은 내용이 바뀌지 않기 때문에 매번 요청하는 것은 네트웍과 서버 자원 낭비이다. 그래서 YSlow 같은 툴은 이런 정적파일들에 Expires 헤더를 지정하여 브라우저가 캐시할 수 있게 힌트를 주도록 권고한다.

Expires 헤더 추가하여 캐시 되도록 하기


CSS나 JavaScript 파일에 대한 요청이 오면 제공한 파일의 내용은 10년 동안 변하지 않는다고 힌트를 주자. 아파치 HTTP 서버의 경우 다음과 같은 설정을 추가한다.


ExpiresActive On
ExpiresDefault "access plus 10 years"


FilesMatch 지시자를 사용했는데 Location 지시자로 특정한 디렉토리에 있는 js, css 확장자 파일에만 Expires 헤더를 지정할 수도 있다.

Expires 헤더를 지정하니 캐시는 잘 된다. 하지만 문제가 하나 생겼다. 서버의 파일을 수정해도 브라우저 캐시 때문에 브라우저가 요청을 하지 않아 내용이 수정된 파일이 배포되지 않는 문제가 발생한다. 브라우저가 가져가게 하려면 캐시에서 찾지 못해 새로운 요청을 하도록 URL을 변경해야 한다. 즉 파일을 수정하면 파일 이름도 변경해주어야 한다. 내용이 바뀔 때마다 파일 이름을 변경하는 작업은 귀잖고 잊기도 쉽다. 빌드 시에 파일 이름에 버전을 붙여 줄 수 있지만 이는 빌드 과정을 복잡하게 만든다. 같은 효과를 내는 다른 방법이 없을까?

버전 붙은 파일 이름 사용하기


아파치 HTTP 서버의 mod_rewrite 모듈을 사용하면 원하는 URL을 원하는 자원에 연결할 수 있다. HTML에서 버전 붙은 파일 이름을 요청하도록 하고 mod_rewrite로 버전을 떼어 내고 제공할 파일에 연결 해주면 된다. HTML에서 버전을 바꾸기 전까지는 브라우저에 캐시된 파일을 사용하게 되고, 파일 내용을 수정한 후 HTML에서 버전만 바꾸면 브라우저는 캐시에 없으니 서버에 새로운 요청을 하고 서버는 수정된 파일을 제공한다.

js나 css 디렉토리에 js나 css 확장자를 가진 파일에 대한 요청이 들어오면 버전 부분을 떼어 주는 규칙을 추가한다. 파일의 원래 이름에 1.2.3과 같은 버전이 붙어 있을 경우까지 같이 처리하려면 정규 표현식을 적당히 수정해야 한다.

RewriteEngine on
RewriteRule ^/(js|css)/(.+)\.(.+)\.(js|css)$ $1.$2.$4 [L]

그럼 파일 내용이 수정될 때마다 모든 HTML을 돌아다니며 버전 식별자도 바꿔줘야할까? 프로그래머는 그러지 않는다.

파일 수정 시각을 버전으로 사용하기


파일 내용을 수정한 후 두 가지 작업이 필요하다. 첫 째 새 버전의 식별자를 정한다. 둘 째 HTML에서 참조 되는 구 버전 식별자를 새 버전 식별자로 모두 바꾼다. 첫 번째 부터 자동화 해보자. 파일의 내용이 바뀌면 같이 바뀌는 버전 식별자를 만들 수 있는 방법이 무엇이 있을까? MD5, SHA1 등의 해쉬가 있겠지만 파일 내용을 다 읽어서 계산해야 하는 문제가 있다. 그냥 간단하게 파일 시스템의 파일 마지막 수정 시각을 사용하자.

두 번째는 조금 어렵다. HTML에서 참조하는 URL을 변경해줘야 한다. 에디터나 유닉스 유틸리티를 이용해서 모든 HTML을 파일 버전 부분을 한 번에 바꿔치기할 수 있다. 다행인지 불행인지 요즘 웹어플리케이션은 대부분 HTML을 동적으로 생성한다. HTML이 동적으로 생성될 때 파일의 수정 시각을 읽어서 파일 이름에 붙여주면 된다. 환경에 따라 각기 다른 방법이 있겠지만, JSP 커스텀 태그로 만들었다. 처음 만든 JSP 커스텀 태그니 태그 만드는 방법은 다른 자료를 참고하는 것이 좋다.

import java.io.File;
import java.io.IOException;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.Tag;

public class VersionedStaticTag implements Tag {
private PageContext pageContext;
private Tag parent;
private String path;

public void setPageContext(PageContext pageContext) {
this.pageContext = pageContext;
}

public void setParent(Tag parent) {
this.parent = parent;
}

public Tag getParent() {
return parent;
}

public void setPath(String path) {
this.path = path;
}

public int doStartTag() throws JspException {
try {
long lastModified = new File(pageContext.getServletContext().getRealPath(path)).lastModified();
pageContext.getOut().print(getVersionedPath(path, lastModified));
} catch (IOException ioe) {
throw new JspException("Error: IOException while writing to client" + ioe.getMessage());
}
return SKIP_BODY;
}

public int doEndTag() throws JspException {
return EVAL_PAGE;
}

public void release() {
}

String getVersionedPath(String path, long version) {
int dotPos = path.lastIndexOf('.');
return path.substring(0, dotPos) + '.' + version + path.substring(dotPos);
}
}

만든 태그를 사용해서 HTML에서 JS와 CSS 참조 부분을 바꿔준다.

<script src="<w:versionedStatic path="/js/jquery-1.2.2.js" />" type="text/javascript"></script>
<script src="<w:versionedStatic path="/js/jsonrpc.js" />" type="text/javascript"></script>

제대로 동작하는지 HTML 소스와 FireBug, YSlow로 확인한다.

<script src="/js/jquery-1.2.2.1200532531000.js" type="text/javascript"></script>
<script src="/js/jsonrpc.1200013694000.js" type="text/javascript"></script>


문제점


여러 대의 웹서버 농장을 운영할 경우 서비스 중단 시간을 줄이기 위해서 새 버전을 모든 서버에 한 번에 배포하지 않고 조금씩 차례로 배포 하기도 한다. 이렇게 한 서비스에 새 버전 HTML과 JavaScript(이하 JS)를 제공하는 서버와 구 버전 HTML과 JS를 제공하는 서버가 공존하게 되면 서로 버전의 HTML과 JS가 조합되어 문제가 발생할 수 있다(고 들었다).

그림 1
새 HTML→(새 JS 요청)→구 웹서버
구 JS←(구 JS 제공)←

그림 2
구 HTML→(구 JS 요청)→새 웹서버
새 JS←(새 JS 제공)←

그림 3
구 HTML→(구 JS 캐시에서 찾음)
구 JS←

새 HTML과 구 JS파일 조합(그림 1), 구 HTML과 새 JS파일 조합(그림 2) 이렇게 두 가지 경우 버전이 다른 HTML과 JS가 조합되어 오류가 발생할 수 있다. 일단 구 HTML은 새 JS파일을 가지고 있는 새 버전 서버에 요청할 가능성이 낮다. 구 HTML이 요구하는 JS파일은 이미 브라우저에 캐시되어 있을 가능성이 높기 때문이다(그림 3).

그림 4
새 HTML→(새 JS 요청)→구 웹서버
404←(404: 새 JS 파일 없음)←

서로 다른 버전의 HTML과 JS파일 조합이 발생하는 상황을 막기 위해 JS파일을 각 버전 별로 다른 파일로 저장하고 구 버전 파일도 계속 남겨 놓을 수 있다. 하지만 이 경우에도 새 HTML이 구 JS파일만을 담고 있는 서버에 새 JS파일을 요구하는 경우가 생기고 아직 새 버전이 배포되지 않은 구 버전 서버는 새 JS파일이 없으므로 404 에러를 돌려준다(그림 4).

버전 별로 JS파일을 남기는 방법도 여전히 문제가 있다. 조사해보지 않았으므로 추측일 뿐이기는 하지만 오류 발생 확률이 크게 줄어 들지 않을 것 같지 않다. 낮은 확률로 발생하는 문제라도 무시할 수 없는 상황이라면 동일한 브라우저에서 오는 요청은 한 서버로만 처리되게 하거나, 별도의 정적 자원을 제공하는 서버를 두고 새 JS파일을 미리 배포해 놓거나, 개발자가 상위, 하위 호환성을 고려하여 개발하는 방법(젠장)을 사용할 수도 있다. 그냥 간단하게 파일 수정 시각만으로 캐시 조절하는 방법이면 충분하지 않을까?

참조


http://particletree.com/notebook/automatically-version-your-css-and-javascript-files/ Automatically Version Your CSS and JavaScript Files by Kevin Hale: 이 글은 헤일씨가 쓴 글의 내용과 거의 같다.


참신한 내용은 아니지만 생각을 정리하여 글로 쓰는 것도 쉽지 않네요. Rails, django 등에서는 그냥 되는 기능 어렵게 써야 하는 주류 웹프레임웍 사용하는 불쌍한 자바 개발자들을 위한 UrlRewriteFilter와 jabsorb 사용법도 대기 중입니다 훼훼훼훼훼.

이 글과 관련있는 글을 자동검색한 결과입니다 [?]

by 이피 | 2008/04/21 00:54 | 트랙백 | 덧글(5)

트랙백 주소 : http://colus.egloos.com/tb/4305660
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by anne at 2008/04/21 09:25
직접 쓴거? 저 문제점은 아직도 있구나. 내가 아는 내용이 나오니 신기;;
Commented by 이피 at 2008/04/21 16:54
내가 아니면 누가 이렇게 잘 썼겠냐
Commented by anne at 2008/04/21 21:20
잘 썼다고 한 적은 없는데.
Commented by 이피 at 2008/04/22 00:41
고맙네.
Commented by anne at 2008/04/23 23:36
뭘또;; ㅋㅋ

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶