캐시를 켜도 로그인한 사람에게는 소용이 없었다
캐시를 붙이고 나면 사이트가 빨라졌다고 생각하기 쉽습니다. 저도 그랬습니다. 그런데 로그인한 상태로 들어가 보면 이야기가 달랐습니다. 로그인 상태에서 응답이 2초를 넘는 구간이 반복해서 나왔습니다. 캐시가 안 듣는 것이 아니라 애초에 로그인 사용자에게는 캐시를 쓰지 않도록 되어 있었던 것입니다. 그게 맞는 설계라고 여겼던 것이 문제였습니다. 이 글은 그 전제를 의심하기까지, 그리고 중간에 한 번 실패하고 방향을 바꾸기까지의 기록입니다.
로그인하면 캐시가 통째로 비켜간다
워드프레스에 캐시를 붙이면 보통 로그인하지 않은 방문자에게만 적용됩니다. 로그인한 사용자는 화면에 자기 이름이 나오고 관리 막대가 뜨고 댓글 폼이 다르게 보이기 때문에, 저장해 둔 HTML을 그대로 내주면 남의 화면을 보게 됩니다. 그래서 로그인 쿠키가 있으면 캐시를 건너뛰고 매번 새로 만듭니다.
| 비로그인 | 로그인 | |
|---|---|---|
| 캐시 사용 | 사용 | 건너뜀 |
| HTML 생성 | 저장된 것 사용 | 매 요청마다 새로 |
| 댓글 폼 | 고정 | 사용자에 따라 다름 |
| 댓글 목록 | 포함된 채 저장 | 매번 다시 만듦 |
여기까지는 당연해 보입니다. 저도 사용자 상태가 다르니 HTML을 공유하면 안 된다고 생각했습니다. 문제는 이 구조에서 로그인 요청이 쌓일 때입니다. 캐시를 쓰지 않는 요청이 누적되면서 서버가 처리해야 할 양이 계속 늘고, 그 결과가 2초 넘는 응답으로 나타났습니다.
관리자 한 명이 글을 쓰는 사이트라면 이 비용은 눈에 안 띕니다. 회원이 로그인해서 둘러보는 사이트라면 이야기가 다릅니다. 로그인 사용자 비중이 늘수록 캐시가 덮어 주는 범위가 줄어드는데, 정작 사이트는 “캐시를 켜 뒀으니 괜찮다”고 믿고 있습니다.
부분만 캐시에서 빼려다 실패했다
처음 잡은 해법은 페이지 전체가 아니라 댓글 영역만 캐시에서 빼는 것이었습니다. 페이지의 나머지는 누구에게나 같으니 저장해 두고, 사람마다 달라지는 부분만 실시간으로 채워 넣는 방식입니다. 이론상 가장 손해가 적습니다.
이 방식을 밀고 가다 접었습니다. 서버 설정과 CDN 환경에 따라 결과가 달라졌기 때문입니다. 한 곳에서 맞춰 놓으면 다른 곳에서 다르게 동작했고, 하나의 방식으로 통일하려 해도 어떤 상황에서는 결국 다시 나눠서 처리해야 했습니다. 고쳐야 할 곳이 사이트 안이 아니라 사이트 밖에 흩어져 있었습니다.
환경마다 결과가 달라진다는 것의 의미
이게 왜 문제냐면, 고쳐 놓고도 고쳐졌는지 확신할 수 없기 때문입니다. 내 서버에서 확인한 동작이 CDN을 거치면 달라지고, CDN 설정을 맞추면 다른 캐시 계층에서 또 달라집니다. 어느 계층이 어떤 규칙으로 판단하는지가 제품마다 다르고, 그 규칙은 내가 바꿀 수 없는 경우가 많습니다.
한 군데만 쓰는 사이트라면 맞춰 놓고 끝낼 수 있습니다. 그러나 나중에 호스팅을 옮기거나 CDN을 바꾸면 그때 다시 무너집니다. 설정으로 푸는 해법은 그 설정이 살아 있는 동안만 유효합니다. 그래서 설정에 기대지 않는 구조로 방향을 바꿨습니다.
댓글 기능 자체를 페이지에서 떼어냈다
결론은 부분을 빼는 것이 아니라 아예 나누는 것이었습니다. 페이지는 누가 보든 항상 같은 HTML을 내보냅니다. 사용자 정보와 댓글은 페이지가 뜬 뒤에 따로 요청해서 채웁니다.
| 구분 | 전 | 후 |
|---|---|---|
| 페이지 HTML | 사용자마다 다름 | 모두 동일 |
| 캐시 적용 | 비로그인만 | 전원 |
| 댓글 목록 | 페이지 생성 시 함께 | 별도 요청 |
| 사용자 정보 | HTML에 포함 | 별도 요청 |
이렇게 하면 로그인 여부가 페이지 생성에 끼어들지 않습니다. 캐시 계층 입장에서는 모든 요청이 같은 문서를 원하는 요청이라 고민할 것이 없습니다. CDN이 무엇이든, 호스팅이 어디든 판단 기준이 하나뿐이라 환경을 타지 않습니다.
페이지가 모두에게 같아진다는 뜻
대신 포기하는 것이 있습니다. 첫 화면에는 댓글이 없습니다. 댓글은 뒤이어 들어오므로 아주 잠깐 빈 자리가 보입니다. 로그인한 사용자 이름도 마찬가지입니다. 이 간격을 그대로 두면 화면이 덜컥거리므로, 댓글이 들어올 자리를 미리 잡아 두는 편이 낫습니다.
검색 쪽도 생각해야 합니다. 댓글을 나중에 불러오면 처음 내려가는 HTML에는 댓글 글자가 없습니다. 댓글이 본문을 보완하는 종류의 사이트라면 이 점이 손해일 수 있습니다. 반대로 댓글이 한두 줄짜리 인사인 사이트라면 잃을 것이 거의 없습니다. 이건 사이트 성격에 따라 갈립니다.
댓글을 따로 받아 오면 그 요청 자체는 캐시를 타지 않습니다. 다만 이 요청은 페이지 전체를 만드는 것이 아니라 댓글만 내주면 되므로 글 한 편을 여는 데 드는 쿼리보다 훨씬 가볍습니다. 무거운 일을 전부에게서 걷어 내고 가벼운 일만 남긴 셈입니다.
페이지에 남겨 두면 안 되는 것들
페이지를 모두에게 같은 HTML로 만들기로 했다면, 사람마다 달라지는 것이 그 HTML에 하나라도 남아 있으면 안 됩니다. 하나라도 남으면 그 하나 때문에 캐시된 화면이 남의 정보를 보여 줍니다. 바꾸는 작업의 대부분은 사실 이걸 골라내는 일이었습니다.
| 페이지에 있던 것 | 어디로 |
|---|---|
| 로그인 사용자 이름·인사말 | 페이지가 뜬 뒤 따로 채움 |
| 관리 막대 | 로그인 확인 후 붙임 |
| 댓글 작성 폼 | 댓글과 함께 별도 요청 |
| 보안 토큰 | 댓글 요청 시점에 발급 |
| 권한에 따라 달라지는 버튼 | 따로 확인해 표시 |
가장 조심할 것은 보안 토큰입니다. 댓글을 보내거나 좋아요를 누를 때 쓰는 일회성 값인데, 이걸 페이지 HTML에 넣어 둔 채로 캐시하면 저장된 순간의 값이 모두에게 나갑니다. 시간이 지나면 만료돼서 동작하지 않고, 만료 전이라면 남의 값으로 요청이 나가게 됩니다. 이 값은 반드시 페이지와 분리해 필요한 시점에 받아 와야 합니다.
권한에 따라 보이고 안 보이는 버튼도 같습니다. 수정이나 삭제 버튼을 HTML에 조건부로 넣어 두면, 권한이 있는 사람이 본 화면이 저장돼 다른 사람에게 나갈 수 있습니다. 눌러도 서버에서 막히니 큰 사고로 이어지지는 않지만, 없어야 할 버튼이 보이는 것 자체가 문제입니다.
비공개 글이나 회원 전용 영역이 있다면 그 페이지는 이 구조에서 빼는 편이 낫습니다. 공개 여부가 사람마다 갈리는 문서는 애초에 공유할 수 없습니다. 모두에게 같은 HTML을 준다는 전제가 성립하는 페이지에만 적용해야 합니다.
쓰기 전에 확인할 것
이 방식이 항상 맞지는 않습니다. 로그인 사용자가 거의 없는 사이트라면 얻는 것이 적습니다. 캐시가 이미 대부분의 요청을 덮고 있으니 굳이 구조를 바꿀 이유가 없습니다. 반대로 회원 로그인이 잦은 사이트, 특히 댓글이 활발한 사이트라면 효과가 큽니다.
바꾸기 전에 먼저 재 보시기를 권합니다. 로그인한 상태와 로그아웃한 상태로 같은 글을 열어 응답 시간을 비교하면 차이가 바로 보입니다. 둘이 비슷하다면 캐시가 원래 안 걸리고 있었다는 뜻이고, 로그인 쪽만 느리다면 이 글의 상황입니다. 숫자를 보고 시작해야 바꾼 뒤에 나아졌는지도 말할 수 있습니다.
그리고 바꾼 뒤에는 서버 로그를 한 번 보시는 편이 좋습니다. 댓글을 따로 받아 오도록 바꾸면 그 요청 경로가 새로 생기는데, 주소를 잘못 잡았거나 권한이 막혀 있으면 화면에는 댓글이 안 보이는 것으로만 나타나고 원인은 로그에만 남습니다.