개발놀이터

Redis for Client Side Caching (실습) 본문

CS 지식/데이터베이스

Redis for Client Side Caching (실습)

마늘냄새폴폴 2024. 7. 4. 22:22

전 포스팅에 이어 바로 실습으로 들어가보겠습니다. 전 내용이 궁금하시면 아래의 링크를 참고해주세요!

 

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

 

Redis for Client Side Caching (이론)

이번 포스팅은 Redis 6부터 제공해주는 신기능 (무려 4년전 기술인 따끈따끈한 신기술입니다.) Redis for Client Side Caching에 대해서 공부해봤습니다.  Client Side Caching사실 이 내용을 공부하게 된 계기

coding-review.tistory.com

 

@Configuration
@EnableRedisHttpSession
@EnableRedisRepositories
@Slf4j
public class RedisConfig {

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

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

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

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    @Bean
    public RedisClient redisClient() {
        return RedisClient.create(String.format("redis://%s:%d", host, port));
    }

    @Bean
    public StatefulRedisConnection<String, String> connection() {
        return redisClient().connect();
    }

    @Bean
    public RedisCommands<String, String> redisCommands() {
        return connection().sync();
    }
    
    @Bean
    public CacheFrontend<String, String> frontend(StatefulRedisConnection<String, ItemDto> connection) {
        Map<String, String> clientCache = new ConcurrentHashMap<>();
        return ClientSideCaching.enable(CacheAccessor.forMap(clientCache), connection, TrackingArgs.Builder.enabled());
    }

    @PostConstruct
    public void init() {
        RedisCommands<String, String> commands = redisCommands();
        commands.set("test", "hello world!"));
    }
}

 

우리의 핵심이라고 할 수 있는 RedisConfig.java입니다. 

 

이제 모든 준비는 끝났습니다. 

 

바로 사용해보죠. 

 

@Controller
@RequiredArgsConstructor
@Slf4j
public class ItemController {

    private final CacheFrontend<String, String> frontend;

    @GetMapping("/get/local")
    @ResponseBody
    public String getMessage() {
        return frontend.get("test");
    }
}

 

정말 너무 간단하게 사용할 수 있지만 말도안되는 기능이지 않을 수가 없습니다. 

 

  • Redis에 데이터를 요청하는 네트워크 비용
  • Redis와 커넥션 맺는 비용
  • Redis에서 하드웨어에 있는 데이터를 읽는 비용
  • 다시 스프링에 전달해주는 비용

이 모든게 생략되는 기술입니다. 

 

한번 테스트해보겠습니다. 

 

 

일단 저희가 생성한 텍스트가 잘 보입니다. 이제 Redis CLI에서 값을 변경해보겠습니다. 

 

 

 

진짜로 동기화가 되어서 바뀌는 것을 확인할 수 있습니다. 

 

그런데... 뭔가 아쉽습니다. 우리는 단순 텍스트를 캐싱하진 않거든요. JSON이 캐싱되면 좋겠습니다. 

 

이를 위해서 몇가지 추가적인 작업이 필요합니다. 

 

@Configuration
@EnableRedisHttpSession
@EnableRedisRepositories
@Slf4j
public class RedisConfig {

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

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

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

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    @Bean
    public RedisClient redisClient() {
        return RedisClient.create(String.format("redis://%s:%d", host, port));
    }

    @Bean
    public StatefulRedisConnection<String, ItemDto> connection() {
        return redisClient().connect(new ItemDtoCodec());	// 이부분 중요!
    }

    @Bean
    public RedisCommands<String, ItemDto> redisCommands() {
        return connection().sync();
    }

    @Bean
    public CacheFrontend<String, ItemDto> frontend(StatefulRedisConnection<String, ItemDto> connection) {
        Map<String, ItemDto> clientCache = new ConcurrentHashMap<>();
        return ClientSideCaching.enable(CacheAccessor.forMap(clientCache), connection, TrackingArgs.Builder.enabled());
    }

    @PostConstruct
    public void init() {
        RedisCommands<String, ItemDto> commands = redisCommands();
        commands.set("test", new ItemDto("itemName", 1000, "localhost:8080", 3L));
    }
}

 

우선 ItemDto를 하나 만들어줍니다. 우리는 이포맷으로 JSON을 내려줄겁니다. 

 

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ItemDto {

    private String itemName;
    private int price;
    private String url;
    private Long itemId;
}

 

그리고 ItemCodec이라는 클래스를 하나 만들어줍니다. 

 

public class ItemDtoCodec implements RedisCodec<String, ItemDto> {

    private final StringCodec stringCodec = new StringCodec();


    @Override
    public String decodeKey(ByteBuffer bytes) {
        return stringCodec.decodeKey(bytes);
    }

    @Override
    public ItemDto decodeValue(ByteBuffer bytes) {
        String decodedString = StandardCharsets.UTF_8.decode(bytes).toString();
        String[] parts = decodedString.split(",");
        return new ItemDto(parts[0], Integer.parseInt(parts[1]), parts[2], Long.parseLong(parts[3]));
    }

    @Override
    public ByteBuffer encodeKey(String key) {
        return stringCodec.encodeKey(key);
    }

    @Override
    public ByteBuffer encodeValue(ItemDto value) {
        String encodedString = String.join(",",
                value.getItemName(),
                String.valueOf(value.getPrice()),
                value.getUrl(),
                String.valueOf(value.getItemId()));
        return StandardCharsets.UTF_8.encode(encodedString);
    }
}

 

이러면 작업 끝입니다. 

 

그리고 컨트롤러를 조금 손봐줄겁니다. 

 

@Controller
@RequiredArgsConstructor
@Slf4j
public class ItemController {

    private final CacheFrontend<String, ItemDto> frontend;
    private final RedisCommands<String, ItemDto> redisCommands;

    @GetMapping("/set/item")
    @ResponseBody
    public String setItem(@RequestParam("itemName") String itemName) {
        redisCommands.set("test", new ItemDto(itemName, 1000, "localhost:8080", 10L));
        return "ok";
    }

    @GetMapping("/get/local")
    @ResponseBody
    public ItemDto getMessage() {
        return frontend.get("test");
    }
}

 

redis command로 우리의 키값인 test의 값을 변경해줄겁니다. 우선 복잡도를 최대한 낮추기위해 itemName만 바꿔보겠습니다. 

 

 

 

이제 다시 조회해보면?

 

 

와우... JSON이 캐싱된다는건 엄청나게 이점이 될 것 같습니다. 

 

그럼... 다시 등장해주세요!

 

공식문서에서 발췌한 very small latency를 확인해보겠습니다. 

 

사실 우리의 목적은 Redis에서 요청한 데이터와 Client Side Caching 의 성능차이이지만 극단적인 상황을 위해 RDB 100만행을 준비했습니다. 

 

이게 테스트를 해보니까 맨처음 요청은 다음 요청에 비해 꽤 높게 응답속도가 나오더라구요. 두번 눌렀을 때 데이터를 내부적으로 가지고있나봅니다. 그래서 여러번 눌러서 가장 작게 나온 값으로 측정했습니다. 

 

1번타자 RDB 100만행!

 

2번타자 Redis에 직접 요청!

 

3번타자 Redis for Client Side Caching!

 

처음에 Redis에 직접요청을 테스트해봤는데 4ms가 찍히는걸 보고 Redis가 괜히 캐시로 쓰는게 아니구나... 싶었습니다. 그리고 Client Side Caching을 테스트하기 전에 과연 얼마나 차이가 날까... 두근두근 하고 눌러봤는데?

 

2ms!!!

 

어디서 들은건데 우리가 키보드를 눌렀을 때 입력된 키가 OS를 거쳐 CPU에서 작업을 하고 다시 우리 화면에 보여지기까지 걸리는 시간이 대충 2ms라고 합니다. 

 

진짜 미쳤다고밖에 할 말이 없네요. 하지만 그럴만합니다. 왜냐하면 Map에 있는 데이터를 가져오는건데 심지어 HashMap이라 Hashing 알고리즘으로 시간 복잡도가 O(1)이니까요. 

 

 

마치며

Client Caching이 저를 여기로 이끌었지만 정말 엄청난 기술이라고밖에 할 말이 없네요. 제가 개발공부를 해온게 만으로 3년이 다됐는데 그전엔 계속 이전에 있던 기술들을 공부하곤 했는데 오늘 처음으로 신기술을 적용해봤네요. 트랜드를 따라가는 느낌을 들었습니다. 

 

물론 4년도 그리 짧은 시간은 아니지만 구글링 래퍼런스로 미루어봤을 때 사람들이 많이 사용하진 않은 것 같습니다. 오늘도 정말 뿌듯하네요!

 

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