깃허브 맞팔 탐지기 [05] - Redis 글로벌 캐시 도입

2025. 8. 28. 17:44·토이 프로젝트/깃허브 맞팔 탐지기

 

<서론>

https://backend-newbie.tistory.com/47 , 이전 포스팅에서 Redis를 우리 서버의 글로벌 캐시로 선정하는 과정을 다루었다. 이번에는 이를 실제로 적용해 볼 생각이다.

 

 

 

<Redis 연결 설정>

 

<Spring Data Redis 공식문서>

 

 Lettuce, Jedis 커넥터를 사용하고 싶다면 Spring Data Redis에서 내장해서 지원하지만, Redisson의 경우 별도로 추가해줘야 했다.

 

 

 

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.redisson:redisson-spring-boot-starter:3.43.0'

 

 나는 redisson을 이용한 분산락을 사용하고 있기 때문에 위와 같이 두 가지 의존성을 추가해 줬다.

 

 

 

 

@Bean
public RedissonClient redissonClient() {
    Config config = new Config();
    config.useSingleServer()
       .setAddress("redis://" + redisProperties.host + ":" + redisProperties.port);
    return Redisson.create(config);
}

 

 

 그 후, 위와 같이 RedissonClient를 빈으로 등록했다. 이 때, 연결 방식은 redis://와 rediss://로 나뉘는데, redis://는 평문 연결(SSL 미적용)이고, rediss://는 TLS/SSL을 적용한 보안 연결이다.

 

 또한, 운영 환경에서는 커넥션 풀 사이즈, 최소 idle 커넥션 수, 타임아웃, 재시도 횟수 및 간격과 같은 옵션들을 조정해줘야 하는데 일단 이는 나중에 진행할 생각이다.

 

 

 

 

<Redis 자료구조 선택>

 

<실제 전달 받는 데이터>

 

 우리 API 서버에서 깃허브에 특정 유저의 팔로워, 팔로잉 리스트 조회 요청을 보내면 위와 같은 데이터가 팔로워 + 팔로잉 수 만큼 오게 된다.

 

 

 

 

<필요한 필드 추출>

 

 API 서버에서는 위와 같이 필요한 데이터만 추출한다.

 

 

 

 

 

 

<Set, SortedSet에 데이터 캐싱>

 

 이 때, Redis에는 Set, SortedSet 이라는 자료구조를 제공하는데 이를 이용해서 데이터를 저장하면 데이터 중복방지, 원하는 score 기준 정렬이 보장된다.

 

 

 

 

<Redis Set 연산을 활용>

 

 

 이 때, Redis의 Set이 제공하는 SINTER(교집합), SDIFF(차집합) 연산을 활용하면, 맞팔 탐지 기능을 WAS에서 별도의 로직을 구현하지 않고도 손쉽게 처리할 수 있다. 즉, 애플리케이션 코드 단에서 팔로워와 팔로잉 목록을 비교하는 연산을 수행하지 않아도 되고, Redis가 제공하는 집합 연산으로 결과를 바로 얻을 수 있다는 장점이 있다.

 

 다만 이런 방식은 로직의 일부가 Redis에 들어가게 되므로 관리 포인트가 하나 더 늘어난다는 단점이 존재한다. 예를 들어, 팔로워 집합은 갱신되었는데 팔로잉 집합은 아직 반영되지 않은 상태에서 교집합 연산을 수행하면 잘못된 결과가 반환될 수 있다. 따라서 맞팔 탐지기를 위한 캐시 갱신 시점에 follower와 following 집합의 생명주기를 함께 관리해야 한다.

 

 이 문제를 해결하기 위해 Redis에서 추후 지원할 예정인 키 어노테이션 기능을 활용하면, 연관된 키들을 묶어서 함께 관리할 수 있을 것으로 보인다. 하지만 이 기능은 아직 출시되지 않았기 때문에 현재로서는 직접 동기화를 보장하는 별도의 로직이 필요하다.

 

 또한 API 서버의 수가 Redis 서버에 비해 많다. API 서버에서 팔로잉, 팔로워를 가져와서 집합 연산을 수행하고 페이징 처리를 하는 선택지 대신, Redis에서 조회 외에 연산을 수행하게 되면 오히려 Redis 서버의 부담이 증가할 수 있다. 

 

 

 

 

 

 

<String, Hash 사용>

 

 Redis의 String이나 Hash를 사용하는 경우, 데이터를 JSON 형태로 직렬화하여 그대로 저장할 수 있다. String을 사용할 경우 구조가 단순해 개발이 매우 간단해진다는 장점이 있다. 반면, Hash를 사용할 경우 조금 더 복잡해지기는 하지만, 저장된 데이터 중에서 followers, followings 등을 개별적으로 분리해 다룰 수 있다는 이점이 있다.

 

 우리 서비스에서 이 데이터들을 따로 조회하는 일은 매우 드물다. 또한, 대부분 사용자의 데이터가 7KB를 넘지 않기 때문에 String을 선택하는게 더 좋을 것 같았다.

 

 

 

자료구조 String Hash Set, Sorted Set
복잡도  낮음 (Spring Cache) 중간 (Redis Repository) 높음 (Redis Template)
관리 난이도 낮음 중간 높음
네트워크 비용 높음 (항상 전체 단위 조회) 중간 (원하는 필드 단위 조회) 낮음 (연산 결과만 조회)
서버 메모리 부담 높음 (항상 전체 단위 조회) 중간 (원하는 필드 단위 조회) 낮음 (연산 결과만 조회)
Redis 연산 부담 없음 없음 높음
데이터 변경 부담 높음 (항상 전체 단위 수정) 중간 (원하는 필드 단위 수정) 낮음

 

 정리하자면 위의 표와 같은 상황이다. 하지만 유저들의 캐시 데이터가 매우 작아서 네트워크 비용이나 서버 메모리 부담 문제에서 비교적으로 자유롭고 대부분의 필드들이 따로 조회될 일이 없는 우리 서비스의 특징을 고려해서 String 형식의 Json 데이터로 저장해 두고 이를 서버로 가져와서 역직렬화 해서 사용하기로 결정했다.

 

 

 

<Spring Cache와 함께 사용>

구분 Redis Template Redis Repository  Redis Cache
사용 기준 Redis의 대부분의 자료형, 기능들을 Low Level 에서 직접 사용해야 할 때 Redis를 서브 DB로 사용하거나 저장하려는 데이터에 기본적인 CRUD 작업이 필요할 때  Redis를 단순 캐시 서버 목적으로 사용할 때
사용 방식 Redis Template 직접 이용 Redis의 해시 자료형과 CrudRepository와 함께 이용  Spring Cache 추상화를 통해서 이용 
주요 어노테이션  X @RedisHash,
@Id,
@Indexed 
@Cacheable,
@CacheEvict,
@CachePut

 

 RedisTemplate은 Spring Data Redis의 저수준 API로 모든 Redis 자료구조 연산을 직접 다룰 때 쓴다. 트랜잭션·파이프라이닝, 직렬화 전략 교체, 바운드 연산 등 세밀한 컨트롤이 가능하다. JdbcTemplate 포지션인 것 같다. 

 

 Redis Repository는 Redis를 도메인 저장소처럼 쓰고 싶을 때의 객체 매핑 계층이다. 보통 엔티티를 Redis Hash로 매핑하고, @RedisHash, @Id, @Indexed, @TimeToLive 같은 어노테이션으로 키스페이스/ID/인덱스/TTL을 지정한다. JPA 포지션인 것 같다.

 

 Redis Cache는 Spring의 캐시 추상화를 Redis로 구현한 것이다. 사용, 유지보수가 매우 간단하다. 이 중에서 나는 Redis Cache를 사용할 생각이다.

 

 

 

 

<Spring Data Redis 공식 문서>

 

 Spring Data Redis의 공식 문서에서는 Spring이 캐시 추상화를 제공하는데, 그 구현체로 Redis Cache를 사용할 수 있으니 이를 사용하고 싶으면  RedisCacheManager를 스프링 빈으로 등록하라고 말하고 있다.

 

 

 

 

<Spring 공식 문서>

 

 Spring 공식 문서에는 스프링으로 개발하다 보면 캐시를 사용할 일이 많을테니 우리가 AOP로 만들어 뒀다, @Transactional처럼 캐시 솔루션도 지원해서 코드에 미치는 영향을 최소화할 수 있게 해주겠다고 나와 있다.

 

 다만, 여기서 말하는 캐시는 Redis Cache만을 뜻하는 것은 아니다. Spring의 캐시 추상화는 Ehcache, Caffeine, Hazelcast 같은 다양한 캐시 솔루션도 모두 지원한다. 즉, 스프링이 캐시 추상화를 제공하니까, 개발자가 알아서 원하는 구현체를 골라 끼워 쓰면 된다. 나의 경우 스프링 캐시를 사용하기 위해 레디스 캐시를 등록해서 사용하면 된다.

 

 

 

 

 

<Spring 캐시 추상화 공식 문서>

 

 캐시 추상화의 핵심은 어떤 메서드를 실제로 실행할 때 반환값을 캐시에 저장해 두고, 같은 메서드가 여러 번 호출될 때는 캐시된 결과를 이용해서 실제 메서드 실행 횟수를 줄여준다는 점이다.

 

 

 

 

<Spring 캐시 추상화 공식 문서>

 

 

 스프링 캐시 추상화가 캐시 로직을 작성할 필요가 없게 해주지만 실제 캐시 저장소를 제공하지 않는다. 이 캐시 추상화는 스프링의 CacheManager라는 인터페이스를 통해서 구체화 된다.

 

 

 

 

 

<Spring 캐시 추상화 공식 문서>

 

 

 캐시 추상화에서 내가 어떤 캐시 구현체를 사용하든 위와 같은 어노테이션을 이용해서 캐싱을 이용할 수 있도록 제공해 준다고 한다. 필요한 상황에 맞는 캐싱 어노테이션을 사용하면 된다.

 

 

 

 

 

캐시의 밸류는 메서드의 반환값이고 캐시의 키는 메서드의 매개변수를 기반으로한 캐시 추상화의 KeyGenerator를 통해서 만들어지고 관리된다

 

 

 

 

 

 이 방식은 메서드 시그니처가 복잡해질수록 적합하지 않으므로, SpEL을 활용해 KeyGenerator 없이 키를 직접 생성하고 관리하는 것을 권장한다고 나와있다.

 

 

 

 

 

 

 쉽게 말하자면 이런식으로 사용하면 Redis에 follow::1 과 같은 형태의 Key와 해당 어노테이션이 붙은 메서드의 반환값이 Value로 저장된다.

 

 

 

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name)

 

 또한, condition이나 unless 속성으로 캐시에 저장할지 말지를 조절할 수도 있다. SpEL을 활용한 캐시의 세부적인 컨트롤은 공식문서에 더 잘 안내되어 있다. 캐싱 어노테이션을 이용해서 쉽게 캐시를 제어할 수 있게 되었다.

 

 

 

 

 

<Spring Data Redis 공식 문서>

 

 이제 다시 Spring Data Redis 공식 문서로 돌아오면 Spring 캐시 추상화를 이용하고 싶다면 CacheManager의 구현체를 위와 같이 등록하라고 안내하고 있다.

 

 

 

@EnableCaching
@Configuration
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        // LocalDateTime 직렬화 / 역직렬화를 위한 설정 추가
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        // 캐시 Value 직렬화기
        Jackson2JsonRedisSerializer<?> serializer = new Jackson2JsonRedisSerializer<>(objectMapper, GithubFollowRelation.class);

        // Redis 캐시 관련 설정들 추가
        RedisCacheConfiguration redisCacheConfig = RedisCacheConfiguration.defaultCacheConfig()

                // value 직렬화기 설정
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                // 메서드가 null값 반환시 캐싱 안함
                .disableCachingNullValues()
                // TTL
                .entryTtl(Duration.ofMinutes(15));

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(redisCacheConfig)
                .build();
    }
}

 

 

 공식문서를 보면서 내 상황에 맞게 위와 같이 캐시 설정을 해주었다. 스프링 캐시 추상화를 사용할 경우 .transactionAware()를 통해 트랜잭션 성공·실패와 캐시 로직을 연동할 수 있는데, 나는 RDB에서 데이터를 조회하지 않아 이 부분은 고려하지 않았다.

 

 또한 RedisCache를 단순히 팔로우 탐지기용 캐시 서버뿐만 아니라, 다른 도메인에서도 범용적으로 사용할 수 있게 확장하는 방법도 있지만, 이번 프로젝트에서는 대부분 팔로우 기능에만 캐시를 사용할 가능성이 높아 이 부분은 주석으로 남겨두고 적절히 트레이드오프 했다.

 

/**
 *   [추후 확장시 고려 사항]
 *
 *   팔로우 관련 뿐만 아니라 다른 객체도 범용적으로 저장 하는 캐시로 사용하고 싶다면 ObjectMapper의 activateDefaultTyping()을 통한 추가 설정이 필요함.
 *   이 경우, JSON 데이터에 클래스 메타 데이터가 추가로 저장됨.
 *
 *   또는, 각기 다른 직렬화기를 갖는 여러가지의 RedisCacheManager를 스프링 빈으로 등록하고 사용해도 됨.
 *   이 경우, 어떤 캐시 매니저를 사용할지 명시적으로 지정 해줘야 할 수 있음.
 */

 

 

 

 

<캐시의 용량이 가득 찬 경우 대비>

 

<AWS ElastiCache Scale-Up 최대 사양>

 

 

 대부분의 유저들의 팔로우 관련 캐시 데이터가 7KB를 넘지 않는다고 추정했고 100만명의 사용자가 우리 서비스에서 팔로우 관련된 기능을 사용한다고 해도 7GB 정도의 데이터를 캐싱한다. 아주 넉넉 잡아서 100GB 정도의 공간이 필요하다고 해도 AWS ElastiCache를 Scale-UP해도 문제 되지는 않을 것 같다.

 

 

 

 

 

 

<내 Redis 인스턴스 메모리 관련 정보>

 

 

 하지만, 비용상의 문제로 내 ElastiCache의 메모리 여유 공간은 400MB정도 라고 생각해야 할 것 같다. Redis 에서는 메모리 사용량이 maxmemory에 도달하면 설정해둔 maxmemory_policy에 맞게 데이터를 정리한다. 

 

 행복한 고민이지만 넉넉 잡아서 1만명 정도의 회원이 우리 서비스에서 팔로우 관련 기능을 캐시 TTL 시간인 15분 이내에 마구 사용할 경우 Redis 메모리 사용량 한계에 도달할 것으로 추정된다. 문제는 여기서 나는 Scale-UP으로 대응할 돈이 없다. 따라서 maxmemory_policy 설정으로 이를 대응해야 한다.

 

 

 

 

<Redis 공식 문서>

 

 

 나의 경우 maxmemory_policy가 기본으로 volatile-lru로 설정 되어 있었다. volatile은 Redis에서 메모리 용량이 부족할 경우 TTL이 설정된 키만 제거하겠다는 설정이다.

 

 TTL이 설정된 키는 캐시 데이터일 가능성이 높고 TTL이 설정되지 않은 키는 중요한 데이터일 가능성이 높기 때문에 volatile 계열 정책만 사용할 생각이다.

 

 

구분 기능 기대 효과
volatile-lru TTL이 있는 키 중, 가장 오랫동안 사용되지 않은 키 제거. 오랫동안 사용하지 않았으니 앞으로도 사용 안할 것으로 예상하고 삭제
volatile-lfu TTL이 있는 키 중, 사용 빈도가 가장 낮은 키 제거. 사용빈도가 적으니 앞으로도 사용 안할 것으로 예상하고 삭제
volatile-random TTL이 있는 키 중, 무작위로 키 제거. 그냥 공평하게 랜덤으로 삭제.
volatile-ttl TTL이 있는 키 중, 남은 시간이 가장 짧은 키부터 제거. TTL이 얼마 남지 않은 키는 어차피 리프레시를 위해서 곧 삭제되어야 하니 삭제

 

 

 사실 내가 원하는 것은 용량이 낮은 데이터부터 제거였는데 그런 정책은 존재하지 않았다. lru, lfu, ttl 중에서 한 개를 고르면 될 것 같다.

 

 일단 lfu는 선택지에서 제외했다. 어떤 사용자 A가 100번 캐시를 사용하더라도 다른 사용자 B가 방금 서비스를 사용하기 시작해서 캐시를 연속으로 3번 사용할 예정일 수 있는데 lfu가 적용되어 있으면 안될 것 같았다.

 

 lru의 경우에도 실제 사용자 시나리오와 다르게 흘러갈 경우 원하는 결과를 보장하지 못하는 문제가 있었다. 이후 사용될 가능성이 있음에도 불구하고 단순히 최근에 사용되지 않았다는 이유로 제거될 수 있었다.

 

 

<AWS ElastiCache에서 수정>

 

 가장 예측이 쉬워보이고 안정적이라고 생각하는 ttl기반 정책을 사용하였다. 물론 해당 정책이 쓰일 만큼 캐시에 데이터가 찰지는 미지수이다.

 

 

 

 

<Redis 캐시 에러 핸들링>

@Slf4j
public class CustomCacheErrorHandler implements CacheErrorHandler {

    @Override
    public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
       handleExceptionInternal(exception);
    }

    @Override
    public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
       handleExceptionInternal(exception);
    }

    @Override
    public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
       handleExceptionInternal(exception);
    }

    @Override
    public void handleCacheClearError(RuntimeException exception, Cache cache) {
       handleExceptionInternal(exception);
    }

    private void handleExceptionInternal(RuntimeException exception) {
       log.error("Redis 에러", exception);
    }
}

 

 캐시와 관련된 에러가 발생할 경우, 상황에 맞게 에러 핸들러를 구현해주면 된다. 구체적인 에러 핸들링은 추후 추가할 생각이다.

 

 

@EnableCaching
@Configuration
public class CacheConfig implements CachingConfigurer {

    @Override
    public CacheErrorHandler errorHandler() {
       return new CustomCacheErrorHandler();
    }
}

 

 위와 같이 적용시킬 수 있다. 

 

 

 

 

 

 

<참고 자료>

 

Cache Abstraction :: Spring Framework

Since version 3.1, the Spring Framework provides support for transparently adding caching to an existing Spring application. Similar to the transaction support, the caching abstraction allows consistent use of various caching solutions with minimal impact

docs.spring.io

 

 

Redis :: Spring Data Redis

The Redis support provides several components.For most tasks, the high-level abstractions and support services are the best choice.Note that, at any point, you can move between layers.For example, you can get a low-level connection (or even the native libr

docs.spring.io

 

 

Spring Boot에서 CacheManager를 에러 핸들링하는 방법

본 글은 아래 링크의 글의 내용과 이어집니다.

choieungi.github.io

 

 

Key eviction

Overview of Redis key eviction policies (LRU, LFU, etc.)

redis.io

 

 

Redis strings vs Redis hashes to represent JSON: efficiency?

I want to store a JSON payload into redis. There's really 2 ways I can do this: One using a simple string keys and values. key:user, value:payload (the entire JSON blob which can be 100-200 KB) SE...

stackoverflow.com

 

 

실전 레디스 - 예스24

최적의 운영을 위한 레디스 실전 노하우레디스는 인메모리에서 빠르게 동작하고, 자료형과 기능을 이용하여 데이터를 유연하게 표현할 수 있어 최근 웹 시스템 등에서 널리 사용되고 있다. 『

www.yes24.com

 

 

개발자가 알면 좋은 Redis 꿀팁 모음 | 올리브영 테크블로그

실무에서 바로 쓰는 Redis 핵심 팁 공유

oliveyoung.tech

 

 

 

 

 

 

 

 

 

 

 

 

'토이 프로젝트 > 깃허브 맞팔 탐지기' 카테고리의 다른 글

깃허브 맞팔 탐지기 [04] - 저장할 데이터 분석 후 캐시 저장소 선정  (0) 2025.08.27
깃허브 맞팔 탐지기 [03] - Redisson 분산락 도입  (0) 2025.08.24
깃허브 맞팔 탐지기 [02] - MySQL 네임드 락 도입 고민  (0) 2025.08.23
깃허브 맞팔 탐지기 [01] - 문제상황 분석 & API 설계  (0) 2025.08.22
'토이 프로젝트/깃허브 맞팔 탐지기' 카테고리의 다른 글
  • 깃허브 맞팔 탐지기 [04] - 저장할 데이터 분석 후 캐시 저장소 선정
  • 깃허브 맞팔 탐지기 [03] - Redisson 분산락 도입
  • 깃허브 맞팔 탐지기 [02] - MySQL 네임드 락 도입 고민
  • 깃허브 맞팔 탐지기 [01] - 문제상황 분석 & API 설계
moyoy
moyoy
  • moyoy
    백엔드 공부 내용 정리
    moyoy
  • 전체
    오늘
    어제
    • 분류 전체보기 (9)
      • 궁금한거 (0)
      • 회고 (0)
      • 토이 프로젝트 (9)
        • 깃허브 연동 로그인 (3)
        • 깃허브 맞팔 탐지기 (5)
        • 깃허브 기반 랭킹 시스템 (0)
        • 공통 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
moyoy
깃허브 맞팔 탐지기 [05] - Redis 글로벌 캐시 도입
상단으로

티스토리툴바