Redis를 캐시 저장소로 사용할 때 고려할 점
Programming

Redis를 캐시 저장소로 사용할 때 고려할 점

상황

팀 내에서 캐시 저장소로 레디스를 고려하고 있어, RedisCacheManager를 세팅하는 업무를 맡았습니다. 작년에 우테코 팀 프로젝트를 진행하면서 Refresh token을 레디스에 넣어 사용했던 적은 있었으나, 실무에서 레디스를 운영해 본 경험은 없었습니다. 따라서 토이 프로젝트 환경이 아닌 실무에서 레디스 환경을 구축하면서 고민했던 점을 이야기해 보고자 합니다.

 

로컬 캐시 vs 글로벌 캐시

글로벌 캐시를 도입하기 전에 로컬 캐시와 글로벌 캐시의 차이점에 대해 명확히 할 필요가 있습니다. 로컬 캐시는 서버의 인메모리에 저장하는 캐시로 외부 저장소를 이용하지 않기 때문에 빠르다는 장점이 있습니다. 그러나 서버마다 각기 다른 캐시를 저장하기 때문에 변경 사항이 생길 경우 동기화가 즉시 이루어지지 않는다는 단점이 있습니다.

 

이에 반해, 글로벌 캐시는 외부 저장소를 이용하기 때문에 즉각적으로 동기화가 반영된다는 이점이 있습니다. 그러나 네트워크 I/O와 디스크 I/O 비용으로 인해 로컬 캐시에 비해 속도가 느리다는 단점이 있습니다. 또한 외부 저장소를 이용하기 때문에 장애에 대비해야만 합니다.

 

글로벌 캐시는 성능 저하를 감수하더라도 동기화의 이점을 취하고 싶을 때 사용합니다. 대표적으로 환율 정보를 예로 들 수 있습니다. 실시간으로 변하는 환율이 즉각적으로 반영되어야 하기에 속도가 느리더라도 로컬 캐시 대신 글로벌 캐시를 사용하는 것이 좋습니다.

 

멤캐시드 vs 레디스

글로벌 캐시로 레디스를 많이 선택하는 이유는 손쉽게 구축이 가능하기 때문입니다. 보통 글로벌 캐시로 레디스와 멤캐시를 고려하는데, 레디스는 멤캐시에 비해 다양한 데이터 타입을 지원한다는 장점이 있습니다. 또한, AWS에서 제공하는 관리형 서비스인 Elasticache를 사용하면 멀티 AZ로 클러스터 모드를 손쉽게 구축할 수 있기 때문에, 고가용성과 확장 가능한 구조로 설계가 가능합니다. 이 글에서는 글로벌 캐시로 레디스를 사용한다는 것을 전제로 설명하겠습니다.

 

스프링에서 레디스를 사용하는 방법

스프링에서 레디스를 사용하는 방법으로는 RedisCacheManager와 RedisTemplate가 있습니다. RedisTemplate은 키와 값을 직접 set, get 할 수 있는 반면, RedisCacheManager는 스프링에서 제공하는 캐시추상화 기능을 제공하기 때문에 객체 그대로를 직렬화해 저장합니다.

 

이 글에서는 제가 사용한 RedisCacheManager에 대해서만 설명하겠습니다. 중요한 점은 RedisTemplate을 쓰는지, RedisCacheManager를 쓰는지가 아닌 '레디스라는 또 다른 외부 저장소를 두면서 그에 대한 대비를 어떻게 했는가'라고 생각합니다.

 

RedisCacheManager

스프링에서는 캐시 추상화 기능을 제공합니다. 캐시를 적용할 메서드 위에 @Cacheable 애노테이션을 붙이면 됩니다. 스프링의 캐시 추상화 기능은 AOP를 이용하기 때문에, 우리는 어떤 CacheManager를 사용할 지만 정하면 됩니다. RedisCacheManager를 빈으로 등록하는 방법에는 많은 자료가 있으니 여기에서는 생략하겠습니다.

 

사용 방법은 간단합니다. 캐싱하려는 메서드 위에 @Cacheable 애노테이션을 붙이고 cacheManager로 빈으로등록한 redisCacheManager를 선언하면 됩니다.

@Cacheable(cacheNames = "getBoards", key = "#root.methodName", sync = true, cacheManager = "redisCacheManager")
public List<Board> getBoards() {
    return boardService.getTree().stream()
            .filter(Objects::nonNull)
            .toList();
}

 

고민했던점

레디스를 설치하고 스프링과 연동하는 과정은 어려운 일이 아닙니다. 그러나 제가 팀 내 환경에 맞춰 RedisCacheManger를 세팅하면서 고민했던 점들이 몇 가지 있었습니다.

 

1. Serializer에 대한 고민

레디스에 value를 저장할 때 GenericJackson2JsonRedisSerializer 를 계속해서 사용했습니다.

 

GenericJackson2JsonRedisSerializer 는 Jackson 라이브러리를 사용하여 Json으로 직렬화하는 방식을 사용합니다. GenericJackson2JsonRedisSerializer는 별도의 Class Type을 지정하지 않아도 자동으로 직렬화해주기 때문에 간편하다는 장점이 있습니다. 그러나 이것을 사용할 수 없는 경우도 존재합니다. 대표적인 예가 바로 양방향으로 순환 참조가 발생해 무한재귀가 일어나는 경우입니다.

Could not write JSON: Infinite recursion

 

따라서 이 경우엔 레디스의 디폴트 Serializer인 JdkSerializationRedisSerializer을 사용해야 합니다.

 

JdkSerializationRedisSerializer는 레디스에 저장하려는 객체에 implements serializable 만 덧붙이면 간단히 사용할 수 있습니다. 하지만 자바의 직렬화 방식을 사용하기에 갖는 단점이 있기에 신중히 사용해야 합니다.

 

JdkSerializationRedisSerializer의 단점

  1. 필드가 추가/제거되거나 필드의 타입이 변경되면 에러가 발생하게 됩니다.

    직렬화한 객체를 다시 역직렬화하려면 동일한 serialVersionUID을 갖고 있어야 합니다. 객체에 serialVersionUID을 명시하지 않으면 런타임에 해시함수를 이용해 자동으로 부여하게 됩니다.자동으로 부여되는 serialVersionUID는 클래스 정보에 기반하여 생성되기 때문에, 필드가 추가/제거되거나 필드의 타입이 변경되면 에러가 발생하게 됩니다.만약 serialVersionUID를 객체에 명시했다 하더라도, 필드 타입이 변경되서 생기는 에러는 피할 수 없습니다.

  2. 용량으로 인한 문제가 발생할 수 있습니다.

    직렬화 시 클래스의 메타정보까지 함께 저장하기 때문에 Json 포맷에 비해 상대적으로 용량이 큽니다.용량은 네트워크 I/O와 디스크 비용을 증가시키기 때문에 객체가 담고 있는 정보가 많다면 용량 문제를 고려해야 합니다.

 

Serializer마다 각각의 장단점이 있기에 사용 환경에 따라 GenericJackson2JsonRedisSerializer , JdkSerializationRedisSerializer, JacksonJsonRedisSerializer 중 어떤 Serializer를 사용할 지 선택할 수 있어야 합니다. 각각의 Serializer로 값을 직렬화하는 서로 다른 RedisCacheManger를 3개 만드는 것도 하나의 방법일 수 있습니다.

 

2. 로컬캐시와 글로벌캐시를 함께 사용

레디스를 글로벌캐시로 사용하더라도 로컬캐시를 함께 사용하는 것이 성능에 더욱 유리합니다. 왜냐하면 로컬캐시에 비해 글로벌 캐시는 네트워크 통신 비용이 발생하기에 레이턴시가 발생하기 때문입니다. 따라서 로컬캐시 → 글로벌캐시 → DB와 같은 순으로 조회하도록 만들 수 있습니다.

 

이 때 TTL은 글로벌 캐시는 보통 1시간 정도로, 로컬 캐시는 그보다 짧은 10초나 1분 정도로 두어 데이터가 변경되더라도 동기화 문제를 최소화할 수 있도록 설정합니다. 로컬 캐시의 시간이 짧으면 짧을 수록 글로벌 캐시를 빈번히 조회하기 때문에 성능은 안좋아지나 동기화 문제는 그만큼 개선됩니다.

 

3. Gzip 압축

레디스에 저장하려는 데이터가 크면 클 수록 네트워크 레이턴시가 비례해 증가하여 성능이 저하될 수 있습니다. 따라서 수 메가바이트 이상의 데이터를 저장하는 경우 gzip으로 압축하여 보내면 성능 증가를 꾀할 수 있습니다.

 

4. 장애 대응

레디스를 사용한다는 의미는 의존하는 외부 서비스가 새로 생긴다는 의미입니다. 따라서 레디스 동작하지 않는 동안에는 원래대로 DB를 조회하도록 fallback 기능을 구현해야 합니다.

 

단순히 fallback 기능만을 구현한다고 해결되는 것은 아닙니다. 실제로 레디스가 동작하지 않아 원 DB로 트래픽이 한꺼번에 몰릴 경우 더욱 심각한 장애로 이어질 수 있기 때문이다. 따라서 충분한 성능 테스트가 함께 진행되어야만 합니다.

 

5. 모니터링 환경 구축

레디스에 대한 의존도가 커질수록 레디스에 장애가 발생하면 서비스의 대규모 장애로 이어질 수 있기 때문에 레디스 메트릭을 모니터링할 수 있는 환경을 구축해야 합니다. 모니터링 해야 하는 레디스의 중요 지표에 대해서는 다음 아티클을 참고하면 좋을 것 같습니다.

 

How to monitor Redis performance metrics

Learn how to monitor Redis performance metrics.

www.datadoghq.com

 

결론

토이 프로젝틀 진행할 때 분산락이나 글로벌 캐시 목적으로 레디스를 많이 사용합니다. 저 역시도 작년 팀 프로젝트를 진행할 때 단일 EC2 위에 레디스를 설치해 사용했던 경험이 있습니다.

 

그러나 이번에 레디스 환경을 세팅하는 업무를 진행하면서 레디스를 단순히 설치하고 사용하는 것만으로 끝나는 것이 아니란 사실을 알게 되었습니다. 서비스를 안정적으로 운영하기 위해서는 레디스의 성능 개선과 장애 대응, 모니터링 환경 구축도 중요합니다. 만약 이 글을 읽는 분들도 레디스 도입을 검토하고 있다면 제가 고민했던 부분들을 한번 생각해 보면 좋을 것 같습니다.