[Spring] 캐싱 가이드 (@Cacheable, @CachePut, @CacheEvict, @Caching)

#1. Spring 캐싱

캐싱은 자주 요청되는 데이터를 임시로 저장하여 애플리케이션 성능을 향상시키는 데 중요한 방법입니다.

데이터베이스 검색 및 API 요청과 같은 비용이 많이 드는 작업을 줄여 응답 시간을 단축하고,

백엔드 부담을 줄이며, 사용자 경험을 향상시킵니다.

 

Spring Boot는 캐싱을 간편하게 구현할 수 있도록 다음과 같은 어노테이션을 제공합니다.

  • @Cacheable: 메서드 파라미터를 기반으로 메서드 결과를 캐시합니다.
  • @CachePut: 메서드 결과로 캐시를 업데이트합니다
  • @CacheEvict: 캐시에서 데이터를 제거합니다
  • @Caching: 단일 메서드에 여러 캐싱 어노테이션을 조합할 수 있습니다

#2. Spring 캐시 구현

Spring Boot 애플리케이션에서 캐싱을 활성화하려면 설정 클래스 중 하나에 @EnableCaching 어노테이션을 추가해야 합니다. 이 어노테이션은 각 Spring 빈에서 캐싱 어노테이션을 탐색하는 후처리기를 트리거합니다.

@Configuration
@EnableCaching
public class CacheConfig {
    // ...
}

 

Spring Boot는 기본적으로 ConcurrentHashMap을 사용하여 간단한 인메모리 캐시를 제공합니다.

상품 서비스에 대한 샘플 캐싱 메커니즘을 구현해보겠습니다.

 

  • 상품 모델 정의
@Getter
@ToString
@AllArgsConstructor
public class Product {
    private Long id;
    private String name;
}

 

@Cacheable 사용

@Slf4j
@Service
public class ProductService {

    @Cacheable(value = "products", key = "#id")
    public Product getProductById(Long id) {
        log.info("getProductById 메소드 수행. id = {}", id);
        // 쿼리 수행이 2초 걸린다고 가정한 상황
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Product(id, "제품"+id);
    }
}
  • 위 예제에서 getProductById 메서드는 @Cacheable 로 어노테이션 처리 되었습니다.
  • 특정 id로 메서드를 처음 호출하면 결과가 캐시에 저장됩니다. 동일한 id로 재호출 시 메서드를 재실행하지 않고 캐시에서 결과를 반환합니다.

 

  • 아래와 같은 코드를 통해서 테스트를 진행해봅니다.
@Slf4j
@RestController
@RequestMapping("cache")
@RequiredArgsConstructor
public class CacheController {

    private final ProductService productService;

    @GetMapping("/get/{id}")
    public Product CacheGet(@PathVariable Long id) {
        StopWatch stopWatch = new StopWatch("CacheTest");
        stopWatch.start("상품 조회");
        Product product = productService.getProductById(id);
        stopWatch.stop();
        log.info(stopWatch.prettyPrint());
        return product;
    }
}

 

  • 1번 상품을 2번 조회하고 이어서 2번 상품을 한번 조회합니다.

  • 1번 상품에 대한 두번째 호출은 캐싱이 되어 응답시간이 줄어든 것을 확인할 수 있습니다.

 

@Cacheable을 사용한 조건부 캐싱

  • @Cacheable 어노테이션의 condition과 unless 속성을 사용하면 캐싱 동작을 조건부로 제어할 수 있습니다.이는 특정 조건에서만 캐시하거나, 특정 조건에서 캐시하지 않도록 처리합니다.
  • 예를 들면, id가 10보다 큰 경우에만 캐싱이 발생하도록 설정할 수 있습니다.
@Cacheable(value = "products", key = "#id", condition = "#id > 10")
public Product getProductById(Long id) {
    // ...
}
  • 이 방식을 통해 불필요한 데이터가 캐시에 저장되는 것을 방지하고, 캐시 공간을 효율적으로 사용할 수 있습니다.

 

@CachePut을 사용하여 캐시 업데이트

  • @CachePut을 사용하면 메서드가 호출될 때마다 캐시를 업데이트할 수 있습니다. 이는 데이터가 변경되었을 때 캐시를 최신 상태로 유지하는 데 유용합니다.
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
    // 상품 업데이트 로직
    return product;
}

@CacheEvict를 사용하여 캐시 항목 제거

  • @CacheEvict는 캐시에서 특정 항목을 제거하는 데 사용됩니다. 이는 데이터가 더 이상 유효하지 않을 때(ex. 상품 삭제) 캐시를 무효화하는 데 유용합니다.
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
    // 상품 삭제 로직
}
  • 전체 캐시를 지우려면 allEntries = true 를 사용할 수 있습니다.
@CacheEvict(value = "products", allEntries = true)
public void clearCache() {
    // 캐시 초기화 로직
}

추가 구성 및 모범 사례

  • 캐시 만료 : 오래된 데이터를 방지하기 위해 캐시 만료 시간을 설정
  • 로깅 : 캐시 모니터링을 위해 관련 로깅을 활성화 
  • 과도한 캐싱 방지 : 자주 접근하고 검색 비용이 높은 데이터만 캐시
  • 고유한 캐시 이름 사용 : 멀티 모듈 프로젝트에서 캐시 충돌을 피하기 위해 캐시에 고유한 이름 사용

Spring Boot의 캐싱 기능을 효과적으로 활용하면 애플리케이션의 성능을 크게 향상시킬 수 있습니다.

캐싱 전략을 신중히 계획하고 모범 사례를 따르면 백엔드 부하를 줄이면서 사용자 경험을 개선할 수 있습니다.