홈페이지나 관리자 화면이 느리다고 하면 서버를 늘리자는 제안이 먼저 나옵니다. 매달 나가는 비용이 늘고, 효과는 애매합니다.
느린 원인이 서버 용량이 아닌 경우가 있습니다. 한 화면을 그리는 동안 같은 데이터를 여러 번 읽고 있으면, 서버를 두 배로 키워도 읽는 횟수는 그대로입니다.
저희 웹사이트 성능을 점검하면서 이 구조를 찾아냈습니다. 무엇이 겹쳤는지, 어떻게 정리했는지, 정리한 뒤 무엇을 확인해야 하는지 정리합니다.
각자 정직하게 요청했는데 합치면 중복입니다
화면을 만드는 코드는 데이터가 필요한 자리에서 각자 가져옵니다. 페이지 제목을 만드는 함수가 글 정보를 읽고, 본문을 그리는 함수가 같은 글 정보를 또 읽습니다. 각 함수만 보면 자기가 필요한 것을 정직하게 요청했을 뿐입니다. 합쳐 놓으면 한 화면을 그리는 동안 같은 데이터를 여러 번 읽습니다.
점검에서 나온 항목은 성격이 조금씩 다릅니다.
| 위치 | 모양 |
|---|---|
| 상세 페이지 | 메타데이터를 만들 때와 본문을 그릴 때 같은 글을 각각 조회 |
| 상세 하단 관련 글 | 목록 전체를 다시 읽어 필터링 |
| 목록 조회 전반 | 한 화면을 그리는 동안 같은 목록 조회가 여러 번 |
세 가지 중 첫 번째만 순수한 중복입니다. 같은 함수에 같은 인자로 두 번 부르는 경우입니다. 나머지 둘은 조금 다릅니다. 관련 글을 고르려고 목록을 읽는 것은 상세 조회와 다른 함수라서 자동으로 합쳐지지 않습니다. 필요한 것은 몇 건인데 전체를 가져와 걸러내는 구조 자체를 손봐야 합니다.
같은 "중복"으로 보여도 해결 방법이 다릅니다. 비용 관점에서도 차이가 납니다. 데이터베이스 조회 횟수에 따라 요금이 매겨지는 구조라면 이 중복은 매달 청구서에 그대로 나타납니다.
캐시는 두 층으로 겹쳐 걸었습니다
바깥에 요청 단위 중복 제거를 두고, 그 안에 시간 기반 서버 캐시를 둡니다. 요청 단위 중복 제거가 먼저 걸리므로, 한 페이지를 그리는 동안 같은 함수에 같은 인자로 여러 번 호출해도 실제 실행은 한 번입니다. 그 한 번이 안쪽 캐시에 닿고, 캐시가 살아 있으면 데이터베이스까지 가지 않습니다.
두 층이 맡는 범위가 다릅니다. 요청 단위 중복 제거는 하나의 요청 안에서만 유효합니다. 방문자가 바뀌면 다시 실행됩니다. 시간 기반 캐시는 설정한 시간 동안 여러 방문자의 요청이 같은 결과를 공유합니다. 하나만 걸면 나머지 절반이 남습니다.
도식에서 관련 글 목록 조회를 따로 그린 이유가 있습니다. 다른 함수라서 위쪽 두 개와 합쳐지지 않습니다. 안쪽 캐시는 함께 쓰지만 요청 단위 중복 제거의 혜택은 받지 못합니다.
빨라진 대신 수정이 반영되지 않으면 곤란합니다
캐시를 거는 순간 새 질문이 생깁니다. 글을 수정했는데 언제 반영되는가입니다.
이 질문을 담당자 언어로 바꾸면 이렇습니다. 가격표를 고쳤는데 고객 화면에는 옛날 가격이 그대로 보이는 상황입니다. 속도를 얻고 정확성을 잃으면 남는 것이 없습니다.
시간이 지나 캐시가 만료되기를 기다리는 방법도 있지만, 콘텐츠를 저장하는 시점에 해당 경로를 명시적으로 다시 만들게 하는 편이 확실합니다. 저희는 관리 화면에서 글을 저장할 때 목록과 상세, 그리고 사이트맵까지 함께 갱신하도록 연결해 두었습니다.
이 연결을 우회하면 문제가 생깁니다. 데이터베이스에서 직접 공개 상태를 바꾼 적이 있는데, 목록과 상세는 시간이 지나 자연히 갱신됐지만 사이트맵은 옛 목록을 그대로 들고 있었습니다. 화면상으로는 정상이라 뒤처진 사실이 드러나지 않았습니다.
캐시를 도입할 때 성능만 보고 갱신 경로를 정하지 않으면, 빨라진 대신 무엇이 언제 반영되는지 알 수 없는 상태가 됩니다. 관리 화면을 거치지 않고 데이터를 직접 고치는 운영 방식이 있다면 그 경로도 함께 정리해야 합니다.
개선 수치를 받으면 무엇과 비교한 값인지 물어봅니다
캐시를 적용한 뒤 같은 경로를 두 번 연속 호출해 기록했습니다.
| 경로 | 1회차 | 2회차 |
|---|---|---|
| 포트폴리오 목록 | 7.6초 | 0.42초 |
| 블로그 목록 | 5.4초 | 0.24초 |
| 사이트맵 | 8.7초 | 0.02초 |
이 값은 개발 서버에서 측정했고, 캐시를 적용한 뒤의 1회차와 2회차 비교입니다. 수정 전후를 같은 조건에서 비교한 자료가 아닙니다. 1회차에는 개발 서버의 첫 컴파일 시간도 섞여 있어서, 줄어든 폭 전부가 캐시 덕분이라고 말할 수 없습니다.
여기서 확인되는 것은 캐시가 채워진 뒤의 재호출이 짧다는 사실까지입니다. 운영 환경의 성능을 이 숫자로 예상하는 것도 맞지 않습니다. 배포 지역과 네트워크 구간이 다릅니다.
담당자가 성능 개선 보고를 받을 때 쓸 수 있는 질문이 여기서 나옵니다. "몇 초가 몇 초로 줄었다"는 문장을 받으면, 무엇과 무엇을 같은 조건에서 비교한 값인지 물어보면 됩니다. 조건이 다른 두 값의 차이는 개선폭이 아닙니다.
화면에 안 쓰는 데이터가 방문자에게까지 갑니다
점검에서 별개의 문제도 나왔습니다. 목록 미리보기 영역이 글 본문 전체를 브라우저로 넘기고 있었습니다. 화면에는 제목과 예상 읽기 시간만 나오는데, 그 읽기 시간을 계산하려고 본문을 통째로 전달한 구조였습니다.
계산을 서버로 옮기고 결과 숫자만 넘기도록 바꿨습니다. 화면은 그대로인데 브라우저가 받는 데이터는 줄었습니다. 모바일 데이터로 접속하는 방문자에게는 이 차이가 체감으로 돌아옵니다.
같은 점검에서 첫 화면 영역 전체가 클라이언트 컴포넌트로 선언돼 있다는 것도 발견했지만, 이쪽은 손대지 않았습니다. 애니메이션이 얽혀 있어 나누는 작업의 범위가 크고, 잘못 건드리면 화면이 깨집니다. 발견 기록만 남기고 별도 작업으로 미뤄 두었습니다.
발견한 것을 전부 그 자리에서 고칠 필요는 없습니다. 다만 고치지 않은 것은 기록으로 남겨야 나중에 다시 찾습니다. 점검 보고서에 "발견했으나 이번에 하지 않은 것"이 적혀 있지 않으면, 다음 담당자는 그 문제를 처음부터 다시 찾게 됩니다.
속도 개선을 맡길 때 확인할 것
- 메타데이터를 만드는 경로와 본문을 그리는 경로가 같은 데이터를 각자 읽는가
- 일부만 필요한 곳에서 목록 전체를 읽고 걸러내는가
- 화면에 표시되지 않는 값이 방문자 브라우저까지 전달되는가
- 캐시를 걸었다면 콘텐츠 수정이 언제 어떻게 반영되는가
앞의 세 가지는 각 파일만 읽어서는 잘 안 보입니다. 한 화면을 그리는 동안 실제로 몇 번 호출되는지 세어보면 드러납니다. 마지막 하나는 캐시를 도입하는 순간 함께 정해야 하는 것이고, 나중으로 미루면 갱신이 안 되는 경로가 조용히 남습니다.
서버를 늘리기 전에 이 네 가지를 먼저 확인하면, 고정비를 올리지 않고도 해결되는 경우가 있습니다.
캐시 갱신이 실제로 배포본에 반영됐는지 확인하려면 개발 서버 화면이 아니라 빌드 산출물과 공개 주소를 봐야 합니다. 갱신이 걸리지 않은 경로는 화면상 정상으로 보이기 때문입니다.



