본문 바로가기

Spring Boot/Cache

Spring Cache 기본 사용 : Spring Cache 활성화 및 주요 캐시 애너테이션

 - Spring Cache의 기본 개념, 동작 원리 이해

 - @EnableCaching을 통해 캐시 기능을 활성화 하는 법

 - @Cacheable, @CachePut, @CacheEvict, @Caching, @CacheConfig 애너테이션의 역할과 사용법

 - 캐시의 생성, 갱신, 삭제 과정을 코드 예제로 실습

 - 각 애너테이션이 실무에서 어떤 상황에서 사용되는가?

 


Spring Cache 기본 개념

메서드의 실행 결과를 캐시에 저장하고, 같은 입력값으로 다시 호출될 때 DB나 외부 API 호출없이 캐시된 결과를 반환하는 기능

성능 향상과 부하 감소를 위해 매우 자주 사용되는 패턴임

 

1. 핵심 동작 원리

1) 메서드가 처음 호출되면, 결과가 캐시에 저장됨 - > 메서드 실행됨

2) 동일한 파라미터로 다시 호출 시, 캐시된 값을 반환함 - > 메서드 실행 x

3) 데이터 변경시 캐시를 갱신(@CachePut)하거나, 삭제(@CacheEvict)하여 일관성을 유지할 수 있음

 

 

2. Spring Cache 활성화

캐시 기능은 기본적으로 비활성되어 있으므로, 명시적으로 활성화해줘야 함

 

1) 설정 클래스 작성

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {
}

 

* @EnableCaching을 통해 캐시를 활성화함

* CacheConfig 안에 필요한경우, 캐시 매니저를 직접 정의할 수 있음

 

 

2) CacheManager 자동 등록

Spring Boot에서는 기본적으로 SimpleCacheManager를 자동으로 등록함

특별한 설정이 없으면 ConcurrentMapCacheManager가 사용되어, 메모리 기반 캐시가 동작함

 

 

3) 구조 예시

cachetest/

 > config/

 > > CacheConfig.java << 여기에서 캐시 설정을 한다

 > service/

 > > ProductService.java

 > controller/

 > > ProductController.java

 


주요 캐시 애너테이션

1. @Cacheable

메서드 실행 결과를 캐시에 저장하고, 동일 인자로 호출시 저장된 값을 반환하는 애너테이션

import com.b1uffer.cachetest.entity.Product;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ProductService {
    
    @Cacheable("products")
    public Product findProductById(Long id) {
        System.out.println("DB에 접근함");
        return new Product(id, "노트북", 120000);
    }
}

 

* @Cacheable("products")의 의미 : 캐시 이름이 products인 저장소에 결과를 저장함

* 동일한 ID로 다시 호출되면 DB접근 없이 캐시된 결과를 반환함(메서드 실행 x)

 

실행 흐름

캐시 실행 흐름

 

 

조건부 캐싱

conditionunless 속성을 사용하면 캐시 조건을 제어할 수 있다고 함

import com.b1uffer.cachetest.entity.Product;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @Cacheable(value = "products", condition = "#id > 0", unless = "#result == null")
    public Product findProductById(Long id) {
        System.out.println("DB에 접근함");
        return new Product(id, "노트북", 120000);
    }
}

 

이런 느낌으로..

 

* condition 속성 : 캐싱을 수행할 조건을 지정함(true일때만 저장함)

* unless 속성 : 메서드를 실행하고 결과에 따라 저장 여부를 결정함

 

 

 

2. @CachePut

항상 메서드를 실행하고, 실행 결과를 캐시에 강제로 갱신하는 애너테이션

import com.b1uffer.cachetest.entity.Product;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @CachePut(value = "products", key = "#product.id")
    public Product updateProduct(Product product) {
        System.out.println("DB에서 제품 업데이트 후 캐시 갱신");
        return product;
    }
}

 

* CachePut에서 key 속성을 입력할 때 입력하고자 하는 값에 대한 get 메서드가 있어야한다(getId 이런것 등)

 

비교

구분 @Cacheable @CachePut
메서드 실행 여부 캐시에 없을때만 실행함 메서드를 항상 실행함
캐시 저장 지점 메서드 실행 후 결과를 저장함 메서드 실행 후 강제로 저장함
사용 목적 조회 성능 향상 데이터 갱신 후 캐시 동기화

 

 

3. @CacheEvict

캐시된 데이터를 제거할 때 사용함

import com.b1uffer.cachetest.entity.Product;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(Long id) {
        System.out.println("제품을 삭제하고 캐시를 제거함");
    }

    @CacheEvict(value = "products", allEntries = true)
    public void clearCache() {
        System.out.println("전체 캐시를 제거함");
    }
}

 

* allEntries 속성을 활용해서 전체 캐시를 삭제할수도 있음

 

beforeInvocation 속성

 - beforeInvocation = true > > 메서드를 실행하기 전에 캐시를 삭제함

 - 기본값은 false(메서드를 실행하고 나서 삭제함)

 

 

4. @Caching

여러 캐시 동작을 묶어서 수행할 수 있는 애너테이션

import com.b1uffer.cachetest.entity.Product;
import com.b1uffer.cachetest.repository.ProductRepository;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {
    private ProductRepository productRepository;

    @Caching(
            put = {@CachePut(value = "products", key = "#product.id")},
            evict = {@CacheEvict(value = "products", key = "'list'")}
    )
    public Product saveProduct(Product product) {
        return product;
    }

    @Cacheable(value = "products", key = "'list'")
    public List<Product> getProducts() {
        return productRepository.findAll();
    }
}

 

* 제품을 추가하면 상세 캐시를 갱신하면서, 목록 캐시(list)는 제거하는 로직이다.

* Cacheable은 캐시DB에서 조회한 후 없으면 메서드를 실행하고 캐시를 저장하는 애너테이션

 

 

5. @CacheConfig

클래스 단위에서 공통 캐시 이름, 키 생성 정책 등을 지정할 수 있는 애너테이션

import com.b1uffer.cachetest.entity.Product;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@CacheConfig(cacheNames = "products")
public class ProductService {

}

 

* @CacheConfig(cacheNames = "products")라고 지정해주어 클래스 전체에 기본 캐시 이름을 설정한다

* 이를 통해 각 메서드마다 value를 중복해서 정의할 필요가 없어져서 깔끔해짐

 


* 캐시를 너무 광범위하게 사용하면, 데이터 불일치(inconsistency)가 발생할 수 있으니 주의

* 변경이 잦은 데이터보다는 조회 빈도가 높고 변경이 드문 데이터에 적합함

* 캐시 이름(cacheNames)은 기능별로 구분하는게 좋다. (userCache, productCache, ...)

* 다중 서버 환경에서는 로컬 캐시보다 분산 캐시(Redis, Caffeine 등)를 사용하는게 일반적임

 


정리

애너테이션 설명 주요 속성
@Cacheable 결과를 캐시에 저장하고,
동일 인자라면 캐시에서 반환함
value, key, condition, unless
@CachePut 메서드를 실행하고 캐시를 강제로 갱신함 value, key
@CacheEvict 캐시를 제거함 value, key, allEntries,
beforeInvocation
@Caching 복합 캐시 동작 정의 cacheable, put, evict
@CacheConfig 클래스 단위 공통 설정 cacheNames, keyGenerator 등