Daily-It

Java·백엔드 운영 경험으로 AI·개발·자동화와 일상 IT의 실제 판단을 기록하는 기술 블로그입니다.

캐시가 느릴 때 Redis부터 붙이기 전에 다시 보는 순서

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다. 검증에는 ETagLast-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로 몰리는 캐시 스탬피드가 생길 수 있다. 인기 상품, 메인 화면, 대량 배치 직후의 키가 특히 위험하다.

운영 관점에서 내가 먼저 확인하려는 것은 세 가지다. 아직 병목을 숫자로 확인하지 않은 상태에서 잠금이나 복제부터 늘리는 실수를 피하기 위해서다.

  1. 만료를 분산한다. 동일 TTL 대신 작은 jitter를 더해 키가 한꺼번에 사라지지 않게 한다.
  2. 재계산을 한 요청으로 제한한다. 로컬 단위의 single-flight 또는 분산 잠금을 검토하고, 잠금 실패 요청은 짧게 기다리거나 이전 값을 허용한다.
  3. 원본 보호 장치를 둔다. DB 타임아웃·커넥션 풀·rate limit을 캐시와 별개로 점검한다.

그리고 Redis 키는 크기와 트래픽도 함께 봐야 한다. 값이 지나치게 큰 big key는 조회·복제·삭제 비용을 키우고, 한 키에 요청이 집중되는 hot key는 특정 노드의 CPU와 네트워크를 태운다. hot key라면 Redis 앞의 로컬 캐시, 읽기 복제본, 키 복제·분산 중 무엇이 현재 병목에 맞는지 측정해서 선택해야 한다.

내가 다음 성능 이슈에서 고를 순서

다음 성능 이슈를 만났을 때도 Redis 도입 여부부터 묻기보다, 나는 아래 네 질문을 먼저 적어둘 생각이다. 이 순서를 문서에 남기면 “캐시를 넣자”는 결론보다 왜 그 계층을 골랐는지가 훨씬 선명해진다.

  • 이 값은 얼마나 자주 바뀌며, 얼마 동안 오래된 값을 허용할 수 있는가?
  • 누구와 공유되는가? 한 브라우저, 전 사용자, 여러 서버 중 어디까지인가?
  • 원본을 다시 읽는 비용과 데이터 크기는 얼마인가?
  • 틀리거나 오래된 값이 보여도 되는가? 안 된다면 무효화 신호는 어디서 오는가?

캐시는 ‘DB 앞에 Redis 하나’가 아니라 요청 경로를 줄이는 선택의 묶음이다. 브라우저에서 끝낼 수 있는 요청을 CDN이나 서버까지 보내지 않는 것, 로컬에서 끝낼 수 있는 공통 조회를 매번 Redis까지 보내지 않는 것부터 시작해 보자. 그 다음에야 Redis가 정말 필요한 자리도 더 정확히 보인다.

FAQ

no-cacheno-store는 어떻게 다른가요?

no-cache는 응답을 저장할 수 있지만 재사용 전 검증을 요구한다. no-store는 브라우저와 중간 캐시가 응답을 저장하지 않도록 지시한다. 이름과 달리 캐시를 완전히 끄는 쪽은 no-store다.

로컬 캐시와 Redis를 함께 써도 되나요?

가능하다. 공통적으로 많이 읽는 값은 로컬 캐시를 1차로 두고, 여러 인스턴스가 공유해야 하는 값은 Redis를 2차로 두는 방식이 흔하다. 대신 갱신·무효화 흐름과 각 TTL을 문서화하지 않으면 오래된 값의 원인을 찾기 어려워진다.

캐시 TTL은 몇 초가 적당한가요?

정답은 없다. ‘얼마나 오래된 값까지 허용하는가’에서 시작해야 한다. 1초라도 오래되면 안 되는 주가나 재고와, 10분 늦어도 되는 공지·통계에는 같은 TTL을 적용할 이유가 없다.

참고 자료