무지를 아는 것이 곧 앎의 시작

Redis

[Redis] 다건 조회를 캐싱할 때 고민했던 것들

Alex96 2023. 2. 21. 01:22

캐시

최근 Redis를 사용해서 여러 조건절을 가지는 검색 쿼리의 응답을 캐싱해야하는 기술적인 과제를 겪었다.

꽤나 많은 고민과 주변의 도움으로 나름대로의 해결법을 도출해냈고 그 과정을 기록할까 한다.

문제 원인

일단 상황을 설명하면, 단건 id로 찍어서 조회하는 쿼리는 캐시하기 쉽지만 다건 조회는 신경쓸 포인트가 많아진다. 이유는 원본 데이터에 변경이 일어났을 경우 정합성 때문인데 캐시를 사용하게 되면 발생하는 고질적인 문제다.

단건 조회의 경우 id로 캐시의 key를 특정할 수 있기 때문에 조회시 캐시하고, 이후로 캐시된 값을 반환하고 변경이 발생하면 변경시 해당 캐시를 무효화하게 하면 정합성이 맞춰진다.

반면, 검색 조건이 여러 개가 들어가는 다건 조회의 경우 검색 조건까지 key에 넣고 결과를 캐싱하기 때문에 해당 검색 결과에 해당하는 요소가 변경이 발생했을 때, 이 데이터가 어떤 검색 결과 캐싱에 포함되고 있는지 알아내야 해당 key들을 무효화할 수 있어서 한동안 무효화할 key를 알아낼 방법을 고민했다.

원초적으로 무효화할 key를 알아내기

Redis에서 key를 조회하는 방법은 두 가지가 있는데 일단 keys 명령어가 있다. 근데 이 명령어는 모든 key를 조회해서 패턴에 안맞는 애들은 결과에 안내려주는 로직을 탄다. 시간복잡도가 O(n)이며 Redis에 키가 100만개 있다면 100만개 다 본다. 싱글스레드인 Redis에선 그거 다 볼 때까지 모든 요청에 병목이 발생한다. 프로덕션 환경에서는 사용하지 말것을 권장한다.

keys 를 대체하기 위해 scan 명령어가 있는데 커서 기반으로 원하는 갯수만큼 읽어올 수 있고, 다음 커서를 반환받는다. 이걸로 여러 번의 요청으로 끊어서 key들을 조회하면 요청 사이사이에 Redis가 다른 요청을 처리할 수 있어서 싱글스레드로 무거운 요청을 수행하느라 생기는 병목현상을 어느 정도 해결할 수 있다. 다만, 이 방법도 프로덕션에선 딱히 권장하지 않는다. 결국 모든 key를 다 읽어야하는 건 똑같기 때문에 수행 시간에는 딱히 개선이 없다.

무효화할 key를 캐시하기

무효화할 key들의 집합을 따로 캐시한다. 조회 결과를 캐시할 때 해당 캐시의 키를 별도의 컬렉션에 캐시해두는 것이다. 만약 요소에 변경이 발생하면 그 컬렉션 key만 딱 조회해서 key들로 캐시를 무효화해줄 수 있다. 자료형은 sorted set이 적절하다고 생각한다. 스코어에 시간을 저장하면 ttl이 지나지 않은 키들만 무효화하고 나머지는 무시할 수 있다.

이어서…

이후 포스팅에서는 해당 내용을 실습해볼까 한다. 단건 조회의 경우 스프링에서 제공하는 @Cacheable, @CacheEvict 만 잘 사용하면 되지만, 이 포스팅에서 다룬 다건 조회 캐시 무효화의 경우엔 별도의 어노테이션을 만들고 스프링의 AOP를 활용하여 구현해볼 예정