
Spring-Cache는 Redis, Caffeine, EHCache 등 다양한 캐시 구현체를 공통된 방식으로 사용할 수 있도록 제공하는 캐시 추상화 계층이다. 애플리케이션 코드는 캐시 구현체에 직접 의존하지 않고, 어노테이션 기반으로 캐싱 로직을 선언적으로 적용할 수 있다. 이로 인해 캐시 교체 비용이 낮아지고, 비즈니스 로직과 캐시 로직의 결합도가 크게 줄어든다.
Spring-Cache 어노테이션 개념
Spring-Cache에서 가장 핵심이 되는 어노테이션은 @Cacheable, @CachePut, @CacheEvict, @Caching 이다. 이 어노테이션들은 메서드 단위로 캐싱 동작을 정의하며, 메서드 호출 시점에 캐시를 조회하거나 갱신하거나 제거하는 역할을 수행한다. 이때 개발자는 캐시 접근 로직을 직접 작성하지 않고, 선언적으로 캐싱 정책만 기술하면 된다.
@Cacheable은 메서드 호출 결과를 캐시에 저장하고, 이후 동일한 키로 호출될 경우 메서드를 실행하지 않고 캐시된 결과를 그대로 반환한다. key, condition, unless, sync 같은 속성을 통해 캐시 키 생성 방식과 캐싱 조건을 세밀하게 제어할 수 있다. 특히 unless는 메서드 실행 결과를 기준으로 캐싱 여부를 결정할 수 있다는 점에서 자주 활용된다.
@CachePut은 @Cacheable과 달리 항상 메서드를 실행한 뒤 결과를 캐시에 반영한다. 데이터 변경이 발생하는 메서드에서 캐시를 최신 상태로 유지하기 위해 사용된다. @CacheEvict는 캐시 데이터를 제거하는 역할을 수행하며, allEntries, beforeInvocation 같은 옵션을 통해 제거 범위와 시점을 제어할 수 있다. @Caching은 여러 캐시 어노테이션을 조합해 하나의 메서드에 복합적인 캐싱 정책을 적용할 수 있게 해준다.
Spring-Cache 캐시솔루션 비교
Spring-Cache는 캐시 구현체와 독립적인 구조를 제공하기 때문에 어떤 캐시 솔루션을 사용할지는 시스템 특성에 따라 선택할 수 있다. 대표적인 캐시 솔루션으로는 Caffeine과 Redis가 있다. 두 솔루션은 구조와 사용 목적이 명확히 다르다.
Caffeine은 JVM 내부에서 동작하는 로컬 캐시다. 네트워크 지연이 없기 때문에 성능이 매우 빠르며, 구조가 단순하고 설정이 간편하다. 하지만 JVM 종료 시 데이터가 모두 사라지며, 단일 서버 환경에서만 사용 가능하다. 따라서 단일 애플리케이션, 짧은 수명의 데이터 캐싱에 적합하다.
Redis는 분산 캐시 서버로, 여러 애플리케이션 인스턴스가 하나의 캐시를 공유할 수 있다. AOF, RDB 같은 영속성 옵션을 통해 데이터 지속성을 확보할 수 있으며, 레플리케이션과 클러스터링을 통한 고가용성 구성도 가능하다. 네트워크 비용으로 인해 로컬 캐시보다는 느리지만, 분산 환경에서는 사실상 표준 선택지다. Spring-Cache는 이러한 차이를 감추고 동일한 어노테이션 모델로 사용할 수 있게 해준다.
Spring-Cache AOP 동작원리
Spring-Cache가 동작하는 핵심 메커니즘은 AOP이다. @Cacheable과 같은 어노테이션이 붙은 메서드는 스프링 AOP 프록시에 의해 감싸진다. 클라이언트 코드가 해당 메서드를 호출하면, 실제 메서드 실행 전에 프록시가 개입하여 캐시 조회, 캐시 저장, 캐시 제거 등의 로직을 수행한다.
이 구조 때문에 중요한 제약이 하나 존재한다. 같은 클래스 내부에서 this.method() 형태로 캐시 어노테이션이 붙은 메서드를 호출하면 AOP 프록시를 우회하게 된다. 이 경우 캐싱 로직은 전혀 적용되지 않는다. 이는 Spring AOP가 프록시 기반으로 동작하기 때문에 발생하는 대표적인 주의사항이다.
이를 해결하기 위해서는 캐시 대상 메서드를 다른 빈에서 호출하거나, 내부 클래스로 분리하여 프록시를 거치도록 설계해야 한다. 즉, 캐싱은 “프록시를 통해 호출될 때만” 동작한다는 점을 항상 염두에 두어야 한다. Spring-Cache는 매우 편리한 추상화이지만, AOP 기반이라는 특성을 이해하지 못하면 의도하지 않은 캐시 미적용 문제를 겪게 된다.