IT/개발지식

Spring @Cacheable을 내부 메서드에 쓰면 안되는 이유

김솔샤르 2022. 3. 12. 18:52

자주 사용되면서 호출이 빈번한 데이터를 메모리에 쉽게 캐싱할 수 있도록 Spring에서는 @Cacheable을 제공한다. 하지만 아래 예제와 같이 내부 메서드에 @Cacheable을 설정하면 캐싱이 작동하지 않는다.

@Slf4j
@Service
@RequiredArgsConstructor
public class ProductService {	
    private final productJpaRepository productJpaRepository;
    
	public ProductInfo getProductInfo(String productName){
        List<Product> products = productJpaRepository.findAllByProductName(productName);

        long allProductCount = getAllProductCount();

        return ProductInfo.builder()
                .products(products)
                .allProductCount(allProductCount)
                .build();
    }

    @Cacheable
    private long getAllProductCount(){
        log.info(">>> cache is not working");
        return productJpaRepository.count();
    }
 }

전체 상품수를 가져오는 내부 메서드를 캐싱하였다. 만약 캐시가 정상적으로 작동했다면 메서드 내부에 찍어놓은 로그가 콘솔에서 보이지 않을 것이다. 테스트를 해보자.

    @Test
    public void test_getProductInfo() {
        productService.getProductInfo(TEST_TODAY_DATE, TEST_PRODUCT);
        productService.getProductInfo(TEST_TODAY_DATE, TEST_PRODUCT);
    }

캐시 작동 여부를 테스트 하기 위해 일부러 두 번을 호출했다. 로그를 보면..

17:00:06.787 [Test worker] INFO com.solshar.mall.domain.product.service.ProductService - >>> cache is not working
17:00:06.792 [Test worker] INFO com.solshar.mall.domain.product.service.ProductService - >>> cache is not working

두번 다 찍혀있다. 두번째 호출에서 캐시된 데이터를 가져왔더라면 로그가 찍히지 않았을 것이다. 따라서 캐시가 되지 않았다는 뜻인데 이유가 뭘까?


Spring Cache는 AOP를 이용한다

정답은 Spring cache가 AOP(Aspect Oriented Programming)를 통해 이용하기 때문이다. AOP라는 것은 객체지향 프로그래밍처럼 추상적인 개념이고 실제로는 프록시를 통해 동작한다. @Cacheable이 설정된 메서드를 호출할 때, Proxy 객체가 생성되어 해당 호출을 intercept한다. 그리고 Proxy 객체가 다시 @Cachable이 설정된 메서드를 호출하는 식이다.

 

이 Proxy 객체는 외부 메서드 호출에만 관여할 수 있기 때문에 당연히 내부 메서드 호출에는 Cachable이 적용되지 않는 것이다. 

 

해결 방법

내부 호출을 외부 호출로 변경해주면 된다. 가장 간단하고 깔끔한 해결책이다. 개인적으로 선호하는 방법은 Service와 JpaRepository 사이에 프록시 역할을 하는 Repository를 만들고 Service에서 해당 메서드를 호출하도록 변경하는 것이다.

@Slf4j
@Component
@RequiredArgsConstructor
public class ProductRepository {

    private final ProductJpaRepository productJpaRepository;

    @Cacheable(value = "getAllProductCount")
    public long getAllProductCount(){
        log.info(">>> cache is not working");
        return productJpaRepository.count();
    }
}

그리고 동일한 테스트를 해보면

2022-03-12 18:44:21.133  INFO 8496 --- [    Test worker] c.s.m.d.p.repository.ProductRepository   : >>> cache is not working

이번에는 로그가 한번만 찍혀있으니 정상적으로 캐싱이 된 것이다. 이렇게 하는 것이 번거로워보일 수 있지만 고수준의 Domain 로직에서 직접 저수준인 Entity를 바라보지 않게 보호해주는 등 구조적으로도 더 나은 방식인 것 같다. 그 밖에 self-autowiring을 비롯한 여러가지 다양한 방법이 있다고는 하는데 쭉 한번씩 살펴봤으나 개인적으로는 위처럼 하는 것이 가장 낫다고 생각한다.

 

 

참고

https://stackoverflow.com/questions/5152686/self-injection-with-spring/5251930#5251930

 

Self injection with Spring

I tried the following code with Spring 3.x which failed with BeanNotFoundException and it should according to the answers of a question which I asked before - Can I inject same class using Spring? ...

stackoverflow.com

 

반응형