![[실전 성능 11] 사용자 곁에서 응답하기](https://img.thenullpage.com/posts/5765/5765_1_f88413.webp)
지난 편에서 사용자에게 보내는 코드를 줄이는 법을 다뤘다. 그런데 아무리 가볍게 만들어도 사용자와 서버가 물리적으로 멀면 느렸다. 데이터가 지구를 왕복하는 시간은 코드로 줄일 수 없기 때문이다. 이번 편은 사용자 가까운 곳에서 응답하게 하는 엣지 캐시와, 그것이 첫 바이트 시간을 어떻게 줄이는지에 대한 이야기다.
1. 거리가 만드는 지연
내가 처음 간과했던 건 물리적 거리였다. 서버가 한 곳에만 있으면, 그 서버에서 먼 지역의 사용자는 요청과 응답이 오가는 것만으로도 시간이 걸렸다. 빛의 속도는 유한하고 네트워크는 여러 중계를 거치니, 멀수록 왕복 시간이 길어졌다. 아무리 서버가 빨리 처리해도 이 왕복 시간은 고정 비용처럼 붙었다.
이 지연은 첫 바이트 시간에 직접 반영됐다. 사용자가 요청을 보내고 서버의 첫 응답 조각이 도착하기까지의 시간에 왕복 지연이 그대로 더해졌다. 앞서 로딩 편에서 첫 바이트 시간이 로딩 지표의 가장 큰 지렛대라고 했는데, 그 첫 바이트를 늦추는 숨은 원인 중 하나가 바로 이 거리였다. 처리 속도만 봐서는 안 보이는 병목이었다. 나는 서버 처리 시간만 들여다보다가 한동안 이 지연의 정체를 못 찾았는데, 지역별 현장 데이터를 갈라 보고 나서야 특정 지역 사용자만 유독 첫 바이트가 느린 패턴을 발견하고 거리를 의심하게 되었다.
더 나쁜 건 이 왕복이 한 번으로 안 끝날 때가 많다는 점이었다. 연결을 맺고, 보안 절차를 거치고, 요청을 보내는 과정에서 왕복이 여러 번 일어났다. 거리가 멀면 이 여러 번의 왕복이 각각 길어져서 지연이 쌓였다. 그래서 먼 지역 사용자는 첫 화면을 보기까지 유독 오래 기다렸고, 나는 그들의 현장 데이터에서 이 차이를 확인했다.
이 문제는 서버 코드를 최적화해서는 풀리지 않았다. 처리 시간을 0으로 만들어도 왕복 지연은 그대로였기 때문이다. 해법은 다른 층위에 있었다. 처리를 빨리 하는 게 아니라, 응답하는 위치 자체를 사용자 가까이로 옮기는 것이었다. 거리를 줄이면 왕복 지연이 줄고, 그러면 첫 바이트가 빨라졌다. 코드로 못 줄이는 것을 인프라 구조로 줄이는 접근이었고, 나는 이 지점에서 성능이 코드만의 문제가 아니라 어디서 어떻게 응답하느냐의 문제이기도 하다는 걸 배웠다.
2. 엣지라는 개념
해법의 이름은 엣지였다. 하나의 중앙 서버 대신, 세계 곳곳에 분산된 여러 지점에서 응답하게 하는 방식이다. 사용자는 자기에게 가장 가까운 지점에 연결되고, 그 지점이 응답하니 왕복 거리가 짧아졌다. 내가 개발한 사이트는 이런 엣지 네트워크 위에서 도는 구조라, 코드 자체가 사용자 가까운 곳에서 실행되는 이점이 있었다.
엣지에서 응답하면 여러 왕복이 전부 짧아졌다. 연결을 맺는 것부터 응답을 받는 것까지 모든 과정이 가까운 지점과 이뤄지니, 각 왕복의 지연이 줄었다. 특히 서버에서 먼 지역 사용자에게 이 효과가 컸다. 중앙 서버까지 갈 필요 없이 근처에서 끝나니, 예전에 오래 기다리던 사용자들의 첫 바이트가 눈에 띄게 당겨졌다.
다만 엣지에서 실행한다고 모든 게 저절로 빨라지는 건 아니었다. 엣지 지점이 응답을 만들려고 다시 멀리 있는 데이터베이스를 조회해야 한다면, 결국 그 조회에서 거리가 다시 발생했다. 코드는 가까이서 돌아도 데이터가 멀면 소용없었다. 그래서 데이터도 가까이 두거나, 아예 조회 없이 응답할 방법이 필요했다.
그 방법이 캐시였다. 자주 나가는 응답을 각 엣지 지점에 미리 저장해두면, 그 지점은 멀리 조회하지 않고 저장된 응답을 즉시 내보냈다. 사용자는 가까운 지점에서, 그것도 조회 없이 응답을 받으니 첫 바이트가 극적으로 빨라졌다. 엣지와 캐시가 만나야 비로소 진짜 효과가 났다.
3. 엣지 캐시의 동작
엣지 캐시는 이렇게 동작했다. 어떤 사용자가 특정 페이지를 처음 요청하면, 그 지점은 응답을 만들어 사용자에게 주는 동시에 그 응답을 자기 저장소에 넣어뒀다. 같은 지역의 다음 사용자가 같은 페이지를 요청하면, 이번엔 새로 만들지 않고 저장된 것을 즉시 내보냈다. 두 번째 사용자부터는 조회도 없고 왕복도 짧아 아주 빨랐다.
이 방식은 자주 요청되는 공개 페이지에 특히 잘 맞았다. 많은 사람이 보는 인기 있는 목록이나 게시글은 한 번 만들어 캐시해두면 수많은 사용자가 그 캐시로 빠르게 응답받았다. 서버는 매번 만들 필요 없이 처음 한 번만 만들면 됐다. 조회 부담도 줄고 응답도 빨라지는 일석이조였다. 나는 공개 콘텐츠에 이 캐시를 적극 적용했다.
캐시에는 유효 기간을 뒀다. 콘텐츠가 바뀔 수 있으니 무한정 캐시하면 낡은 내용을 보여줄 위험이 있었다. 그래서 짧은 기간 동안만 캐시하고, 그 기간이 지나면 다시 새로 만들게 했다. 얼마나 오래 캐시할지는 콘텐츠가 얼마나 자주 바뀌는지에 따라 정했다. 자주 바뀌면 짧게, 잘 안 바뀌면 길게 잡는 식이었다.
엣지 캐시의 좋은 점은 지점마다 독립적이라는 것이었다. 각 지역의 지점이 자기 지역 사용자의 요청을 자기 저장소로 처리하니, 한 지역의 부하가 다른 지역에 영향을 주지 않았다. 트래픽이 여러 지점으로 자연스럽게 분산됐다. 중앙 서버 한 곳에 부하가 몰리는 구조보다 훨씬 안정적이었다. 분산이 곧 성능이자 안정성이었다.
4. 캐시가 안 되는 것들
모든 응답을 캐시할 수는 없었다. 사용자마다 다른 개인화된 응답은 공유 캐시에 넣으면 안 됐다. 예를 들어 로그인한 사용자의 개인 정보가 담긴 화면을 캐시하면, 다음 사용자에게 그 화면이 잘못 나갈 수 있었다. 이건 성능 이전에 심각한 보안 문제였다. 나는 개인화된 응답은 절대 공유 캐시에 넣지 않는 선을 확실히 그었다.
실제로 이 부분에서 크게 데인 경험이 있었다. 캐시 설정을 잘못해서 로그인 사용자용 응답이 공유 캐시를 타고 다른 사람에게 노출될 뻔한 상황이었다. 다행히 발견하고 바로잡았지만, 캐시가 편리한 만큼 위험하다는 걸 뼈저리게 배웠다. 그 뒤로는 무엇을 캐시할지 정할 때 보안을 가장 먼저 따졌다. 성능보다 안전이 우선이었다.
그래서 나는 캐시 대상을 엄격히 제한했다. 로그인 정보가 없고, 개인화되지 않았고, 누가 봐도 같은 공개 콘텐츠일 때만 공유 캐시를 허용했다. 조금이라도 사용자마다 달라질 여지가 있으면 캐시하지 않았다. 이 판단은 자동으로 두면 위험해서, 캐시해도 되는 조건을 명시적으로 확인하도록 만들었다. 애매하면 캐시 안 함이 원칙이었다.
개인화된 부분과 공개된 부분을 분리하는 것도 방법이었다. 페이지 대부분은 공개 콘텐츠라 캐시할 수 있고, 사용자마다 다른 작은 부분만 따로 처리하면 됐다. 이렇게 나누니 캐시의 이점을 살리면서도 개인화를 안전하게 지킬 수 있었다. 전부 캐시하거나 전부 안 하거나가 아니라, 캐시 가능한 부분을 최대한 살리는 설계였다.
5. 거리를 지우는 효과
엣지 캐시를 제대로 적용하니 첫 바이트 시간이 확연히 좋아졌다. 특히 서버에서 먼 지역 사용자의 로딩이 크게 개선됐다. 예전에는 그들이 유독 오래 기다렸는데, 이제는 가까운 지점의 캐시에서 즉시 응답받으니 지역 간 격차가 줄었다. 어디에 있든 비슷하게 빠른 경험을 주는 것이 목표였고, 엣지 캐시가 그걸 가능하게 했다.
이 개선은 다른 최적화와 성격이 달랐다. 이미지나 코드를 줄이는 건 모두에게 고르게 이득이었지만, 엣지 캐시는 특히 멀리 있던 사용자에게 큰 이득이었다. 그동안 소외됐던 지역 사용자를 챙기는 최적화였다. 나는 현장 데이터에서 지역별 로딩을 확인하며 이 효과를 검증했다. 격차가 줄어드는 것이 숫자로 보였다.
엣지 캐시는 비용 측면에서도 좋았다. 캐시가 요청을 흡수하니 중앙 서버의 조회 부담이 줄었고, 그만큼 자원을 아꼈다. 성능과 비용이 같은 방향으로 개선되는 반가운 경우였다. 특히 트래픽이 늘수록 캐시의 가치가 커졌다. 많은 사람이 같은 콘텐츠를 볼수록 캐시 한 번의 이득이 여러 배로 돌아왔다. 한 번 만든 응답을 천 명이 재사용하면 조회 부담이 천분의 일로 줄어드는 셈이라, 인기 있는 콘텐츠일수록 캐시가 성능과 비용을 동시에 지켜주는 방패가 되어주었다.
이렇게 응답 위치를 사용자 가까이 옮기고 캐시로 조회를 없앴다. 그런데 캐시가 안 되는 개인화된 페이지나, 처음 만들어지는 응답은 여전히 빠르게 그려야 했다. 다음 편에서는 서버가 첫 화면을 미리 완성해서 보내는 서버 렌더링과, 반대로 클라이언트에서 그릴 때 빠지기 쉬운 함정을 다루겠다.