![[실전 DB 13] 캐시 계층으로 읽기를 0에 수렴시키기](https://img.thenullpage.com/posts/5705/5705_1_9b87e1.webp)
인덱스로 각 쿼리를 가볍게 하고 페이지네이션과 반복 조회를 잡았는데, 더 근본적으로 읽기를 줄이는 방법이 있다. 캐시다. 같은 데이터를 매번 데이터베이스에서 읽지 말고, 한 번 읽은 걸 저장해두고 재사용하는 것이다. 앞선 서버리스 시리즈에서도 다뤘지만, 이번엔 데이터베이스 부담을 줄이는 관점에서 파고든다.
인덱스가 한 번 읽는 비용을 줄인다면, 캐시는 읽는 횟수 자체를 줄인다. 이 둘을 함께 쓰면 효과가 더해지는 게 아니라 곱해진다.
1. 왜 캐시가 근본 해법인가
인덱스는 쿼리가 적게 읽게 하지만, 어쨌든 매 요청마다 데이터베이스를 읽는다. 자주 조회되는 목록이라면, 인덱스로 가벼워졌어도 그 가벼운 읽기가 수없이 반복된다. 캐시는 이 반복 자체를 없앤다. 한 번 읽어 저장해두고, 그 사이 오는 요청은 저장된 걸 돌려준다. 데이터베이스를 아예 안 읽는 것이다.
그래서 캐시는 읽기를 근본적으로 줄인다. 인덱스로 회당 읽기를 줄이고, 캐시로 읽는 횟수를 줄이면, 둘이 곱해져 데이터베이스 부담이 극적으로 준다. 자주 조회되고 자주 안 바뀌는 데이터일수록 캐시 효과가 크다. 목록처럼 봇이 끊임없이 긁는 데이터가 대표적이다.
내가 만든 사이트에서 캐시가 읽기 폭증을 잡는 결정적 역할을 했다. 봇들이 목록을 끊임없이 긁었는데, 그 목록을 캐시하니 봇이 아무리 긁어도 데이터베이스는 잠깐에 한 번만 읽혔다. 봇 트래픽의 대부분을 캐시가 흡수한 것이다. 인덱스만으로는 못 잡던 반복 읽기를 캐시가 잡았다.
이 대목에서 최적화의 층위가 정리된다. 인덱스는 한 번의 읽기를 싸게 만드는 것이고, 캐시는 그 읽기를 아예 안 하게 만드는 것이다. 층위가 다르니 둘은 경쟁하지 않고 겹쳐 쓴다. 가장 무거운 쿼리를 인덱스로 가볍게 하고, 그중 자주 반복되는 걸 다시 캐시로 덮으면, 남는 읽기가 얼마 안 된다.
캐시가 잘 듣는 데이터에는 공통점이 있다. 자주 읽히면서 자주 안 바뀌고, 누가 보든 똑같은 것이다. 목록이나 공개된 글이 딱 그렇다. 반대로 사람마다 다르거나 시시각각 바뀌는 데이터는 캐시가 잘 안 맞는다. 그래서 캐시를 걸기 전에 이 데이터가 캐시에 어울리는 성격인지부터 따진다. 어울리는 데만 걸어야 효과가 크다.
2. 여러 층의 캐시
캐시는 한 층이 아니라 여러 층으로 쌓을 수 있다. 앞선 시리즈에서 다뤘듯, 사용자와 가장 가까운 곳의 캐시와 여러 곳이 공유하는 캐시를 겹치는 것이다. 가까운 캐시는 가장 빠르지만 위치마다 따로이고, 공유 캐시는 조금 느리지만 모든 위치가 함께 쓴다.
데이터베이스 관점에서 중요한 건 공유 캐시다. 가까운 캐시는 위치마다 따로라, 봇이 여러 곳에서 오면 각 위치의 첫 요청은 데이터베이스까지 간다. 공유 캐시가 이 빈틈을 메운다. 어느 위치에서 한 번 데이터베이스를 읽어 공유 캐시에 넣으면, 다른 위치들도 그걸 읽어 쓴다. 데이터베이스 원본 읽기가 잠깐에 한 번으로 준다.
이 공유 캐시를 데이터베이스 안에 둘 수도 있다. 캐시 전용 자리를 만들어 목록 응답을 저장해두고, 요청은 그 저장된 응답을 읽는다. 그러면 무거운 목록 쿼리 대신 캐시 한 줄만 읽는다. 무거운 쿼리 한 번이 가벼운 읽기 한 번으로 바뀌는 것이다. 이게 여러 위치에 공유되니 효과가 크다.
내 경우 가까운 캐시와 데이터베이스 안의 공유 캐시를 함께 썼다. 대부분의 반복 요청은 가까운 캐시에서 끝나고, 그걸 뚫고 오는 첫 요청만 공유 캐시가 받고, 공유 캐시마저 비었을 때만 원본 쿼리가 도는 구조다. 세 층을 지나야 원본에 닿으니, 원본 읽기가 자연스럽게 마지막 수단으로 밀려난다.
층을 쌓을 때 중요한 건 각 층이 서로를 보완하게 배치하는 것이다. 가까운 캐시는 빠르지만 위치마다 따로라 빈틈이 있고, 공유 캐시는 그 빈틈을 메우지만 조금 느리다. 두 층을 겹치면 대부분은 가까운 캐시가 받고, 그걸 뚫는 것만 공유 캐시가 받고, 둘 다 비었을 때만 원본이 나선다. 원본이 가장 뒤로 밀려나는 구조가 목표다.
3. 미리 계산해 저장하기
캐시의 강력한 응용이 미리 계산해두는 것이다. 자주 쓰이는 무거운 결과를 정기 작업으로 미리 계산해 저장해두면, 실제 요청이 왔을 때 계산 없이 그 저장된 결과를 바로 준다. 앞서 다룬 정기 작업과 캐시를 결합하는 것이다.
예를 들어 인기 글 목록이나 통계 위젯처럼 계산이 무거운 걸, 매 요청마다 계산하지 않고 정기적으로 미리 만들어둔다. 사용자가 오기 전에 준비해두니, 요청이 오면 준비된 걸 읽기만 한다. 무거운 계산이 사용자 요청 경로에서 완전히 빠지는 것이다. 그 계산은 뒤에서 정기적으로만 조용히 돈다.
이 방식으로 위젯류의 데이터베이스 읽기를 거의 0으로 만들 수 있다. 매 페이지마다 계산하던 위젯을, 정기적으로 한 번 계산해 저장해두고 모든 페이지가 그걸 읽게 하는 것이다. 페이지가 아무리 많이 열려도 그 계산은 정기 작업이 한 번만 한다. 요청 수와 계산 횟수가 완전히 분리되니, 읽기가 0에 수렴한다.
미리 계산해두는 방식의 또 다른 이점은 요청이 몰려도 흔들리지 않는다는 것이다. 매 요청마다 계산하면 요청이 몰릴 때 계산도 몰려 부하가 치솟지만, 미리 만들어둔 걸 읽기만 하면 요청이 아무리 몰려도 하는 일은 가벼운 읽기뿐이다. 무거운 계산을 사용자 경로에서 빼두면, 순간적인 몰림에도 응답이 일정하게 빠르다.
4. 캐시 유효 시간의 균형
캐시의 핵심 조율은 유효 시간이다. 너무 길면 데이터가 바뀌었는데 옛것을 보여주고, 너무 짧으면 캐시 효과가 준다. 이 둘 사이의 균형을 데이터 성격에 맞춰 잡는다. 목록처럼 몇십 초 차이가 무의미한 건 짧게 둬도 봇 트래픽을 충분히 흡수한다.
몇십 초라는 짧은 시간도 효과가 크다. 봇이 초당 여러 번 긁어도 그 몇십 초 동안은 캐시된 하나만 돌려주면 되기 때문이다. 초당 수십 번의 요청이 몇십 초에 한 번의 데이터베이스 읽기로 준다. 사용자에겐 몇십 초 전 목록과 지금 목록이 거의 같으니 이 지연은 전혀 문제가 안 된다.
정기 계산으로 만드는 캐시는 계산 주기가 곧 유효 시간이다. 인기 글을 한 시간마다 계산하면 한 시간 동안은 그 결과가 유지된다. 이 주기를 데이터의 변화 속도에 맞춘다. 자주 바뀌면 짧은 주기로, 천천히 바뀌면 긴 주기로 잡는다. 데이터마다 알맞은 신선도를 정하는 것이다.
여기서 조심할 게 있다. 유효 시간을 데이터의 성격에 따라 다르게 줘야지, 전부 똑같이 길게 주면 어떤 데이터는 너무 옛것이 된다. 자주 바뀌고 정확해야 하는 것과, 천천히 바뀌고 대략이어도 되는 것을 구분해 각각 다른 시간을 주는 것이다. 이 구분이 다음 편에서 다룰 무효화 문제와 곧장 이어진다.
유효 시간을 정할 때 사용자 입장에서 생각하면 답이 쉬워진다. 이 데이터가 몇십 초, 몇 분 뒤처져 보여도 사용자가 불편할까를 묻는 것이다. 목록의 최신 글이 조금 늦게 뜨는 건 아무도 신경 안 쓰지만, 방금 누른 내 행동의 결과가 안 보이면 곧장 이상하게 느낀다. 그래서 사용자가 즉시성을 기대하는 데이터일수록 짧게, 또는 캐시를 안 한다.
5. 캐시가 읽기를 0에 수렴시킨다
캐시를 잘 쌓으면 데이터베이스 읽기가 극적으로 준다. 인덱스로 각 읽기를 가볍게 하고, 캐시로 읽는 횟수를 줄이고, 정기 계산으로 무거운 걸 미리 만들어두면, 실제 서비스 요청이 데이터베이스에 주는 부담이 거의 사라진다. 내가 읽기를 한도의 네 배에서 한도 안으로 줄인 것도 이 캐시가 결정적이었다.
특히 봇 트래픽에 캐시가 강력하다. 봇은 같은 걸 끊임없이 긁는데, 캐시가 있으면 그 반복이 데이터베이스에 안 닿는다. 봇 트래픽이 많은 사이트일수록 캐시의 효과가 크다. 사람보다 봇이 더 많이 긁는 신생 사이트에서, 캐시가 데이터베이스를 지키는 방패가 된다.
다만 캐시는 공짜가 아니다. 저장된 걸 언제 버리고 새로 채울지, 저장된 게 실제와 어긋나면 어떻게 할지, 그리고 남에게 보여선 안 될 걸 실수로 캐시하면 어떻게 되는지 같은 어려운 문제가 따라온다. 캐시의 힘이 클수록 이 문제들도 커진다. 캐시를 잘 쓰려면 채우는 법만큼 비우는 법을 알아야 한다.
캐시를 다루며 얻은 큰 그림은, 최적화가 결국 일을 덜 하게 만드는 것이라는 점이다. 인덱스는 한 번의 일을 가볍게 하고, 캐시는 그 일을 반복하지 않게 하고, 미리 계산은 사용자를 기다리게 하지 않는다. 방향은 하나다. 같은 결과를 더 적은 일로 내는 것이다. 이 관점을 쥐고 있으면 새로운 병목을 만나도 무엇을 해야 할지 감이 잡힌다.
정리하면 캐시는 자주 조회되고 자주 안 바뀌는 데이터를 저장해 재사용함으로써 데이터베이스 읽는 횟수를 근본적으로 줄이고, 여러 층으로 쌓고 미리 계산해두며 유효 시간을 조율해 읽기를 0에 수렴시킨다. 다음 편에서는 이 캐시의 가장 어려운 부분, 무효화와 보안 함정을 다룬다.