redis with spring boot application

2024, Apr 08    

요구 사항

  • github commit이력을 3분마다 조회해서, redis에 저장한다.
  • redis에 저장된 정보를 기준으로 co-component-change, co-change 위반건을 검출

왜 Redis인가?

  • 제품 기술 스택에 포함되어 있지 않아서 한번 사용해보고 싶었다. 🤭
  • 생각보다 설치도 간단했고, spring 통합도 복잡하지 않았다!!
  • 설치할 서버에 이미 hazelcast가 사용중이여서 다른 캐시 구현체로 해보는 것도 운영할 때 장애전파가 적을 것이라고 판단했다.

준비

1. Redis 설치

$ docker version
$ docker pull redis
  • 실행
$ docker run --name redis-latest -d -p 6379:6379 redis

2. redis-cli 접속 방법

$ docker exec -it 0eb966e6477f redis-cli
명령어
keys *
flushall

구현

1. 프로젝트 생성 및 redis정보 설정

  • spring boot application을 생성하고 application.java에 @EnableCaching을 추가한다.
  • application.yml에 redis정보 설정

      spring:
        data:
          redis:
            host: localhost
            port: 6379
    

2. 샘플 pojo 클래스 생성

  • redis에 json형태로 저장하고 파싱하기 위한 모델 클래스를 생성한다.
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CoComponentChangeTargetSdo implements JsonSerializable {
    private String taskId;

    private String commitId;

    private String project;

    public static CoComponentChangeTargetSdo fromJson(String json) {
        return JsonParser.fromJson(json, CoComponentChangeTargetSdo.class);
    }

    @Override
    public String toString() {
        return toJson();
    }
}

3. redis configuration 클래스 생성

  • key-value의 serializer를 설정한다.
    • StringRedisSerializer : 문자열 키로 생성
    • GenericJackson2JsonRedisSerializer : 위에서 생성한 데이터 모델 클래스 CoComponentChangeTargetSdo를 json형태로 해서 value를 직렬화
      @Configuration
      public class RedisConfiguration {
          @Value("${spring.data.redis.host:localhost}")
          private String redisHost;
        
          @Value("${spring.data.redis.port:6379}")
          private int redisPort;
        
          @Bean
          public RedisConnectionFactory redisConnectionFactory() {
              return new LettuceConnectionFactory(redisHost, redisPort);
          }
        
          @Bean
          public RedisTemplate<String, Object> redisTemplate() {
              RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
              redisTemplate.setConnectionFactory(redisConnectionFactory());
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        
              return redisTemplate;
          }
        
          @Bean
          public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
              RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                  .serializeKeysWith(RedisSerializationContext
                      .SerializationPair
                      .fromSerializer(new StringRedisSerializer()))
                  .serializeValuesWith(RedisSerializationContext
                      .SerializationPair
                      .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
              return RedisCacheManager
                      .RedisCacheManagerBuilder
                      .fromConnectionFactory(redisConnectionFactory)
                      .cacheDefaults(redisCacheConfiguration)
                      .build();
          }
      }
    

4. 데이터 저장

  • github의 커밋로그에 있는 jira Id를 조합해서 key로 한다.
@CachePut(value = "bteam-ccp-ccc-target-find", key = "#coComponentChangeTargetSdo.getTaskId().concat('_').concat(#coComponentChangeTargetSdo.getCommitId())")
public CoComponentChangeTargetSdo register(CoComponentChangeTargetSdo coComponentChangeTargetSdo) {
    return coComponentChangeTargetSdo;
}

5. redis 데이터 조회 서비스 작성

  • 서비스 단
@Cacheable(value = "bteam-ccp-ccc-target-find", key = "#coComponentChangeTargetSdo.getTaskId().concat('_').concat(#coComponentChangeTargetSdo.getCommitId())")
public CoComponentChangeTargetSdo find(CoComponentChangeTargetSdo coComponentChangeTargetSdo) {
    throw new NoSuchElementException("bteam-ccp-ccc-target-find not found. :" + coComponentChangeTargetSdo);
}
public List<CoComponentChangeTargetSdo> findAll() {
    try {
      return cacheService.getAllCacheKeys("bteam-ccp-ccc-target-find")
          .stream()
          .map(value -> coComponentChangMapService.find(CoComponentChangeTargetSdo.fromJson(value.toString())))
                .collect(Collectors.toList());
        } catch (NoSuchElementException e) {
            return Collections.EMPTY_LIST;
        }
}
  • 캐시 조회 서비스단
public List<Object> getAllCacheKeys(String cacheName) {
        List<Object> keys = new ArrayList<>();

        Cache cache = cacheManager.getCache(cacheName);
        if (cache != null) {
            if (cache.getNativeCache() instanceof ConcurrentHashMap) {
                for (Object key : ((ConcurrentHashMap<?, ?>) cache.getNativeCache()).keySet()) {
                    keys.add(key);
                }
            } else {
                Set<String> redisKeys = redisTemplate.keys("*");
                redisKeys
                    .stream()
                    .filter(k -> k.contains(cacheName))
                    .forEach(k -> {
                        keys.add(redisTemplate.opsForValue().get(k));
                        log.debug("forEach key: {}-{}-{}-{}", cacheName, k, k.contains(cacheName), k.startsWith(cacheName));
                    });
            }
        }
        return keys;
    }
  • key를 조회하는 이 부분에서 좀 많이 삽질했다..
  • 전체 keys를 조회해서 contains로 String값을 필터하는 것으로 되어있는데, 애초에 key생성할 때부터 setKeySerializer에 String으로 하면 안되었을 것 같다. ;ㅁ;
    • contains는 안쓰고 싶었다….😭

Redis 조회 snapshot

  • 차곡차곡 저장되어있다.

../img/redis_with_spring_boot_application.png

Reference