회사에서 DB 부하를 감소시키고, 응답속도를 개선하기 위해 로컬 캐시를 사용중이다. 캐시를 그냥 생각없이 사용할 때는 “캐시하니까 어떤 쿼리를 날려도 상관 없지 않을까?”라는 안일한 생각을 했었다.

동시성을 공부하다가 문득, 캐시가 삭제되고, 캐시를 갱신하는 시점에 동시에 요청이 몰려 들어오게되면 어떻게 될까? 그런데 하필 쿼리가 슬로우 쿼리라서 시간이 오래 걸린다면 어떤 영향이 있을까? 라는 궁금증이 생겼다.

로컬 캐시의 작용 기전은 다음과 같다

  1. 캐시가 있는지 확인한다

    → 캐시가 있으면 캐시 결과를 반환한다

  2. 캐시가 없을 경우, 메서드 내 로직을 수행한다

  3. 메서드 내 로직이 완료되고, 결과가 리턴되면 캐시가 완료된다

위 내용을 토대로, 아래의 가정을 해보았다.

  1. 주기적으로 도는 스케줄러가 캐시를 삭제했다
  2. 캐시를 갱신하는 메서드를 실행했다
  3. 그런데 캐시를 위한 쿼리가 수행되는 데 N초가 걸렸다
  4. N초동안 들어오는 요청들에 대해서는 모두 캐시를 사용하지 못하고, 해당 요청 모두 N초가 걸릴 것이다

실험을 수행했다

// 캐시 갱신 메서드 
@Cacheable(cacheNames = "Post", key = "'all'", cacheManager = "defaultCacheManager")
public List<Post> getPosts(LocalDateTime localDateTime) throws InterruptedException {
    log.info("[{}] getPosts 호출", Thread.currentThread().getName());
    List<Post> result = postRepository.findAll();  // 쿼리
    Thread.sleep(4000);  // 4초간 대기 
    log.info("[{}] query 결과 획득", Thread.currentThread().getName());
    return result;
}

// 캐시 테스트
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CacheTest {
    @LocalServerPort
    private int port;

    private RestTemplate restTemplate;

    @BeforeEach
    void setUp() {
        restTemplate = new RestTemplateBuilder()
                .rootUri("<http://localhost>:" + port)
                .build();
    }

    @Test
    void name() {
        try (ExecutorService threadPool = Executors.newFixedThreadPool(10)) {
            for (int i = 0; i < 10; i++) {  // 스레드 풀을 이용해 10회 요청 
                threadPool.submit(() -> {
                    restTemplate.getForEntity("/posts", String.class);
                });
            }
        }
    }
}

내 가정대로라면, 쿼리가 갱신되는 4초간 들어오는 요청은 모두 캐시를 이용하지 못하고 쿼리가 날아갈 것이다.