개발놀이터

Redis 로 캐싱 구현하기 ver.2 (Low Level 코딩) 본문

CS 지식/데이터베이스

Redis 로 캐싱 구현하기 ver.2 (Low Level 코딩)

마늘냄새폴폴 2023. 5. 15. 15:26

https://coding-review.tistory.com/365

 

Redis를 이용해 캐싱 구현하기 (with Spring)

저번 포스팅에선 Redis의 기본적인 개념에 대해서 알아봤습니다. Redis가 어떻게 NoSQL중에서 가장 빠른 성능을 보여줄 수 있었는지, 자료구조는 어떤 것을 지원하는지, 사용처는 어떤게 있는지, 배

coding-review.tistory.com

우리는 저번 캐싱 구현에서 Spring Data Redis를 적극적으로 활용해 CrudRepository를 상속받아 사용했습니다. 하지만 이 방법은 high level 코딩이었기 때문에 문제가 몇가지 있습니다. 

 

바로 Redis에서 제공해주는 다양한 자료구조 중 Hash 밖에 이용할 수 없다는 것이죠. 

 

Redis에선 String, Set, Sorted Set, Hash, List 이렇게나 많은 자료구조를 지원하는데 그 중 Hash만 사용할 수 있다는 것은 확실히 제 기능을 다 쓰지 못하는 것이죠. 

 

이번 시간에는 Redis를 low level로 사용하는 방법에 대해서 알아보도록 하겠습니다. 

 

package com.hello.capston.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        return redisTemplate;
    }

    @SuppressWarnings("deprecation")
    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager.RedisCacheManagerBuilder builder =
                RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());

        RedisCacheConfiguration configuration =
                RedisCacheConfiguration.defaultCacheConfig()
                        .serializeValuesWith(RedisSerializationContext
                                .SerializationPair
                                .fromSerializer(new GenericJackson2JsonRedisSerializer()))
                        .prefixCacheNameWith("cache")
                        .entryTtl(Duration.ofMinutes(30));
        builder.cacheDefaults(configuration);
        return builder.build();
    }
}

이전에 만들었던 RedisConfig.java 입니다. cacheManager를 빈으로 등록해서 컨트롤러에 붙여서 사용했었죠.

 

이 방식은 프론트에서 사용자의 정보가 필요해 API를 요청하는 경우에 사용하면 좋은 방법입니다. 

 

하지만 우리는 지금 서버 내부에서 캐싱해서 사용하고 싶은 상황입니다. 

 

RedisTemplate 객체를 만들고 빈으로 등록한 다음 Key는 String으로 직렬화할 것이고, Value에 해당하는 값은 Json으로 직렬화하기 위해 GenericJackson2JsonRedisSerializer를 사용하겠습니다. 

 

이제 Key를 만들어주는 Generator를 만들어보도록 하죠. 

 

package com.hello.capston.repository.cache;

public class KeyGenerator {

    private static final String MEMBER_KEY = "member";
    private static final String USER_KEY = "user";

    public static String memberKeyGenerate(String loginId) {
        return MEMBER_KEY + " : " + loginId;
    }

    public static String userKeyGenerate(String email) {
        return USER_KEY + " : " + email;
    }
}

저는 세션에 저장한 Member 객체의 loginId와 User 객체의 email을 가지고 데이터를 가져올 것이기 때문에 파라미터로 loginId와 email을 넣어줬습니다. 

 

이제 Repository를 만들면 됩니다. 

 

package com.hello.capston.repository.cache;

import com.hello.capston.entity.Member;
import com.hello.capston.entity.User;
import com.hello.capston.repository.MemberRepository;
import com.hello.capston.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.concurrent.TimeUnit;

@Repository
@RequiredArgsConstructor
public class CacheRepository {

    private final RedisTemplate<String, Object> redisTemplate;
    private final MemberRepository memberRepository;
    private final UserRepository userRepository;

    public void addMember(Member member) {
        String key = KeyGenerator.memberKeyGenerate(member.getUsername());

        if (!isMemberWasFound(member.getUsername())) {
            redisTemplate.opsForValue().set(key, member);
            redisTemplate.expire(key, 60, TimeUnit.MINUTES);
        }
    }

    public void addUser(Long userId) {
        User findUser = userRepository.findById(userId).orElseThrow(
                () -> new RuntimeException("Not Found User")
        );
        String key = KeyGenerator.userKeyGenerate(findUser.getEmail());
        if (!isUserWasFound(findUser.getEmail())) {
            redisTemplate.opsForValue().set(key, findUser);
            redisTemplate.expire(key, 60, TimeUnit.MINUTES);
        }
    }

    public Member findMemberAtCache(String loginId) {
        String key = KeyGenerator.memberKeyGenerate(loginId);
        return (Member) redisTemplate.opsForValue().get(key);
    }

    public User findUserAtCache(String email) {
        String key = KeyGenerator.userKeyGenerate(email);
        return (User) redisTemplate.opsForValue().get(key);
    }

    private boolean isMemberWasFound(String loginId) {
        Member findMember = findMemberAtCache(loginId);
        if (findMember != null) {
            return true;
        }
        return false;
    }

    private boolean isUserWasFound(String email) {
        User findUser = findUserAtCache(email);
        if (findUser != null) {
            return true;
        }
        return false;
    }
}

RedisTemplate을 주입받아서 만들면 되는데 문법은 아주 간단합니다. 자주 쓰이는 문법만 빠르게 알려드릴게요. 

 

  • redisTemplate.opsForValue() : 이 메서드는 Value를 String으로 저장하는 방식입니다. 
  • redisTemplate.opsForSet() : 이 메서드는 Value를 Set으로 저장합니다. 
  • redisTemplate.opsForHash() : 이 메서드는 Value를 Hash로 저장합니다. 
  • redisTemplate.opsForList() : 이 메서드는 Value를 List로 저장합니다. 
  • set(String Key, Object Value) : 두번째 값이 Object인 이유는 우리가 redisTemplate을 처음 만들 때 Object로 만들었기 때문입니다. 저는 Member, User 객체 그 자체를 넣을 것이기 때문에 Object로 설정했습니다. 
  • redisTemplate.expire(String Key, int timeOut, TimeUnit timeUnit) : TTL을 설정하는 부분입니다. 원래 우리는 Redis 객체를 클래스로 만들고 TTL 설정 값을 부여했었죠? 

 

이렇게 만들면 되겠습니다. 

 

그리고 CacheRepository를 주입받아서 캐싱할 객체를 addMember혹은 addUser를 이용해 저장하고  cacheRepository.findMemberAtCache(String loginId) 를 사용하면 됩니다! 

 

저는 캐싱할 데이터를 저장하는 곳을 OAuth 객체를 만들때와 Spring Security가 Principal을 만들 때 저장했습니다. 

 

 

마치며

여기까지 Redis로 low level 코딩을 해봤습니다. 이전 시간에는 @Cacheable을 이용해서 캐싱하기 때문에 컨트롤러를 타지 않으면 캐싱이 안되는줄 알았습니다. 하지만 Redis의 속도가 워낙 빠르기 때문에 저장하고 가져오는 것이 캐싱하는 효과를 불러 일으킨 다는 것을 알게되었습니다. 

 

주의하셔야할 점은 캐싱을 하면 성능이 좋아지긴 하지만 모든 데이터를 캐싱하지 않으셨으면 좋겠습니다. 값이 자주 바뀌는 경우에는 캐싱을 하는 것이 오히려 좋지 않으니 고정적인 데이터를 캐싱하는 것을 추천드립니다. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~