클라이언트 사이드 캐싱

클라이언트 사이드 캐싱 (Client-side caching)

클라이언트 사이드 캐싱(Client-side caching) 은 Redis 클라이언트와 서버 사이의 네트워크 트래픽을 줄여주는 기법이에요. 트래픽이 줄면 전체적인 성능이 좋아지는 건 당연하겠죠.

기본적으로는 사용자 앱과 데이터베이스 사이에 있는 애플리케이션 서버가 모든 읽기 요청마다 클라이언트 라이브러리를 통해 Redis 서버에 접속해요. 사용자 앱 → 애플리케이션 서버 → 데이터베이스 → 다시 돌아오는 흐름이죠.

클라이언트 사이드 캐싱을 쓰면, 클라이언트 라이브러리가 데이터베이스에서 가져온 데이터 항목들을 로컬 캐시에 보관해요. 같은 항목이 다시 필요해지면 데이터베이스 대신 캐시에서 읽기 요청을 처리할 수 있어요. 캐시 접근은 네트워크를 거쳐 데이터베이스와 통신하는 것보다 훨씬 빠르고, 네트워크 트래픽도 줄어들죠. 서버 부담이 줄어드니 더 적은 하드웨어 리소스로도 운영할 수 있는 여지가 생겨요.

다른 형태의 캐싱과 마찬가지로, 이 기법은 전체 데이터 중 일부 데이터가 나머지보다 훨씬 자주 접근되는 매우 흔한 경우(파레토 원칙)에 잘 맞아요.

출처: 공식문서

데이터가 바뀌면 캐시를 어떻게 갱신하나요? (tracking)

모든 캐싱 시스템은 메인 데이터베이스의 데이터가 바뀌었을 때 캐시의 데이터도 함께 갱신하는 방식을 가져야 해요. Redis는 이를 tracking이라는 방식으로 처리해요.

클라이언트 사이드 캐싱이 켜져 있으면, Redis 서버는 각 클라이언트 연결이 이전에 읽었던 키의 집합을 기억(track) 해요. 이때는 GET처럼 클라이언트가 직접 데이터를 읽는 경우와, STRLEN처럼 서버가 저장된 데이터로부터 값을 계산해 주는 경우 모두 포함돼요. 어떤 클라이언트가 tracked 키에 새 데이터를 쓰면, 서버는 그 키에 접근했던 모든 클라이언트에게 무효화(invalidation) 메시지를 보내요. 이 메시지는 "캐시에 담아둔 사본이 더 이상 유효하지 않다"는 경고이고, 클라이언트는 응답으로 낡은 데이터를 캐시에서 내보내요(evict). 다음에 같은 키를 읽을 때는 데이터베이스에 직접 접근해 갱신된 데이터로 캐시를 새로 채우게 돼요.

참고: 클라이언트의 어떤 연결이 끊기면(커넥션 풀의 연결 포함), 클라이언트는 클라이언트 사이드 캐시의 모든 키를 비웁니다. 이후에는 여전히 살아있는 연결들을 통한 읽기부터 다시 캐싱이 이뤄져요.

두 클라이언트가 같은 키에 접근하고 갱신하며 상호작용하는 과정은 시퀀스 다이어그램으로 표현할 수 있어요.

어떤 클라이언트 라이브러리가 클라이언트 사이드 캐싱을 지원하나요?

다음 클라이언트 라이브러리들이 명시된 버전부터 클라이언트 사이드 캐싱(CSC)을 지원해요:

클라이언트 버전
redis-py v5.1.0
Jedis v5.2.0
node-redis v5.1.0
go-redis v9.22.0

참고로 일부 다른 클라이언트들은 서버에서 CSC를 구성하는 CLIENT TRACKING 명령은 지원해도, CSC에 필요한 기능 자체는 지원하지 않을 수 있어요.

어떤 명령이 데이터를 캐시할 수 있나요?

@read ACL 카테고리에 속하는 모든 읽기 전용 명령은 캐시된 데이터를 사용해요. 다만 다음은 예외예요:

클라이언트 사이드 캐싱을 쓸 때 서버 동작을 확인하고 싶다면 MONITOR 명령을 사용해 볼 수 있어요. MONITOR는 서버에서 발생한 활동만 보여주므로, 키에 대한 첫 번째 캐시 가능한 접근에서만 서버 응답이 보이고, 이후 접근은 캐시로 처리되기 때문에 제대로 동작 중이라면 MONITOR에 서버 활동이 잡히지 않아야 해요.

명령에 대해 어떤 데이터가 캐시되나요?

크게 보면, 명령 호출에 대한 특정 응답으로 온 데이터는 첫 사용 후 캐시되고, 그 데이터의 일부나 그로부터 계산된 값은 평소처럼 서버에서 가져온 뒤 별도로 캐시돼요. 예를 들어:

  • GET이 가져온 전체 문자열이 캐시에 추가돼요. 같은 문자열의 일부를 가져오는 SUBSTR은 첫 번째에만 서버에서 계산되고, 원본 문자열과는 별도로 캐시돼요.
  • 문자열에 GETBIT이나 BITFIELD를 쓰면 반환된 값이 원본 문자열과 분리되어 캐시돼요.
  • 키로 접근하는 복합 데이터 타입(hash, JSON, set, sorted set)에서는 전체 객체가 개별 필드와 분리되어 캐시돼요. 그래서 JSON.GET mykey $JSON.GET mykey $.myfield의 결과는 캐시에 각각 다른 항목으로 만들어져요.
  • list, stream, sorted set의 범위(range)는 그 객체와 분리되어 캐시되고, SINTER, SDIFF가 반환한 부분집합도 별도 캐시 항목을 만들어요.
  • MGET 같은 멀티 키 읽기 명령에서는 키의 순서가 중요해요. 예를 들어 MGET name:1 name:2MGET name:2 name:1과 별도로 캐시되는데, 서버가 지정한 순서대로 값을 반환하기 때문이에요.
  • 데이터 타입에서 계산된 불리언·숫자 값(예: SISMEMBER, LLEN)은 그 객체와 분리되어 캐시돼요.

사용 권장 사항

어떤 캐싱 시스템이든 제약이 있듯, 클라이언트 사이드 캐싱에도 몇 가지 한계가 있어요:

  • 캐시는 제한된 메모리만 사용할 수 있어요. 한도에 도달하면 클라이언트는 새 항목을 위한 공간을 만들기 위해 유용한 항목을 내보내야(evict) 해요.
  • 캐시 미스, tracking, 무효화 메시지는 항상 약간의 성능 오버헤드를 더해요.

이 한계 안에서 효율적으로 쓰기 위한 가이드라인이에요:

  • 캐시 친화적이지 않은 데이터는 별도 연결을 사용하세요: 캐싱은 자주 읽히고 드물게 갱신되는 키에 가장 큰 이득을 줘요. 하지만 카운터·스코어보드처럼 자주 갱신되는 데이터도 있죠. 이런 경우 무효화 메시지의 오버헤드가 캐싱으로 아끼는 것보다 클 수 있어요. 캐시 친화적이지 않은 데이터는 클라이언트 사이드 캐싱 없이 별도 연결로 처리해 이 문제를 피하세요.
  • 얼마나 많은 항목을 캐시할 수 있을지 추정하세요: 클라이언트 라이브러리는 캐시에 보관할 최대 항목 수를 지정할 수 있어요. 이 숫자는 "캐시에 쓸 수 있는 최대 메모리 ÷ 저장하려는 항목의 평균 크기"로 추정할 수 있어요(MEMORY USAGE 명령으로 키의 메모리 사용량을 알 수 있어요). 예를 들어 캐시에 10MB(10485760바이트)를 쓸 수 있고 항목 평균 80바이트라면, 대략 10485760 / 80 = 131072개의 항목을 담을 수 있어요. 실제 테스트 부하로 서버 메모리 사용량을 관찰하며 추정치를 높이거나 낮추세요.

참고 (Reference)

Redis 서버는 기본적인 Redis 클라이언트들이 쓰지 않지만, 커스텀 클라이언트나 고급 애플리케이션에는 유용할 수 있는 클라이언트 사이드 캐싱용 추가 기능을 구현해요. 클라이언트 사이드 캐싱에서 쓸 수 있는 모든 옵션에 대한 전체 기술 가이드는 Client-side caching reference를 참고하세요.

더 알아보기 (Learn more)