Development··5 min read

캐싱하면서 저지른 실수 모음집

캐싱은 간단해 보이지만 무효화, 일관성, 메모리 관리에서 삽질한 이야기. Redis를 쓰면 다 해결될 줄 알았다.

캐싱하면 빨라진다, 맞는 말이긴 한데

API 응답 시간이 느려서 캐싱을 도입했다. 상품 목록 API가 평균 1,200ms였는데 Redis 캐싱을 붙이니까 45ms로 떨어졌다. 26배 빨라졌다. 여기까진 해피엔딩이다.

그 다음에 벌어진 일들이 문제다.

실수 1: TTL을 안 걸었다

처음에 캐시 만료 시간을 안 설정했다. "데이터가 바뀌면 그때 무효화하면 되지" 라고 생각했는데, 무효화 로직을 빼먹은 곳이 있었다. 상품 가격이 바뀌었는데 캐시에는 옛날 가격이 남아 있었다. 고객이 23,900원짜리를 19,800원에 결제하는 사고가 생겼다.

이거 발견하는 데 3일이 걸렸다. 그 사이에 할인 안 된 가격으로 결제된 건이 17건 있었다. 차액을 회사에서 물었다.

실수 2: 캐시 키 설계를 대충 했다

products:list라는 키로 상품 목록을 캐싱했다. 근데 페이지네이션이 있었다. 1페이지든 5페이지든 같은 캐시 키를 쓰니까 항상 1페이지 데이터만 나왔다.

products:list:page:1:size:20으로 바꿨다. 근데 이번엔 정렬 조건이 빠져 있었다. 가격순과 최신순이 같은 캐시를 보고 있었다. products:list:page:1:size:20:sort:price로 다시 바꿨다. 그랬더니 필터 조건도 빠져 있었고...

(캐시 키 설계를 대충 하면 이런 식으로 끝없이 수정하게 된다.)

실수 3: 캐시 스탬피드

상품 목록 캐시가 만료되는 순간, 동시에 200명의 유저가 접속해 있으면 200개의 DB 쿼리가 동시에 날아간다. 이걸 캐시 스탬피드라고 하는데, 캐시 TTL을 5분으로 설정한 뒤에 5분마다 DB 부하가 스파이크치는 현상이 생겼다.

해결법은 캐시 만료 전에 미리 갱신하는 방법이 있고, 락을 걸어서 한 요청만 DB를 조회하게 하는 방법이 있다. 나는 후자를 택했는데, Redis 락을 구현하는 데 또 하루가 갔다.

실수 4: 메모리 관리를 안 했다

Redis에 maxmemory 설정을 안 했다. 캐시 데이터가 계속 쌓이다가 서버 메모리 8GB를 다 먹었다. Redis가 죽었다. 캐시가 죽으니까 모든 요청이 DB로 직행했고, DB도 과부하로 느려졌다.

maxmemory를 2GB로 설정하고 eviction 정책을 allkeys-lru로 바꿨다. 이건 Redis 기본 설정 가이드에 나오는 내용인데 처음에 건너뛰었던 내 잘못이다.

캐싱은 간단한 게 아니다

"Redis 붙이면 빨라져" 이 말은 맞다. 근데 그 뒤에 따라오는 무효화, 일관성, 키 설계, 메모리 관리를 제대로 안 하면 캐시 때문에 더 큰 문제가 생긴다.

솔직히 캐싱 없이 DB 쿼리를 최적화하는 게 먼저였을 수도 있다. 1,200ms짜리 쿼리 자체가 문제였는데 캐시로 덮어버린 거니까. 인덱스 하나 걸었으면 200ms로 줄었을 수도 있다. 근거는 없지만 그랬을 것 같다는 찝찝함이 아직 남아있다.

관련 글