본문 바로가기

.Net Technology/UI Performance

넥슨 사이트의 성능튜닝 - #2 캐시의 활용



안녕하세요~ 암암리에 넥슨의 뒤를 캐고 있는 튜닝요원. HOONS입니다. 이전 포스트(
넥슨 사이트의 성능 튜닝 – #1 성능 분석에 앞서서)에서는 브라우저가 동작되는 기본적인 내용과 HTTP 요청과 응답에 대한 내용을 다루어 보았습니다. 그럼 이제 하나 둘씩 넥슨 사이트를 튜닝할 수 있는 기술의 실마리를 풀어 보도록 하겠습니다. 단, 넥슨을 통해서 살펴볼 부분은 UI단(Frontend)의 기술을 튜닝하는 것입니다. 때문에 메모리 부하로 웹 서버가 죽거나 하는 문제는 UI단의 기술과는 상관 없는 부분인 것입니다. (해깔리지 말라는 의미로 ^^) 

서버 단의 튜닝기술은 넥슨에 대한 포스트를 끝낸 후에 이어서 진행하도록 하겠습니다. 맨 처음 포스트에서도 다루었듯이 사이트의 성능에 문제가 있다고 판단되는 경우 그 문제가 UI단의 동작인지 서버 단의 동작인지 판단해서 튜닝을 진행해야 합니다. 그럼 먼저 캐시에 대한 이야기를 수사를 진행해 보도록 하겠습니다. 



 
캐시(Cache)의 활용
 
넥슨은 캐시를 활용하지 않고 있습니다. 보다 정확하게 말하자면 캐시에 저장되는 구성요소(이미지, 스크립트, 스타일)들에 만료기간을 지정하지 않고 있는 것입니다. 그렇다면 먼저 캐시는 무엇을까요? 캐시는 검색된 웹 페이지 정보가 저장된 메모리 또는 디스크의 임시 기억 장소를 말합니다. 즉, 넥슨 페이지를 방문한 후에 넥슨 페이지에 있던 모든 파일들은 바로 캐시에 저장하게 되는 것입니다. 때문에 사이트를 재방문 해도 그 파일이 변하지 않았다면 캐시에서 파일을 불러와 로드하기 때문에 훨씬 속도를 높일 수 있습니다.
 
그렇다면 만료기간은 무엇이고 넥슨(
www.nexon.com)에서 그걸 어쨌다는 겁니까? 

다음 화면은 넥슨 사이트에 재방문 하게 되었을 때 일어나는 동작을 요약한 화면입니다.


[넥슨 사이트를 재방문 했을 때의 통계]
 
넥슨을 재방문 해보면 이미 다운받은 파일은 받지 않고 않지만 155번의 HTTP Request가 발생하고 있다는 부분이 바로 오늘 풀어야 할 과제입니다. 이 요청은 파일은 이미 다운 받았지만 최신 파일이 맞는지 확인하기 위해서 서버에 물어보는 요청인 것입니다. 하지만 헤더의 Expires나 Cache-Control을 통하여 만료기간을 지정하게 되면 이 요청은 그 기간 동안 일어나지 않게 됩니다. 예를 들어 만료기간을 하루로 지정하였다면 하루 동안은 무조건 최신 파일이라고 가정하고 확인 요청을 서버로 보내지 않게 됩니다. 
 
다음 포스트에 다룰 내용의 주제이지만 이런 HTTP 요청을 줄이는 것이 성능을 개선하기 위해서 가장 핵심적인 부분입니다. 넥슨을 재방문 했을 때의 상태코드를 보면 대부분 304로 나오는 것을 볼 수 있습니다. 304 요청은 서버의 버전과 클라이언트가 가지고 있는 파일과 일치한다는 코드입니다. 이런 요청 자체를 주고 받지 않는다면 사이트의 속도는 엄청나게 향상됩니다. 
 
단, 여기서의 속도는 처음 방문한 사용자가 아닌 재방문일 경우일 때의 속도입니다. 넥슨 사이트는 한번 방문했던 사람이 꾸준히 방문하는 충성도 높은 유저들이 굉장히 많을 것입니다. 게임을 하는 사람들 정말 꾸준히 방문하죠. 즉, 어쩌다가 정보가 필요할 때 방문하게 되는 사용자는 그렇게 많지 않을 거라는 것입니다. 이점으로 미루어 볼 때 분명 이 작업은 상당한 가치가 있다고 생각하고 못해도 200% 이상의 성능을 끌어 올릴 수 있을 것입니다.
 
“그럼 만약 홈페이지를 수정하게 될 경우 어떻게 되나요? 이미지가 변경되었습니다. 그래서 사용자에게 그 이미지를 내려 주어야 하는데 사용자가 가지고 있는 파일은 아직 만료기간이 지나지 않았습니다.” 이런 생각을 먼저 머리 속에 그리는 분들이 있을 것 같은데요 이 부분은 맨 뒷부분에서 언급하도록 하겠습니다. 
 


Expires/Cache-Control
 
컨텐츠의 만료기간을 지정하는 방법은 Expires를 지정하는 방법과 Cache-Conrol을 이용하는 두 가지 방법이 존재합니다. Expires는 HTTP 1.0에서부터 지금까지 사용하고 있는 방법이고 Cache-Control은 HTTP 1.1에서 새롭게 탄생한 녀석입니다. 99%는 HTTP 1.1을 사용하고 있다고 보면 되지만 그래도 HTTP 1.0 사용자도 무시할 수 없기 때문에 둘 다 지정해 주는 것이 좋습니다. 여기서 먼저 만료기간을 지정하여 사용하고 있는 다음(
www.daum.net)의 로고의 응답을 받아 보도록 하겠습니다. 
 

요청(Request)
GET /top/2008/logo_daum2008.gif HTTP/1.1
 
… 생략 …
응답(Reponse)
HTTP/1.1 200 OK
Cache-Control: max-age=7776000
Expires: Mon, 02 Jun 2008 12:09:53 GMT

 
… 생략 …

 
이 파일은 Cache-Control과 Expires 둘 다 지정하여 전달하는 것을 볼 수 있습니다. Expires에 지정된 날짜까지는 확인 요청(Conditional Get Request)을 보내지 않고 무조건 캐시에 저장된 파일을 이용하게 됩니다 이 날짜가 지나게 되면 서버로 확인 요청을 보내게 되겠죠. 그럼 Cache-Control: max-age=7776000은 무엇을 의미 할까요? 이것은 7776000초가 지나지 않을 때까지 요청을 보내지 않겠다는 것입니다. 이렇게 둘 다 지정되어 있을 경우 Cache-Control이 우선권을 가지게 됩니다. 
 


성능 테스트
 
그럼 만약 넥슨 사이트의 파일에 만료기간을 지정할 경우 성능이 얼마나 향상될지 궁금하지 않습니까? 그래서 HOONS요원이 테스트를 해보도록 하겠습니다. 상당히 귀찮은 작업이지만 처음 맡겨진 임무인 만큼 최선을 다해보겠습니다.(^_^;) 먼저 HOONS의 컴퓨터를 넥슨과 비슷한 환경의 IIS서버를 구성해야 합니다. 하지만 비밀요원인 HOONS가 넥슨 사이트의 소스를 가지고 있는 것도 아닌데 어떻게 구성해야 할까요? 여기서 HOONS가 테스트하고자 하는 것은 단순히 만료기간을 지정했을 때의 그 속도를 보는 것입니다. 때문에 넥슨에서 인터넷 임시폴더로 다운받은 파일만 고대로 옮기면 되는 것입니다. 그럼 넥슨 사이트에서 다음 그림과 같이 다른 이름으로 파일을 내보내겠습니다.
 

[넥슨 페이지를 다른 이름으로 저장]
 
이제 내보낸 HTML파일과 파일 폴더를 IIS서버를 생성한 후에 옮겨 보도록 하겠습니다. 
 

[넥슨 웹 서버의 폴더]

 
150개 정도의 파일이 와야 하지만 HOONS가 입수 할 수 있었던 파일은 99개 였습니다. 나머지는 절대 경로를 이용하여 직접 요청하거나 IFrame이 있을 가능성도 있습니다. 아무튼 99개로도 충분히 성능 테스트가 가능합니다. 그럼 HOONS가 둔갑시킨 가짜 넥슨 사이트를 호출해 보도록 하겠습니다.
 

[로컬에서 최초의 요청]
 
처음 요청의 경우 1.1초가 나오는 것을 볼 수 있습니다. 로컬에서 작업하기 때문에 실제 넥슨 사이트 보다 응답속도가 빠른 것을 볼 수 있습니다. 우리가 테스트하고자 하는 것은 재방문시의 속도이기 때문에 다시 한번 호출해 보겠습니다. 두 번째 요청에서는 파일을 직접 다운로드 하지 않고 최신 파일 여부만 확인하게 됩니다.


[평범한 2번째 호출]
 
그럼 이제 캐시의 만료기간을 지정해 보도록 하겠습니다. 만료기간을 설정하기 위한 방법이 2가지가 존재합니다. 하나는 IIS에서 지정하는 방법이고 다른 하나는 닷넷의 http핸들러를 이용해서 요청을 가로챈 후에 헤더를 추가해주어 내려 보내는 방법입니다. 전자의 경우 폴더 별로 지정하거나 파일 하나하나 수동으로 지정해 줄 수 있습니다. 일단은 쉽게 IIS에서 직접 설정하는 방법으로 진행해 보도록 하겠습니다. 
 
 


[IIS에서 제공하는 기능을 이용할 경우]
 

[Cache-Control헤더에 max-age를 직접 추가할 경우]

파일 개개로 열어서 만료기간을 지정해도 되고 폴더 전체를 열어도 됩니다. HOONS는 폴더 전체에 만료기간을 설정해 보도록 하겠습니다. 그럼 이렇게 만료기간의 지정은 끝났습니다. 이제 전체 요청 수와 전체 시간을 측정해보도록 하겠습니다. (과연 결과는 두구두구..)

[만료기간을 지정했을 때의 재방문]

 
위의 화면을 보면 대략 90개의 요청이 줄어든 것을 볼 수 있습니다. 그리고 시간도 0.73초에서 0.34초로 거의 50%로 줄어든 것 볼 수 있습니다. (테스트한 보람이 있군요^^;) 나머지 60개의 요청에서 줄일 수 있는 요소들을 더 줄인다면 그만큼 성능은 향상될 수 있을 것입니다. 좀 더 정교하게 테스트를 하려면 저 60개의 요청을 빼고 했어야 했겠지만 그냥 처음 수사이니 양해 부탁 드리겠습니다.(_ _;) 테스트할 때 주의 할 점이 있습니다. [F5]를 누를 경우, 즉 페이지를 새로고침 할 경우 만료기간에 상관없이 서버로 파일이 최신 파일인지 확인하는 요청(Conditional Get Request)을 보내게 됩니다. 때문에 개발자는 만료기간이 지정되었다 하더라도 수정한 내용이 잘 반영되었는지 쉽게 확인할 수 있습니다. 
 



  
 

쉬어가는 문제

 
  


만료기간 설정 in Code
 
이제 어느 정도 감이 오십니까? 아직 머리가 알쏭달쏭 하다면 위에서부터 다시 천천히 읽어보셔야 다음 수사를 쉽게 따라올 수 있을 것입니다. 위에서 살펴본 방식은 IIS에서 설정한 부분이고 이제 코드를 이용해서 여러 파일에 캐시 만료기간을 설정해 보도록 하겠습니다. 이 작업을 위해서 닷넷의 aspnet_wp.exe에서 동작되는 httpHandler를 이용할 것입니다. 뭐; 이 개념을 알고 있다면 상관없습니다만, 닷넷에 입문한지 얼마 안되었고, ASP.NET 동작 구조가 머리 속에 잘 들어와 있지 않다면 안재우님의 강좌(
http://blog.naver.com/saltynut?Redirect=Log&logNo=120027090312)를 한번 읽어 보는 것을 추천해드립니다. (^^) HOONS요원은 성능 튜닝 임무에 충실할 예정이기 때문에 링크하나로 넘어감을 이해해 주시길 부탁 드리겠습니다 (_ _;) 
 
aspnet_isapi.dll라는 ISAPI 처리기는 일반적으로 닷넷과 관련된 요청만 받아서 처리합니다.(aspx, asmx 등의 요청들) 그렇기 때문에 이미지나 자바스크립트와 같은 파일은 ISAPI 처리가 처리하지 않도록 설정되어 있기 때문에 이 부분을 수동으로 추가해주어야 합니다. IIS 등록정보에서 구성에 확장자와 ISAPI 처리기와 매핑하는 처리가 가능합니다. 한방에 따라 할 수 있는 그림 하나 첨부합니다.
 

[이미지 확장자와 aspnet_isapi.dll과의 매핑]
 
보통 닷넷에서 이미지 작업을 할 때는 ashx라는 확장자를 이용합니다. 그러면 이렇게 추가해주지 않아도 작업이 가능합니다. 예를 들어 <img src=”hoons.jpg”>를 서버에서 처리하고 싶다면 <img src=”hoons.jpg.ashx”> 이렇게 주소를 설정하고 httphandler에서 받아 처리할 수 있습니다. 그럼 간단하게 코드를 살펴보겠습니다. 먼저 web.config에 httpHandler와 연결해주는 설정을 추가하였습니다. 

<httpHandlers>
  <!--*이미지포맷 설정*-->
  <add verb="*" path="*.gif" type="CacheControl"/>
  <add verb="*" path="*.jpg" type="CacheControl"/>
</httpHandlers>

[Web.Config 설정]
 

public class CacheControl : IHttpHandler
{
    public CacheControl()
    {    }

    #region IHttpHandler Members

    public bool IsReusable

    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        string file = context.Server.MapPath(context.Request.FilePath);//파일 경로
        string filename = file.Substring(file.LastIndexOf('\\') + 1);//파일 이름
        string extension = file.Substring(file.LastIndexOf('.') + 1);//파일 확장자

        //Cache-Control:max-age=31536000으로 설정하기 -> 365
        context.Response.Cache.SetMaxAge(new TimeSpan(365, 0, 0, 0, 0));

        //ContentType설정
        if (extension == "gif")
            context.Response.ContentType = "image/gif";
        if (extension == "jpg")
            context.Response.ContentType = "image/jpeg";

        //
화면 출력
        context.Response.AddHeader("content-disposition""inline; filename=" + filename);
        context.Response.WriteFile(file);
    }
    #endregion
}


친절한 주석으로 코드는 충분히 이해할 수 있을 것입니다. 이 프로젝트는 파일로 첨부할 것이기 때문에 다운받아서 확인해 보시기 바랍니다. 실행하면 gif와 jpg 이미지는 모두 365일의 만료기간을 설정하게 됩니다. 그럼 헤더를 살펴보도록 하겠습니다. 
 


[이미지에 동적으로 추가된 만료기간]
 
헤더를 확인해보면 Cache-Control의 max-age값이 설정되는 것을 볼 수 있습니다. 후후; 그런데 IEWatch툴의 버그가 발견되었습니다. 이상하게 코드에서 설정할 경우 Cache를 이용하고 있음이 분명함에도 전체 Request 수는 그대로 나오더군요. 전 포스트에서도 파일사이즈 버그를 포스팅 했던 것 같았는데요. 이거 장비가 이래서 수사나 제대로 할 수 있겠습니까? 참고로 HOONS는 예전부터 이 툴을 사용해왔기 때문에 30일 동안 쓸 수 있는 무료기간을 이미 다 소진했습니다. 하지만 날짜를 뒤로 돌리면 불편하지만 쓸 수 있더군요. 때문에 제 컴퓨터의 달력은 항상 뒤로 갑니다. (ㅠ_ㅠ;) 누가 가난한 HOONS요원에게 후원을.. 응? (-_-;)
 
아무튼 이 예제에서는 이미지만 처리하게 설정되었지만 이미지뿐만 아니라 자바스크립트, 스타일시트, 플래쉬 등등의 여러 파일에 몽땅 만료기간을 추가하면 성능은 그만큼 좋아지게 될 것입니다.
 


서버의 파일이 변경되었을 경우는?
 
앞에서도 언급했듯이 만료기간을 지정하면 그 기간이 지정되기 전까지 캐시에 있는 파일들을 사용하게 됩니다. 하지만 서버에서 업데이트가 있었고 더 이상 그 구성요소를 사용하지 않을 경우에 어떻게 해야 할까요? 참고로 네이버나 다음과 같은 사이트 또한 1달 이상의 만료기간을 설정하여 사용하고 있습니다. 그럼 큰 사이트는 업데이트가 거의 안되기 때문에 이렇게 지정하여 사용하고 있는 것일까요?
 
이 부분을 해결하는 방법은 간단합니다. 업데이트 될 때마다 파일 이름을 변경해주면 되는 것입니다. 예를 들면 현재날짜_시간(hoon_20080305_0123.js)과 같은 파일 포맷을 지정하여 사용하는 것입니다. 이미지나 스크립트나 모든 파일에 말입니다. 이 부분도 배포과정에서 자동화하여 처리할 수 있을 것입니다. 또 큰 서비스의 회사일수록 업데이트 빈도는 적을 것이고, 업데이트 되면 보통 새로운 파일명을 사용할 경우가 많습니다. 때문에 충분히 가치 있는 일이 될 것입니다.
 


정리
 
회사에서 저녁 먹고 7시부터 쓰기 시작했는데 예제 만들고 캡쳐하고 하다 보니 새벽 2시가 되어 가는군요. 후다닥 정리하고 찜질방 가야겠습니다. (-_-;)
 
헤더의 만료기간을 지정하면 넥슨의 경우 사이트의 속도를 절반 이상 줄일 수 있습니다. 이번 수사에서는 150개의 요청 중에 100개 정도의 요청을 줄였는데도 사이트의 성능은 2배 정도 빨라지는 것을 눈으로 확인하였습니다. 하지만 HOONS의 수사는 이게 전부가 아닌 이제부터가 시작인 것입니다. 뒤에 다루는 내용까지 모두 적용한다면 성능은 더 끌어 올릴 수 있을 것입니다. 단, UI단의 성능입니다.(^_^) 

일반적으로 서버 단에서 성능을 튜닝하기 위해서 어떻게 합니까? DB 구조 바꾸고, 코드 다시 짜고 프레임구조 바꾸고 하지 않습니까? 이런 서버 단의 작업은 쉽지 않기 때문에 굉장히 오래 걸리는 작업이고 유지보수 하는 개발자로서는 당연히 하기 싫은 일이 될 것입니다. 또 그렇게 크게 빨라진다는 효과를 눈으로 확인하기 힘듭니다. 하지만 지금 HOONS 요원이 설명한 이 방법을 맘먹고 진행하면 일주일 안에 충분히 사이트에 적용할 수 있을 것입니다. 
 
다음 포스트에서는 HTTP 요청을 줄이는 방법에 대해서 살펴볼 것입니다. 이번 예제는 한번 방문한 사람을 즉, 재방문자를 위한 튜닝이었다면 다음 시간에는 처음 방문한 사람을 위한 튜닝이라고 보면 됩니다. 부족한 글 읽어주셔서 감사합니다. (_ _;)



.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.