앞 편에서 렌더링 성능의 여러 국면을 살펴보며, 첫 화면을 앞당기는 핵심이 필요한 자원을 빨리 손에 넣는 데 있음을 짚었다. 자원을 빨리 얻는 가장 확실한 방법은 애초에 다시 받지 않는 것이다. 한 번 받아 둔 것을 재사용하면, 이름 조회부터 연결과 전송까지 이어지는 긴 여정을 통째로 건너뛸 수 있다. 이번 편은 브라우저가 자원을 저장해 두고 재사용하는 여러 층의 저장 구조, 곧 캐시 계층을 다룬다.
캐시는 단순히 속도를 높이는 장치가 아니라, 신선함과 속도 사이의 긴장을 다루는 장치다. 오래된 것을 재사용하면 빠르지만 낡은 내용을 보여줄 위험이 있고, 매번 새로 받으면 신선하지만 느리다. 이 긴장을 어떻게 조율하느냐가 캐시를 다루는 일의 본질이다. 여러 층의 캐시가 각각 이 조율을 어떻게 하는지를 알면, 왜 어떤 변경은 즉시 반영되고 어떤 변경은 한참 뒤에야 보이는지가 이해된다.
다시 받지 않는다는 이득
어떤 자원을 처음 받을 때는 이름을 숫자 주소로 바꾸고, 서버와 연결을 맺고, 요청을 보내 응답을 받는 긴 과정을 거친다. 이 과정에는 여러 번의 오고 감과 그에 따른 시간이 든다. 그런데 같은 자원을 이미 받아 둔 적이 있다면, 이 모든 과정을 건너뛰고 저장된 것을 곧바로 꺼내 쓸 수 있다. 이것이 캐시가 주는 근본적인 이득이다.
이 이득은 속도에 그치지 않는다. 다시 받지 않으면 통신망을 오가는 데이터가 줄어, 사용자의 데이터 사용량이 절약되고 서버의 부담도 가벼워진다. 특히 여러 페이지에서 공통으로 쓰이는 자원을 한 번만 받아 두고 계속 재사용하면, 페이지를 옮겨 다녀도 그 자원을 다시 받지 않아 이동이 매끄러워진다. 캐시는 사용자와 서버 양쪽에 이득을 준다.
그러나 이 이득에는 대가가 따른다. 저장해 둔 것이 낡을 수 있다는 위험이다. 서버의 자원이 바뀌었는데 브라우저가 옛 저장본을 계속 재사용하면, 사용자는 낡은 내용을 보게 된다. 그래서 캐시는 무엇을 얼마나 오래 저장해도 좋은지, 저장본이 아직 쓸 만한지를 어떻게 확인할지를 함께 정해야 한다. 이 규칙이 없는 캐시는 빠르지만 믿을 수 없다.
이 신선함과 속도의 조율을 위해 여러 장치가 마련되어 있다. 저장본을 얼마 동안 그대로 믿어도 좋은지를 정하는 유효기간, 그 기간이 지난 뒤 저장본이 여전히 유효한지를 서버에 가볍게 물어 확인하는 재검증, 그리고 자원이 바뀌었을 때 그것이 다른 것임을 알아채게 하는 표식이 그것이다. 이 장치들이 어떻게 맞물리는지가 캐시 동작의 핵심이다.
얼마나 믿고 언제 다시 묻는가
서버는 자원을 내려줄 때, 이 자원을 얼마 동안 그대로 재사용해도 좋은지를 함께 알려줄 수 있다. 브라우저는 이 기간 동안에는 서버에 다시 묻지 않고 저장본을 곧바로 꺼내 쓴다. 이 구간에서는 통신이 전혀 일어나지 않으므로 재사용이 가장 빠르다. 유효기간을 넉넉히 주면 그만큼 다시 묻는 일이 줄어 부담이 가벼워진다.
유효기간이 지나면 저장본을 곧바로 버리는 것이 아니라, 그것이 아직 쓸 만한지를 서버에 가볍게 물어 확인한다. 브라우저는 저장본에 딸린 표식을 서버에 보내며 이것이 여전히 최신인지 묻고, 서버는 바뀐 것이 없으면 내용을 다시 보내는 대신 그대로 써도 좋다는 짧은 답만 돌려준다. 이 재검증은 전체 자원을 다시 받는 것보다 훨씬 가볍다.
이 표식은 두 가지 방식으로 만들어진다. 하나는 자원이 마지막으로 바뀐 시각이고, 다른 하나는 자원의 내용으로부터 계산한 짧은 지문이다. 브라우저는 이 표식을 저장해 두었다가 재검증할 때 함께 보낸다. 서버는 지금의 자원과 이 표식을 비교해, 같으면 바뀌지 않았다고 답하고 다르면 새 자원을 내려준다. 내용 지문 방식은 시각 방식보다 정밀해, 내용이 실제로 같은지를 정확히 가린다.
유효기간을 얼마로 정할지는 자원의 성격에 따라 갈린다. 자주 바뀌는 자원은 유효기간을 짧게 주어 신선함을 지키고, 거의 바뀌지 않는 자원은 길게 주어 재사용을 극대화한다. 이 판단을 자원마다 세심하게 하면, 신선해야 할 것은 신선하게 유지하면서도 안정적인 것은 오래 재사용하는 균형을 이룰 수 있다.
이름을 바꿔 새것임을 알리기
거의 바뀌지 않는 자원은 유효기간을 아주 길게 주어 오래 재사용하고 싶다. 그런데 이렇게 하면 정작 그 자원을 바꿔야 할 때 문제가 생긴다. 브라우저가 긴 유효기간 동안 옛 저장본을 계속 믿어 버려, 바뀐 내용이 사용자에게 한참 동안 전달되지 않기 때문이다. 오래 재사용하려는 욕심과 바꿨을 때 즉시 반영하려는 욕심이 충돌한다.
이 충돌을 푸는 널리 쓰이는 방법은 자원의 이름에 내용을 반영한 표식을 넣는 것이다. 자원의 내용이 바뀌면 이름도 함께 바뀌도록 해 두면, 바뀐 자원은 브라우저에게 전혀 다른 새 자원으로 보인다. 브라우저는 옛 이름의 저장본과 무관하게 새 이름의 자원을 처음 받는 것으로 여겨 곧바로 내려받는다. 옛것을 오래 재사용하면서도 새것을 즉시 반영하는 두 욕심을 동시에 채우는 것이다.
이 방식이 성립하려면 자원을 부르는 쪽의 주소도 함께 바뀌어야 한다. 자원의 이름이 바뀌었는데 그것을 가리키는 문서가 옛 이름을 그대로 부르고 있으면 소용이 없다. 그래서 문서는 유효기간을 짧게 주어 항상 최신으로 유지하고, 그 문서가 가리키는 무거운 자원들은 이름에 표식을 넣어 오래 재사용하는 구성이 흔히 쓰인다. 자주 바뀌는 얇은 문서가 오래가는 무거운 자원들의 최신 이름을 늘 알려 주는 셈이다.
이 구성의 이점은 자원을 바꿨을 때 반영이 즉각적이면서도 안전하다는 것이다. 새 이름의 자원이 준비된 뒤에 문서가 그 이름을 가리키게 바뀌므로, 전환 도중에 옛 자원과 새 자원이 어긋나 뒤섞이는 일도 없다. 이름을 바꿔 새것임을 알리는 이 방법은 긴 유효기간의 위험을 없애면서 그 이득만 취하는 정석으로 자리 잡았다.
여러 곳에 흩어진 저장소
캐시는 브라우저 한 곳에만 있는 것이 아니라 자원이 오가는 길목 여러 곳에 흩어져 있다. 브라우저 안에도 서로 다른 성격의 저장소가 여럿 있다. 방금 쓴 자원은 빠르게 꺼낼 수 있는 임시 저장소에 두어 곧바로 재사용하고, 오래 두고 쓸 자원은 전원을 꺼도 남는 저장소에 보관한다. 앞의 것은 빠르지만 오래 남지 않고, 뒤의 것은 조금 느려도 지속된다.
브라우저 바깥에도 저장소가 있다. 사용자와 서버 사이의 길목에 놓여, 여러 사용자가 공통으로 찾는 자원을 대신 저장해 두었다가 내려주는 중간 저장소가 대표적이다. 이 중간 저장소는 사용자와 가까운 곳에 자원을 두어, 먼 서버까지 가지 않고도 빠르게 자원을 받게 한다. 특히 여러 지역에 흩어진 사용자를 상대할 때 이 방식이 큰 이득을 준다.
이렇게 여러 층의 저장소가 있으면, 자원을 찾을 때 가까운 층부터 차례로 뒤진다. 브라우저 안에 있으면 즉시 꺼내 쓰고, 없으면 중간 저장소에 묻고, 그것도 없으면 비로소 원래 서버까지 간다. 가까운 층에서 찾을수록 빠르므로, 자주 쓰이는 자원일수록 가까운 층에 남아 빠르게 재사용된다. 이 구조는 앞서 이름 조회에서 본 여러 층의 저장소와 같은 원리다.
다만 저장소가 여러 곳에 흩어져 있다는 것은 자원을 바꿨을 때 그 모든 곳의 옛 저장본을 함께 처리해야 함을 뜻한다. 어느 한 층에 옛것이 남아 있으면 그 층을 거치는 사용자는 낡은 내용을 받는다. 그래서 앞서 다룬 이름에 표식을 넣는 방법이 여기서도 빛을 발한다. 이름이 바뀌면 모든 층이 그것을 새 자원으로 보아 자동으로 내려받으므로, 흩어진 저장소를 일일이 비울 필요가 없다.
저장하지 말아야 할 것
캐시가 속도를 준다고 해서 모든 것을 저장해도 되는 것은 아니다. 어떤 응답은 저장해 재사용하면 오히려 위험하거나 잘못된 결과를 낳는다. 무엇을 저장하고 무엇을 저장하지 말아야 하는지를 가리는 것도 캐시를 다루는 중요한 부분이다.
먼저 사용자마다 다른 개인화된 응답은 함부로 저장하면 안 된다. 어떤 사용자에게 맞춘 응답을 저장했다가 다른 사용자에게 그대로 내주면, 한 사람의 정보가 다른 사람에게 노출된다. 특히 여러 사용자가 공유하는 중간 저장소에 이런 개인화된 응답이 저장되면 그 노출의 범위가 넓어진다. 그래서 개인화된 응답에는 그것을 공유 저장소에 두지 말라는 표시를 분명히 해 두어야 한다.
로그인 뒤에만 보이는 민감한 내용도 마찬가지다. 이런 내용이 저장되어 남으면, 로그아웃한 뒤에도 저장본을 통해 그 내용이 다시 드러날 수 있다. 공용 기기에서 이런 일이 벌어지면 다음 사용자가 이전 사용자의 민감한 화면을 보게 된다. 민감한 응답에는 저장 자체를 막는 표시를 걸어, 재사용될 여지를 없애야 한다.
매 요청마다 달라져야 하는 응답도 저장 대상이 아니다. 지금 이 순간의 상태를 반영해야 하는 응답을 저장해 재사용하면, 사용자는 이미 지나간 옛 상태를 본다. 이런 응답은 저장하지 않고 매번 새로 받아야 한다. 무엇이 저장해도 되는 안정적인 것이고 무엇이 매번 새로워야 하는 것인지를 나누는 판단이 필요하다.
저장을 결정할 때 함께 고려할 것은 저장한 것을 나중에 비우기가 어렵다는 점이다. 앞서 다룬 대로 저장소는 여러 곳에 흩어져 있어, 한번 저장된 것을 모든 곳에서 되돌리기가 까다롭다. 그래서 저장은 신중하게 결정하되, 저장한 것을 바꿔야 할 때를 대비해 이름에 표식을 넣는 방법을 함께 써 두는 것이 안전하다. 잘못 저장한 것을 뒤늦게 거두는 것보다, 처음부터 저장 여부를 옳게 정하는 편이 훨씬 값싸다.
결국 저장 여부의 판단은 두 질문으로 요약된다. 이 응답이 사용자마다 달라야 하는가, 그리고 시간이 지나도 같은가다. 사용자마다 다르거나 민감한 응답은 공유 저장소에 두지 않고, 매 순간 달라져야 하는 응답은 아예 저장하지 않으며, 여러 사용자에게 두루 같고 오래 안정적인 응답만 마음 놓고 저장한다. 이 구분을 자원마다 분명히 해 두는 것이 빠르면서도 안전한 캐시의 바탕이다.
스크립트가 가로채는 저장소
지금까지의 캐시는 브라우저가 정해진 규칙에 따라 자동으로 다루는 것이었다. 여기에 더해, 스크립트가 직접 통제하는 저장소도 있다. 페이지와 서버 사이에 자리 잡고 오가는 요청을 가로채, 무엇을 저장하고 무엇을 다시 받을지를 스크립트가 정하는 방식이다. 이 저장소는 자동 캐시보다 훨씬 세밀한 통제를 가능하게 한다.
이 방식의 큰 이점은 통신망이 끊긴 상황에서도 저장해 둔 자원으로 화면을 띄울 수 있다는 것이다. 스크립트가 필요한 자원을 미리 저장해 두면, 나중에 연결이 없어도 그 저장본으로 페이지를 구성한다. 자동 캐시가 속도를 위한 것이라면, 이 방식은 연결이 없는 상황까지 대비하는 것이다. 이 오프라인 대응은 뒤의 편에서 따로 깊이 다룬다.
세밀한 통제가 가능한 만큼 다루기도 까다롭다. 무엇을 저장하고 언제 새로 받을지를 스크립트가 직접 정하므로, 그 규칙을 잘못 짜면 옛 자원을 계속 내주어 사용자가 낡은 화면에 갇히는 사고가 생긴다. 자동 캐시는 정해진 규칙이 안전망 역할을 하지만, 직접 통제하는 저장소는 그 안전망까지 스스로 만들어야 한다. 강한 통제에는 그만한 책임이 따른다.
정리하면, 브라우저는 자원을 여러 층의 저장소에 두고 재사용해 다시 받는 비용을 없애며, 유효기간과 재검증과 내용 표식으로 신선함과 속도를 조율한다. 오래가는 자원은 이름에 표식을 넣어 새것임을 알리고, 스크립트가 직접 통제하는 저장소는 연결이 없는 상황까지 대비한다. 캐시를 다루는 일은 결국 무엇을 얼마나 믿고 언제 다시 확인할지를 자원마다 정하는 일이다. 다음 편에서는 이렇게 자원을 저장하는 것과는 다른 결의 저장, 곧 사용자와 관련된 데이터를 브라우저에 담아 두는 쿠키와 스토리지를 다룬다.