복붙노트

[SPRING] SpringCache를 통해 캐시 된 중첩 된 작업 캐싱

SPRING

SpringCache를 통해 캐시 된 중첩 된 작업 캐싱

DB 조회 횟수를 줄이기 위해 SpringCache 서비스를 사용하기위한 작업이있었습니다. 구현을 테스트하는 동안 캐시 가능한 작업 중 일부가 로그 문을 통해 여러 번 호출되는 것으로 나타났습니다. 캐시 가능 메소드가 캐시 가능한 메소드 내에서 호출되는 경우, 중첩 된 오퍼레이션은 전혀 캐시되지 않는다는 조사 결과가 있습니다. 따라서 중첩 된 작업을 나중에 호출하면 추가 조회가 발생합니다.

문제를 설명하는 간단한 단위 테스트가 아래에 나열됩니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringCacheTest.Config.class} )
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class SpringCacheTest {

  private final static String CACHE_NAME = "testCache";
  private final static Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  private final static AtomicInteger methodInvocations = new AtomicInteger(0);

  public interface ICacheableService {

    String methodA(int length);
    String methodB(String name);
  }

  @Resource
  private ICacheableService cache;

  @Test
  public void testNestedCaching() {

    String name = "test";
    cache.methodB(name);
    assertThat(methodInvocations.get(), is(equalTo(2)));

    cache.methodA(name.length());
    // should only be 2 as methodA for this length was already invoked before
    assertThat(methodInvocations.get(), is(equalTo(3)));
  }

  @Configuration
  public static class Config {

    @Bean
    public CacheManager getCacheManager() {
      SimpleCacheManager cacheManager = new SimpleCacheManager();
      cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache(CACHE_NAME)));
      return cacheManager;
    }

    @Bean
    public ICacheableService getMockedEntityService() {
      return new ICacheableService() {
        private final Random random = new Random();

        @Cacheable(value = CACHE_NAME, key = "#root.methodName.concat('_').concat(#p0)")
        public String methodA(int length) {
          methodInvocations.incrementAndGet();
          LOG.debug("Invoking methodA");
          char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
          StringBuilder sb = new StringBuilder();
          for (int i=0; i<length; i++) {
            sb.append(chars[random.nextInt(chars.length)]);
          }
          String result = sb.toString();
          LOG.debug("Returning {} for length: {}", result, length);
          return result;
        }

        @Cacheable(value = CACHE_NAME, key = "#root.methodName.concat('_').concat(#p0)")
        public String methodB(String name) {
          methodInvocations.incrementAndGet();
          LOG.debug("Invoking methodB");

          String rand = methodA(name.length());
          String result = name+"_"+rand;
          LOG.debug("Returning {} for name: {}", result, name);
          return result;
        }
      };
    }
  }
}

두 가지 방법의 실제 작업은 캐싱을 테스트해야하기 때문에 테스트 케이스 자체에서는 중요하지 않습니다.

나는 어떻게 든 중첩 된 오퍼레이션의 결과가 캐쉬되지 않는 이유를 이해하지만, 중첩 된 캐쉬 가능한 오퍼레이션의 리턴 값에 캐싱을 사용 가능하게하기 위해 사용할 수있는 구성이 있는지 궁금하다.

리팩터링을 통해 중첩 된 작업의 반환 값을 외부 작업에 대한 인수로 제공한다는 것을 알고 있지만이 작업은 여러 가지 작업 (단위 테스트뿐 아니라)을 변경하기 위해 구성 또는 기타 해결 방법 (if if 사용 가능) 우리의 구체적인 경우에 바람직합니다.

해결법

  1. ==============================

    1.문제는 methodB에서 직접 methodA에 액세스하고 있으므로 캐싱 메커니즘을 처리하는 Java 프록시를 통과하지 못하는 것입니다. 또한 @EnableCaching 주석을 추가하지 않았으므로 테스트에서 전혀 캐싱이 없었습니다.

    문제는 methodB에서 직접 methodA에 액세스하고 있으므로 캐싱 메커니즘을 처리하는 Java 프록시를 통과하지 못하는 것입니다. 또한 @EnableCaching 주석을 추가하지 않았으므로 테스트에서 전혀 캐싱이 없었습니다.

    다음 테스트는 Spring에서 만든 프록시를 제대로 통과하면 중첩 된 캐시 패턴이 예상대로 작동 함을 보여줍니다.

    import static org.junit.Assert.*;
    
    import java.util.Arrays;
    import java.util.Random;
    import java.util.concurrent.atomic.AtomicInteger;
    
    import javax.annotation.Resource;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.CacheManager;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.cache.annotation.EnableCaching;
    import org.springframework.cache.concurrent.ConcurrentMapCache;
    import org.springframework.cache.support.SimpleCacheManager;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.test.annotation.DirtiesContext;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = { SpringCacheTest.Config.class })
    @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
    public class SpringCacheTest {
    
        private final static String CACHE_NAME = "testCache";
        private final static AtomicInteger methodInvocations = new AtomicInteger(0);
    
        public interface ICacheableService {
    
            String methodA(int length);
    
            String methodB(String name);
        }
    
        @Resource
        private ICacheableService cache;
    
        @Test
        public void testNestedCaching() {
    
            String name = "test";
            cache.methodB(name);
            assertEquals(methodInvocations.get(), 2);
    
            cache.methodA(name.length());
            // should only be 2 as methodA for this length was already invoked before
            assertEquals(methodInvocations.get(), 2);
        }
    
        @Configuration
        @EnableCaching
        public static class Config {
    
            @Bean
            public CacheManager getCacheManager() {
                SimpleCacheManager cacheManager = new SimpleCacheManager();
                cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache(CACHE_NAME)));
                return cacheManager;
            }
    
            @Bean
            public ICacheableService getMockedEntityService() {
                return new ICacheableService() {
                    private final Random random = new Random();
    
                    @Autowired
                    ApplicationContext context;
    
                    @Override
                    @Cacheable(value = CACHE_NAME, key = "#root.methodName.concat('_').concat(#p0)")
                    public String methodA(int length) {
                        methodInvocations.incrementAndGet();
                        System.out.println("Invoking methodA");
                        char[] chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toCharArray();
                        StringBuilder sb = new StringBuilder();
                        for (int i = 0; i < length; i++) {
                            sb.append(chars[random.nextInt(chars.length)]);
                        }
                        String result = sb.toString();
                        System.out.println("Returning " + result + " for length: " + length);
                        return result;
                    }
    
                    @Override
                    @Cacheable(value = CACHE_NAME, key = "#root.methodName.concat('_').concat(#p0)")
                    public String methodB(String name) {
                        methodInvocations.incrementAndGet();
                        System.out.println("Invoking methodB");
                        ICacheableService cache = context.getBean(ICacheableService.class);
                        String rand = cache.methodA(name.length());
                        String result = name + "_" + rand;
                        System.out.println("Returning " + result + " for name: " + name);
                        return result;
                    }
                };
            }
        }
    }
    
  2. from https://stackoverflow.com/questions/29562642/caching-of-nested-cacheable-operation-via-springcache by cc-by-sa and MIT license