자바 백엔드 입문 56편 — @Cacheable 캐싱

2026-05-16자바 백엔드 입문

자바 백엔드 입문 시리즈 59편 중 56편. @Cacheable·@CacheEvict로 메서드 결과를 자동 캐싱하는 표준 패턴과 Redis 연동, 키 전략·TTL·자기 호출 함정까지 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 56편 — @Cacheable 캐싱

이 글은 자바 백엔드 입문 시리즈 59편 중 56편이에요. 55편 @Scheduled"반복 실행" 이었다면, 이번 56편은 "같은 결과를 매번 DB 안 가고 메모리에서 반환" 의 표준 패턴 — @Cacheable 로 메서드 결과를 자동 캐싱.

@Cacheable — 메서드 결과 자동 캐싱

같은 매개변수로 호출된 메서드 — 두 번째부터는 "실제 메서드 실행 안 하고 메모리에서 반환" 하는 패턴.

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepo;

    @Cacheable(value = "products", key = "#id")
    public Product findById(Long id) {
        // 첫 호출: DB 조회 → 결과 캐시 저장
        // 두 번째부터: 캐시에서 즉시 반환 (DB 안 감)
        return productRepo.findById(id).orElseThrow();
    }
}

활성화는 @EnableCaching 한 줄.

@SpringBootApplication
@EnableScheduling
@EnableCaching
public class MyApp { ... }

내부 동작 = 23편 AOP 프록시. Spring이 메서드 호출을 가로채 "캐시 있으면 반환, 없으면 메서드 실행 후 저장".

3가지 캐시 어노테이션

어노테이션 동작
@Cacheable 캐시 있으면 반환, 없으면 메서드 실행 후 저장
@CachePut 항상 메서드 실행 + 결과를 캐시에 갱신
@CacheEvict 캐시 무효화 (삭제)
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) { ... }

@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
    return productRepo.save(product);   // 항상 실행 + 캐시 갱신
}

@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
    productRepo.deleteById(id);          // 캐시도 함께 삭제
}

@CacheEvict(value = "products", allEntries = true)
public void deleteAll() { ... }          // 전체 캐시 무효화

세 어노테이션의 조합이 일반 패턴 — 조회는 @Cacheable, 수정은 @CachePut, 삭제는 @CacheEvict.

key — SpEL로 동적 키

캐시 키 생성에 22편 SpEL 활용.

@Cacheable(value = "products", key = "#id")
public Product findById(Long id) { ... }

@Cacheable(value = "search", key = "#status + '_' + #page")
public Page<Product> search(String status, int page) { ... }

@Cacheable(value = "users", key = "#root.methodName + '_' + #user.id")
public UserDetail getDetail(User user) { ... }

#매개변수이름 = 매개변수 참조. #root.methodName = 메서드 이름 자체.

condition·unless — 조건부 캐싱

@Cacheable(value = "products", key = "#id",
           condition = "#id > 0",                // 양수일 때만 캐시
           unless = "#result == null")           // null 결과는 캐시 X
public Product findById(Long id) { ... }

캐시 저장소 — Local·Redis·Memcached

기본 캐시 저장소는 JVM 메모리(Caffeine 등). 한 인스턴스에 한정.

분산 환경 = Redis 연동.

spring:
  cache:
    type: redis
  data:
    redis:
      host: localhost
      port: 6379

spring-boot-starter-data-redis 의존성 추가 + application.yml 설정. 그러면 @Cacheable 캐시 저장소가 자동으로 Redis로 변경. 모든 서버 인스턴스가 같은 Redis 공유.

TTL — 캐시 만료 시간

Redis 캐시는 TTL(Time To Live) 설정 가능.

spring:
  cache:
    redis:
      time-to-live: 600000          # 10분

10분이 지나면 자동 만료 → 다음 호출 시 메서드 재실행 후 캐시 갱신.

캐시별로 다른 TTL 설정:

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
    return RedisCacheManager.builder(factory)
            .withCacheConfiguration("products",
                    RedisCacheConfiguration.defaultCacheConfig()
                            .entryTtl(Duration.ofHours(1)))
            .withCacheConfiguration("hot",
                    RedisCacheConfiguration.defaultCacheConfig()
                            .entryTtl(Duration.ofMinutes(5)))
            .build();
}

캐시 함정 — 자기 호출

23편 AOP 의 자기 호출 함정이 @Cacheable 에도 똑같이.

@Service
public class ProductService {

    public Product findByCode(String code) {
        Long id = lookupId(code);
        return findById(id);           // ❌ 같은 클래스 메서드 호출 — 캐시 안 먹힘
    }

    @Cacheable(value = "products", key = "#id")
    public Product findById(Long id) { ... }
}

해결 = 다른 클래스로 분리. 일반적인 AOP 함정과 동일.

⚠️ 무엇을 캐시할까

변경 거의 없는 데이터(상품 카탈로그·설정값)는 캐시 적합. 실시간성 중요한 데이터(주문 상태·잔고)는 캐시 위험 — 옛 데이터 노출. "읽기 빈도가 쓰기 빈도보다 훨씬 많고, 약간의 지연된 데이터를 허용 가능" 한 시나리오에만.

한 줄 정리 — @EnableCaching + @Cacheable·@CachePut·@CacheEvict 세 어노테이션으로 메서드 결과 자동 캐싱. Redis 연동으로 분산 캐시. TTL·키 전략·자기 호출 함정 주의.

시험 직전 한 번 더 — @Cacheable 입문자가 매번 헷갈리는 것

  • @EnableCaching = 메인 클래스에 박아 활성화
  • @Cacheable = 캐시 있으면 반환, 없으면 메서드 실행 + 저장
  • @CachePut = 항상 실행 + 결과를 캐시에 갱신
  • @CacheEvict = 캐시 무효화
  • value = "캐시이름" = 캐시 그룹
  • key = "#매개변수" = SpEL로 동적 키
  • condition / unless = 조건부 캐싱
  • 기본 캐시 = JVM 메모리 (Caffeine 등)
  • 분산 환경 = Redis 연동
  • application.ymlspring.cache.type=redis
  • TTL = spring.cache.redis.time-to-live
  • 캐시별 다른 TTL = CacheManager Bean 직접 정의
  • 자기 호출 함정 = 같은 클래스 메서드 호출 시 캐시 안 먹힘
  • 해결 = 다른 클래스로 분리
  • 캐시 적합 = 읽기 빈도 ≫ 쓰기 빈도 + 약간의 지연 데이터 OK
  • 캐시 비적합 = 주문 상태·잔고 같은 실시간 데이터
  • AOP 프록시 기반 동작 — public 메서드만 지원
  • 캐시 키 생성 실수 = 모든 호출이 같은 키 → 잘못된 결과 반환
  • 캐시 무효화 정책 = 적극적인 @CacheEvict 또는 짧은 TTL
  • 캐시는 "마지막 최적화 도구" — 먼저 쿼리·인덱스·N+1 개선

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!