공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 18 / 22

로컬 캐시와 @Cacheable

TTL, 크기 제한, 스탬피드 방지, 무효화, Caffeine 설정
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

2. 핵심 원리

2.1 캐시가 필요한 순간, 넣으면 안 되는 데이터

캐시에 적합한 데이터는 읽기가 압도적으로 많고 변경이 드문 데이터입니다. 공통 코드, 권한 목록, 설정값, 환율처럼 "자주 읽고 가끔 바뀌는" 데이터가 여기에 해당합니다. 이런 데이터는 잠깐 오래된 값을 보여줘도 업무에 지장이 없습니다.

반대로 캐시에 넣으면 안 되는 데이터도 있습니다. 계좌 잔액, 재고 수량처럼 정합성이 중요한 값, 사용자별로 매번 다른 개인화 데이터, 그리고 한 번만 쓰고 버리는 값입니다. 이런 데이터를 캐시하면 "오래된 잔액을 보여줬다"는 사고로 이어집니다.

2.2 키 설계

캐시 키는 조회 조건을 빠짐없이 반영해야 합니다. 예를 들어 findUsers(deptId, status) 를 캐시한다면 키는 dept:1:status:ACTIVE 처럼 두 파라미터를 모두 포함해야 하고, 하나라도 빠지면 다른 조건의 결과가 섞여 나옵니다.

Spring @Cacheable 은 기본적으로 파라미터 전체를 조합해 키를 만듭니다. 다만 파라미터가 없거나 null 이면 키가 겹칠 수 있어 key = "#p0 + ':' + #p1" 처럼 명시하는 편이 안전합니다.

null 값 자체를 캐시할지도 결정해야 합니다. null 은 기본적으로 캐시되지 않는 구현이 많아, 없는 값을 조회할 때마다 매번 DB 를 다시 때리는 문제가 생깁니다. 이 문제의 해법이 2.5 의 음수 캐시입니다.

2.3 TTL 과 크기 제한을 함께 두는 이유

TTL(Time To Live)은 항목이 캐시에 머무는 최대 시간입니다. TTL 만 있고 크기 제한이 없으면, 키 종류가 계속 늘어나는 상황(예: 사용자별 캐시)에서 TTL 이 지나기 전까지 힙 메모리가 끝없이 커질 수 있습니다.

크기 제한(LRU, Least Recently Used)은 오래 안 쓴 항목부터 밀어내 메모리 상한을 지킵니다. TTL 과 크기 제한은 서로 다른 문제를 막습니다. TTL 은 "오래된 값을 계속 보여주는 문제"를, 크기 제한은 "메모리가 무한정 느는 문제"를 막습니다. 그래서 둘 다 함께 설정하는 것이 실무 기본값입니다.

2.4 무효화 전략

데이터를 수정했을 때 캐시를 최신 상태로 맞추는 방법은 크게 두 가지입니다.

전략 동작 적합한 경우
evict 수정 시 해당 키만 삭제, 다음 조회에서 재계산 일반적인 갱신
put 수정 시 새 값을 캐시에 바로 채움 조회 비용이 커서 재계산을 피하고 싶을 때
clear 캐시 전체 비움 코드 테이블 대량 갱신, 배치 후

evict 는 구현이 단순하고 다음 조회 시점에 최신 값을 보장하므로 가장 흔히 씁니다. put 은 수정된 값을 이미 알고 있을 때(예: update 메서드가 갱신된 엔티티를 반환) DB 재조회 없이 캐시를 갱신할 수 있어 효율적입니다.

2.5 캐시 스탬피드와 음수 캐시

캐시 스탬피드는 인기 있는 키 하나가 만료되는 순간 동시에 들어온 여러 요청이 전부 캐시 미스로 판단해 DB 를 동시에 때리는 현상입니다. 트래픽이 많은 서비스에서는 이 순간 DB 커넥션 풀이 순식간에 고갈될 수 있습니다.

해법은 "같은 키에 대한 계산은 한 번만" 실행하도록 락을 거는 것입니다. ConcurrentHashMap.computeIfAbsent 나 compute 는 같은 키에 대해 여러 스레드가 동시에 진입해도 계산 함수(loader)를 단 한 번만 실행하도록 보장합니다. 이 레슨의 Cache.get 이 이 방식을 씁니다.

음수 캐시(negative cache)는 "값이 없다"는 결과 자체를 짧게 캐시하는 기법입니다. 존재하지 않는 사용자 ID 를 반복 조회하는 요청이 매번 DB 까지 가는 것을 막아 줍니다. 다만 TTL 을 일반 값보다 짧게 둬서, 실제로 데이터가 생겼을 때 오래 못 보는 일을 방지해야 합니다.

2.6 로컬 캐시의 한계와 분산 캐시로 가는 기준

로컬 캐시는 인스턴스마다 별도의 메모리를 쓰기 때문에 서버가 여러 대(다중 인스턴스)면 인스턴스 간 캐시 내용이 서로 다를 수 있습니다. 한 서버에서 evict 해도 다른 서버는 여전히 옛날 값을 들고 있습니다. 또한 캐시가 커지면 JVM 힙을 그만큼 점유하고, 힙 덤프를 뜨면 캐시에 담긴 개인정보가 그대로 노출될 위험도 있습니다.

다음 조건 중 하나라도 해당하면 Redis 같은 분산 캐시로 옮기는 것을 검토합니다.

조건 이유
서버가 2대 이상 인스턴스 간 캐시 불일치 문제
데이터 변경 즉시 전 서버 반영 필요 로컬 evict 는 자기 서버만 지움
캐시 크기가 힙을 압박 별도 프로세스로 메모리 분리 필요
서버 재시작 시에도 캐시 유지 필요 로컬 캐시는 재시작하면 사라짐

폐쇄망 환경에서는 Redis 서버 반입·운영 부담이 있어, 인스턴스가 1대뿐이거나 데이터 불일치를 잠깐 허용해도 되는 경우 로컬 캐시만으로 충분한 사례가 많습니다.

2.7 Spring @Cacheable 계열 대응표

Spring 캐시 추상화는 로컬 캐시 구현(Caffeine, EhCache)이나 Redis 를 같은 애너테이션으로 다룰 수 있게 해 줍니다.

애너테이션 동작
@Cacheable 캐시에 있으면 반환, 없으면 메서드 실행 후 저장
@CacheEvict 메서드 실행 후 해당 키(또는 전체) 삭제
@CachePut 항상 메서드를 실행하고 결과를 캐시에 저장
@Caching 위 애너테이션을 여러 개 조합할 때 사용

가장 흔한 함정은 self-invocation 입니다. @Cacheable 은 프록시를 통해 동작하는데, 같은 클래스 안에서 this.getUser(id) 처럼 직접 호출하면 프록시를 거치지 않아 캐시가 전혀 동작하지 않습니다. 이 경우 별도 빈으로 분리하거나 AopContext.currentProxy() 로 우회해야 합니다.

설정은 application.yml 에 다음처럼 둡니다.

yaml
spring:
  cache:
    type: caffeine
    cache-names: users, codes
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=10m

적중률(hit ratio) 측정은 운영에서 캐시가 실제로 효과가 있는지 확인하는 핵심 지표입니다. Caffeine 은 recordStats() 를 켜면 actuator 의 /actuator/caches 와 /actuator/metrics/cache.gets 로 hit·miss 수치를 노출할 수 있어, 캐시 크기나 TTL 을 튜닝할 때 근거로 씁니다.

핵심 원리
  • 2.1 캐시가 필요한 순간, 넣으면 안 되는 데이터
  • 2.2 키 설계
  • 2.3 TTL 과 크기 제한을 함께 두는 이유
  • 2.4 무효화 전략
  • 2.5 캐시 스탬피드와 음수 캐시
  • 2.6 로컬 캐시의 한계와 분산 캐시로 가는 기준
  • 2.7 Spring @Cacheable 계열 대응표
이전 섹션1 왜 배우는가2 / 7다음 섹션3 코드 예제