SCM·ERP처럼 조회 화면이 많은 회사 시스템을 만들고 운영해 온 내 입장에서는, “느리니 Redis부터 붙이자”는 말이 가장 익숙한 출발점이었다. 하지만 실제로 요청 경로를 펼쳐 보면 Redis는 여러 선택지 중 뒤쪽에 있다. 이미 받은 CSS를 브라우저가 다시 내려받고, 정적 이미지를 원본 서버가 계속 보내고 있다면 Redis를 더하는 일은 가장 비싼 구간을 그대로 둔 채 뒤쪽만 손보는 셈이다.
그래서 요즘에는 조회 성능 이야기가 나오면 먼저 사용자에게 가까운 곳에서, 그 데이터가 허용하는 최신성 범위 안에서 요청을 끝낼 수 있는가를 본다. 이 글은 특정 제품의 벤치마크 결과가 아니라, 내가 서비스 설계·운영에서 Redis를 선택하기 전에 다시 확인하려는 판단 순서를 정리한 기록이다.
연관 글
요약
- Redis는 캐시 계층 중 하나이며, 사용자와 가까운 브라우저·CDN부터 먼저 설계해야 한다.
- 캐시 계층은 변경 빈도, 공유 범위, 데이터 크기, 최신성 요구로 선택한다.
- TTL만 정하지 말고 만료 순간의 캐시 스탬피드와 원본 DB 보호까지 함께 설계한다.
목차
이 글에서 다루는 내용
내가 Redis보다 먼저 확인하는 세 계층
| 계층 | 적합한 데이터 | 장점 | 조심할 점 |
|---|---|---|---|
| 브라우저 HTTP 캐시 | 해시가 붙은 JS·CSS·폰트·이미지 | 네트워크 요청 자체가 없다 | 파일명이 고정된 HTML을 장기 캐시하면 배포가 꼬인다 |
| CDN | 이미지, 영상, 다운로드 파일, 공개 API 일부 | 사용자와 가까운 엣지에서 원본 부하를 줄인다 | 쿠키·개인 정보가 섞인 응답을 공유 캐시하면 안 된다 |
| 애플리케이션 로컬 캐시 | 국가 코드, 메뉴, 공통 설정 | 네트워크 왕복 없이 가장 빠르다 | 여러 인스턴스의 값이 달라질 수 있다 |
| Redis 같은 공유 캐시 | 여러 서버가 함께 보는 세션, 집계, 사용자별 조회 결과 | 인스턴스가 늘어도 같은 값을 쓴다 | 네트워크 비용, 메모리, 장애·만료 설계가 필요하다 |
이 표를 기준으로 보면 ‘모든 조회를 Redis로’가 좋은 답이 아닌 이유가 보인다. 거의 변하지 않는 국가 코드라면 서버 메모리의 Caffeine이 더 짧은 경로다. 반대로 여러 서버에서 같은 장바구니 상태를 봐야 한다면 로컬 캐시만으로는 일관성을 보장하기 어렵다.
브라우저 캐시부터 다시 보게 된 이유
해시가 들어간 정적 파일은 이름이 바뀌면 새 URL이 된다. 그래서 오래 캐시해도 된다.
# app.4c91e8.js, logo.93a0f2.svg처럼 내용 해시가 붙은 파일
Cache-Control: public, max-age=31536000, immutable
# 배포 때마다 내용이 바뀔 수 있는 진입 HTML
Cache-Control: no-cache
여기서 no-cache는 ‘저장하지 마라’가 아니다. 저장은 하되 재사용 전에 원본 검증을 하라는 뜻이다. 저장 자체를 막는 지시는 no-store다. 검증에는 ETag와 Last-Modified가 쓰이며, 변경이 없으면 서버는 본문 대신 304 Not Modified를 돌려줄 수 있다. 로그인한 사용자 화면이나 결제 응답처럼 민감한 내용은 private 또는 no-store를 먼저 검토해야 한다.
공통 조회는 로컬 캐시로 먼저 끝내는 이유
Spring이라면 자주 읽히고 드물게 바뀌는 공통 코드는 Caffeine으로 시작하기 좋다.
Cache<String, Country> countries = Caffeine.newBuilder()
.maximumSize(1_000)
.expireAfterWrite(Duration.ofHours(6))
.build();
Country country = countries.get(code, this::loadCountryFromDatabase);
get(key, loader)처럼 조회와 적재를 하나의 연산으로 묶으면, 같은 키가 비어 있을 때 여러 스레드가 동시에 원본을 읽는 일을 줄일 수 있다. 다만 서버가 여러 대라면 각 인스턴스의 만료 시점과 값은 독립적이다. 공지처럼 수 분 늦어도 되는 데이터는 괜찮지만, 권한 변경을 즉시 반영해야 하는 경로에는 짧은 TTL, 이벤트 기반 무효화, 혹은 공유 캐시가 필요하다.
Redis를 붙일 때 내가 TTL보다 먼저 보는 것
Redis는 여러 인스턴스가 값을 공유해야 할 때 유용하다. 하지만 TTL만 정하고 끝내면 만료되는 순간 많은 요청이 동시에 DB로 몰리는 캐시 스탬피드가 생길 수 있다. 인기 상품, 메인 화면, 대량 배치 직후의 키가 특히 위험하다.
운영 관점에서 내가 먼저 확인하려는 것은 세 가지다. 아직 병목을 숫자로 확인하지 않은 상태에서 잠금이나 복제부터 늘리는 실수를 피하기 위해서다.
- 만료를 분산한다. 동일 TTL 대신 작은 jitter를 더해 키가 한꺼번에 사라지지 않게 한다.
- 재계산을 한 요청으로 제한한다. 로컬 단위의 single-flight 또는 분산 잠금을 검토하고, 잠금 실패 요청은 짧게 기다리거나 이전 값을 허용한다.
- 원본 보호 장치를 둔다. DB 타임아웃·커넥션 풀·rate limit을 캐시와 별개로 점검한다.
그리고 Redis 키는 크기와 트래픽도 함께 봐야 한다. 값이 지나치게 큰 big key는 조회·복제·삭제 비용을 키우고, 한 키에 요청이 집중되는 hot key는 특정 노드의 CPU와 네트워크를 태운다. hot key라면 Redis 앞의 로컬 캐시, 읽기 복제본, 키 복제·분산 중 무엇이 현재 병목에 맞는지 측정해서 선택해야 한다.
내가 다음 성능 이슈에서 고를 순서
다음 성능 이슈를 만났을 때도 Redis 도입 여부부터 묻기보다, 나는 아래 네 질문을 먼저 적어둘 생각이다. 이 순서를 문서에 남기면 “캐시를 넣자”는 결론보다 왜 그 계층을 골랐는지가 훨씬 선명해진다.
- 이 값은 얼마나 자주 바뀌며, 얼마 동안 오래된 값을 허용할 수 있는가?
- 누구와 공유되는가? 한 브라우저, 전 사용자, 여러 서버 중 어디까지인가?
- 원본을 다시 읽는 비용과 데이터 크기는 얼마인가?
- 틀리거나 오래된 값이 보여도 되는가? 안 된다면 무효화 신호는 어디서 오는가?
캐시는 ‘DB 앞에 Redis 하나’가 아니라 요청 경로를 줄이는 선택의 묶음이다. 브라우저에서 끝낼 수 있는 요청을 CDN이나 서버까지 보내지 않는 것, 로컬에서 끝낼 수 있는 공통 조회를 매번 Redis까지 보내지 않는 것부터 시작해 보자. 그 다음에야 Redis가 정말 필요한 자리도 더 정확히 보인다.
FAQ
no-cache와 no-store는 어떻게 다른가요?
no-cache는 응답을 저장할 수 있지만 재사용 전 검증을 요구한다. no-store는 브라우저와 중간 캐시가 응답을 저장하지 않도록 지시한다. 이름과 달리 캐시를 완전히 끄는 쪽은 no-store다.
로컬 캐시와 Redis를 함께 써도 되나요?
가능하다. 공통적으로 많이 읽는 값은 로컬 캐시를 1차로 두고, 여러 인스턴스가 공유해야 하는 값은 Redis를 2차로 두는 방식이 흔하다. 대신 갱신·무효화 흐름과 각 TTL을 문서화하지 않으면 오래된 값의 원인을 찾기 어려워진다.
캐시 TTL은 몇 초가 적당한가요?
정답은 없다. ‘얼마나 오래된 값까지 허용하는가’에서 시작해야 한다. 1초라도 오래되면 안 되는 주가나 재고와, 10분 늦어도 되는 공지·통계에는 같은 TTL을 적용할 이유가 없다.