ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 디프만 똑스 서버에서 캐싱 공통 모듈 만들기
    극락코딩 2023. 8. 17. 23:34

    디프만 12기에서 진행한 똑스라는 프로젝트가 있다. 12기 활동 진행시에는 운영진으로 참여하여 대부분의 구현 작업에 참여하지는 않았는데.. 12기 종료후에 본격적으로 개발 작업에 참여하게 되었다.

     

    개발 작업에 착수하며, 몇가지 문제점을 파악했는데, 그 중에서 캐싱 부분에 대해 확인해보겠다.

     

    문제가 되는 부분

    1. 자주 사용되는, 하지만 변경이 일어나지 않는 데이터에 대한 캐싱이 없다.

    2. 그렇기에 데이터 조회시 레이턴시가 상당하다.

    3. 조회 성능이 중요한 api의 레이턴시가 300ms를 넘게 초과한다.

     

    해결방안

    1. Network IO를 가능한 적게 타도록 구성

    2. 변경이 적은 데이터에 대한 레디스 캐싱 작업 진행

     

    api의 레이턴시를 낮추기 위해, 가장 좋은 방법은 network IO를 가능한 줄이는 방법이다.

    그중에서 가장 많이 사용하는 Redis Caching 전략을 해당 프로젝트에 도입했다.

     

    프로젝트 내부에서, 어느 곳이든 사용 가능하도록 분리

    @Service
    @RequiredArgsConstructor
    public class CacheService {
        private final StringRedisTemplate redisTemplate;
    
        public <T> T getOrNull(Cache<T> cache) {
            var data = redisTemplate.opsForValue().get(cache.getKey());
            return (data != null) ? MapperUtil.readValue(data, cache.getType()) : null;
        }
    
        @SneakyThrows
        public <T> T get(Cache<T> cache, Callable<T> callable) {
            var data = redisTemplate.opsForValue().get(cache.getKey());
    
            if (data == null) {
                var dataObject = callable.call();
    
                asyncSet(cache, dataObject);
    
                return dataObject;
            } else {
                return MapperUtil.readValue(data, cache.getType());
            }
        }
    
        public <T> void set(Cache<T> cache, T data) {
            redisTemplate.opsForValue().set(
                    cache.getKey(),
                    MapperUtil.writeValueAsString(data),
                    cache.getDuration()
            );
        }
    
        @Async
        public <T> void asyncSet(Cache<T> cache, T data) {
            CompletableFuture.runAsync(() -> set(cache, data));
        }
    
        public <T> void delete(Cache<T> cache) {
            redisTemplate.delete(cache.getKey());
        }
    
        @Async
        public <T> void asyncDelete(Cache<T> cache) {
            CompletableFuture.runAsync(() -> delete(cache));
        }
    
        public <T> Long increment(Cache<T> cache) {
            return redisTemplate.opsForValue().increment(cache.getKey());
        }
    
        public <T> Long decrement(Cache<T> cache) {
            return redisTemplate.opsForValue().decrement(cache.getKey());
        }
    }

     

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Cache<T> {
        private String key;
        private Class<T> type;
        private Duration duration;
    }

     

    Redis에 데이터를 저장하고 조회하는 것도 결국에는 Network IO를 타게 된다.

    그래서 캐싱 전략에도 비동기 처리가 가능하도록 수적 작업을 진행했다.

    @Async의 기본 executor는 taskExecutor이다.

     

     

    캐싱이 되어있다면 바로 데이터를 반환하고, 캐싱된 데이터가 없다면 Redis 조회후 반환 (insert하는 것은 별도 스레드에서 진행)

    public QuizModel getCachedQuiz(Long quizId) {
        return cacheService.get(CacheFactory.makeCachedQuiz(quizId), () -> {
            var quiz = quizRepository.findQuizByIdAndDeleted(quizId, false)
                    .orElseThrow(() -> new ApplicationErrorException(ApplicationErrorType.NOT_FOUND_QUIZ_ERROR));
    
            return QuizModel.from(quiz);
        });
    }

     

    그래서 결론은

    캐싱 진행전 api latency : 300~500

    캐싱 이후 api latency : 20~30

     

    물론, 캐싱만으로 이정도 성능 상승이 된 것은 아니다. (다른 각각의 모듈들을 전부 비동기로 돌려 join했다.)

    그래도 자주 사용되는 데이터에 대한 캐싱을 통해 Network IO를 줄일 수 있다.

     

    여기서 잠깐 질문

    왜 @Cacheable 을 쓰지 않았나? -> 직렬화, 역질렬화의 고통에서 해방되기 위하여.. 최근에는 mapper를 통해 직접 말아주는 방식이 조금 더 정싱적으로 편안하다.

     

    - GITHUB Repo

    '극락코딩' 카테고리의 다른 글

    온디맨드가 뭔디?  (1) 2023.08.20
    Redis Pub-Sub 사용하기  (0) 2023.08.19
    Http에서 국가 정보 얻기  (0) 2023.08.17
    Kotlin에서 Builder Pattern을?  (0) 2023.08.15
    ThreadLocal을 간단하게 살펴보기  (0) 2023.08.15
극락코딩