본문 바로가기

Language & Framework/삽질기록

삽질 기록(17) 레디스로 데이터를 캐싱해보자

 

우리 프로젝트 메인화면에는 랭킹 조회가 있는데, 생각보다 필요로하는 데이터가 많다.

사실 지금 화면에서는 고작 랭킹과 유저 정보, 프로필 이미지만 나오고 있어서 별로 그렇지 않아보이겠지만..

프론트 팀원분들의 시간 관계상 이 상태에서 개발이 멈춰버렸을 뿐 ㅎ;

 

원래 기획으로는 유저가 작성한 게시글 수, 답글 수, 받은 좋아요 수, 댓글 수 등의 정보가 표시되는 것이였고, 백엔드에서는 해당 정보를 실제로 다 보내주고 있기 때문에.. 나의 허접한 쿼리 + 프리티어 rds의 느려 터진 성능이 만나서 이 부분을 개선하느라 애를 먹었었다.

(내 생각에는 테이블이 너무 많이 일어나기 때문에 이 정보들을 합치는 별도의 테이블을 만들던가 레디스의 SortedSets으로 랭킹을 관리해줘야 할 것 같다.)

 

근데 생각해보면 이게 게임의 랭킹도 아니고 커뮤니티 랭킹인데 매번 실시간 데이터를 보여줄 필요가 있을까?

내 생각에는 1시간에 한 번 정도만 갱신되어도 충분할 것 같다.

 

또한 이외에도 3개월간의 채용 데이터를 받아와서 캘린더에 보여주는데, 어차피 이건 새벽 4시마다 크롤링하기 때문에 하루 동안은 캐싱한 데이터만 보여줘도 될 것이다.

전체 게시글 같은 경우 새로운 게시글이 작성될 때 CacheEvict로 강제 갱신하고, 그 외에는 10분 내에 한 번씩 갱신하면 될 것 같다.

 

마침 리프레시 토큰과 이메일 인증키 때문에 Redis도 사용하고 있으니 Redis Cache에 랭킹 데이터를 적용해놓고 1시간마다 갈아주면 좋을 것이라고 판단하여 바로 실천에 옮기기로 했다.

 

참고로 레디스를 사용하지 않고 자체 서버에 캐싱하는 방법도 존재하며, 당연히 외부 통신 과정이 없기 때문에 더 빠르다.

우리의 프로젝트가 분산 환경이 아니며, 큰 용량을 차지하지도 않기 때문에 사실 지금 같은 경우는 자체 서버에 저장하는 게 더 적절하다는 게 내 판단이다. (확실하지 않음)

그러나 굳이 이번 포스팅에서 레디스에 캐시를 저장하고 있는 이유는 이 쪽이 조금이나마 설정이 더 까다롭고 내가 할 줄 몰라서 시도해보는 목적이 더 크다. 자체 서버에 캐싱하는 방법이 궁금하다면 EhCache를 검색해보자.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    @Bean
    public RedisCacheManager redisCacheManager() {
        RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));
 
        HashMap<String, RedisCacheConfiguration> cacheConfiguration = new HashMap<>();
        cacheConfiguration.put(MAIN_ARTICLE_LIST, redisCacheConfig.entryTtl(Duration.ofMinutes(3)));
        cacheConfiguration.put(USER_RANK, redisCacheConfig.entryTtl(Duration.ofHours(1)));
        cacheConfiguration.put(JOB_CALENDAR, redisCacheConfig.entryTtl(Duration.ofDays(1)));
 
        return RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory())
                .cacheDefaults(redisCacheConfig)
                .build();
    }
cs

 

기존에 사용하던 레디스의 Configuration 파일에 RedisCacheManager의 Bean을 만들었다.

key는 String, value는 Json으로 직렬화하도록 했으며, 적용할 캐시 지속시간이 다르기 때문에 cacheConfiguration이라는 map을 만들어 관리할 것이다.

 

 

 

 

1
2
3
4
5
6
7
8
    @Cacheable()
    public ResponseMultiplePaging<UserDto.ResponseRanking> getUserRankList(PageRequest request) {
        Page<User> page = userQueryRepository.getRankData(request);
        List<UserDto.ResponseRanking> dto = userMapper.toResponseRankDto(page.getContent());
        reorderRank(request, dto);
 
        return new ResponseMultiplePaging<>(dto, page);
    }
cs

 

위에 저렇게 Cacheable 붙여주고 속성값만 적용해주면 되는데, 인터넷 보고 대충 베껴서 적지 말고 소스코드를 읽어보자.

읽어도 모르겠으면 그 때 찾아봐도 늦지 않다.

읽어도 무슨 말하는지 반도 모르는 경우가 대부분인데, 그래도 내 블로그 같은 거 보면서 따라 치는 것보다는 소스코드 읽어보고 때려 맞추는 게 낫다.

 

대충 읽어본 속성 목록

value = cache names.

cacheNames = 메서드 호출 결과가 저장되는 캐시 이름. 빈 이름이나 qualifier value(수동 등록한 빈 이름)로  타겟 캐시를 판별하는데 사용된다.

key = SpEL (Spring Expression Language)를 사용하고, custom keyGenerator를 설정하지 않는 한 모든 매개 변수가 키로 간주된다고 한다.

#root.method, #root.target, #root .caches 메서드, 대상, 타겟에 대한 참조고 뭐 인덱스로도 접근할 수 있고 어쩌고 저쩌고..

keyGenerator = 사용자 정의로 만들 수 있다는데 뭔지 모르겠고 지금은 안 궁금하네요

cacheManager = 빈 이름으로 캐시 매니저를 별도로 선택할 수 있는 듯

cacheResolver = 사용자 지정 cacheResolver 이름 쓰래요

condition = SPeL로 캐싱 조건 표현할 때 쓰는 속성. 디폴트는 항상 캐싱되는 것. 문법은 위의 key와 갖다고 합니다.

unless = 또 SpEL 어쩌고 저쩌고.. 이 조건이 true면 캐싱하지 않는다고 합니다. 설명이 조금 더 있는데 지금 저한테는 필요 없는 거라서 더 안읽음.

snyc = 각 쓰레드가 같은 키를 동시에 호출할 때 필요한 어쩌고 저쩌고..

 

대충 보니까 저한테 필요한 건 value랑 key 밖에 없는 듯.

value에는 우리가 수동으로 설정해준 configuration의 이름을 넣어주면 되고 key에는 말 그대로 key가 될 파라미터를 넣으면 된다.

예를 들어 게시판의 각 글을 모두 캐싱하고 싶다면 게시판의 글 번호를 key로 지정하면 되는 것 같다.

 

참고로 레디스에 캐시로 저장할 데이터 (즉, 클라이언트에게 반환하는 반환값)는 모두 Serializable을 구현해야 하는데, 이 때 엔티티를 직렬화하면 망한다.

지금 당장 JsonManagedReference 이런 어노테이션 덕지덕지 붙여서 어떻게든 회피하더라도 나중에 후회할 일이 생긴다.

redis에 토큰 저장할 때 귀찮아서 그렇게 했다가 피눈물 흘렸으니 그냥 하지 말자.

 

여기까지 따라하면..

 

 

 

 

 

펑~ 망한다.

모든 경우에 망하는 건 아니고, 반환값에 Page 객체 혹은 LocalDateTime 객체가 들어있으면 망한다.

이 중 LocalDateTime 때문에 망하는 건 쉽게 해결할 수 있으나, Page 객체로 인해 터지는 문제는 하나하나 일일히 TypeReference를 지정해주던지, 아니면 PageImpl 객체를 상속 받아서 우리가 JsonProperty를 지정해주던지 해야하는데.. 우리 프로젝트에는 이 Dto를 사용 중인 서비스가 너무 많아서 그랬다간 대참사가 일어난다.

 

(혹시나 해결하고 싶다면 Redis page deserialization이라고 검색해라. Stackoverflow에 해답이 있다.)

 

그렇게 한참을 해메던 도중 value를 기본 Serializer인 JdkSerializationRedisSerializer()로 지정하면 당장 동작 가능한 정도로는 해결된다는 사실을 알게 되었다.

 

 

1
2
3
4
5
6
7
8
9
        RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration
                .defaultCacheConfig()
                .disableCachingNullValues()
                .serializeKeysWith(RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(new JdkSerializationRedisSerializer()));
cs

 

이렇게 수정해주고 요청을 날려보면 ?

 

1차 요청

 

 

2차 요청

 

 

 

2차 요청부터는 응답 시간이 10베 빨라진 것을 확인할 수 있다.

 

사실 이런 장난감 프로젝트에서는 이렇게 재미삼아 아무거나 캐싱해도 별 문제가 없겠지만, 실제 서비스에서는 메모리 용량이나 캐시 히트율을 신중하게 고려해서 캐싱을 해야 한다고 한다.

캐시에 대해서도 보다 깊게 공부해보고 싶다.