Tech

[MSA, Spring] Multi-Redis?

소녀니 2024. 11. 9. 16:34

MSA에서 Multi-Redis?

MSA를 하다보면 다양한 아키텍쳐를 사용하여 데이터를 분리하게 된다.

그 중 Redis는 주로 세션, 캐시 관리 측면에서 사용하게 된다.

데이터 분리가 목적이기 때문에 1개의 Application에 Redis를 다중으로 연결하여 사용하게 되는 경우도 생길 수 있다.

Spring Application에서 여러 개의 Redis를 연결하는 방식에 대해 알아보자.

Redis 란?

NoSQL 데이터를 인메모리(In-memory) 형태로 저장

- NoSQL 데이터인 'Key-Value' 형태로 데이터를 저장함

- Value로는 문자열 뿐 아닌 List, Set, Hash, Object 등 다양한 형태를 지원

- 주로 캐시, 메시지 브로커 등의 용도로 사용

- 인메모리 형태이므로 데이터를 저장하므로 빠른 성능

- 인메모리 데이터 구조이지만 서버가 다운되도 영구 데이터 저장 기능 제공

 

* In-memory 형태란?

데이터를 디스크에 저장하는 것이 아닌 메모리에 저장함으로써 데이터를 보다 빨리 읽고/쓰기가 가능하지만,

서버가 다운되거나 동작 과정 중 데이터가 삭제되므로 유실의 위험이 있음.

In-memory DB와 Disk DB

 

Java - Redis

Java-Redis Client에는 두가지가 있다.

1. Jedis

Jedis는 Redis 내부의 클라이언트 라이브러리로, 사용 편의성이 좋음

다른 Redis Java Client에 비해 가벼움

기능이 적지만 많은 양의 메모리 처리 가능

동기식으로만 작동

 

2. Lettuce

Lettuce는 완전 비차단 Redis Java 클라이언트

동기 및 비동기 통신을 모두 지원

추상화로 확장 가능
클러스터, 센티넬, 파이프라이닝 및 코덱 지원 등 고급 기능을 제공

 

Jedis는 멀티스레드 애플리케이션을 잘 처리하지만, Jedis 연결은 thread-safe 하지 않고 Lettuce는 비동기 통신이 가능함으로써 대량의 요청과 응답 처리에 있어서 더욱 유리하여 대부분 Lettuce 사용하는 추세

Spring Data Redis

Spring Data Redis는 Spring 애플리케이션에서 Redis에 대한 쉬운 구성 및 액세스를 제공

https://spring.io/projects/spring-data-redis

 

Spring Data Redis

Spring Data Redis, part of the larger Spring Data portfolio, provides easy configuration and access to Redis from Spring applications. It offers both low-level and high-level abstractions for interacting with the store, freeing users from infrastructural c

spring.io

* gradle 사용법

dependencied에 해당 라이브러리 추가

dependencies {
...
	// Redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

 

1번 레디스 연결

1번 레디스를 연결할 때는 일반적으로 java spring과 redis의 연결 방식과 동일하다.

이 때, CacheManager를 Bean에 반드시 등록해줌으로써 기본 cacheManger라고 지정을 해준다.

 

* RedisConfig

@Configuration
@RequiredArgsConstructor
public class RedisConfig extends CachingConfigurerSupport {

    public static final String CACHE_TEST = "cacheTest";
    @Value("${spring.redis.ttl}")
    private int ttl;

    private final RedisProperties properties;
    
    // connection
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory() {
        RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(properties.getHost(), properties.getPort());
        standaloneConfiguration.setPassword(properties.getPassword().isEmpty() ? RedisPassword.none() : RedisPassword.of(properties.getPassword()));
        return new LettuceConnectionFactory(standaloneConfiguration);
    }

   // RedisCache configuration
    @Bean
    @Override
    public RedisCacheManager cacheManager() {
        Map<String, RedisCacheConfiguration> customCacheConfigurations = new HashMap<>();
        customCacheConfigurations.put(CACHE_TEST, RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(TOSS_TTL_58_MINUTES))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonSerializerWithCustomMapper()))
                .disableCachingNullValues());

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(lettuceConnectionFactory()).cacheDefaults(defaultCacheConfiguration())
                .withInitialCacheConfigurations(customCacheConfigurations).build();
    }

    @Bean
    public RedisCacheConfiguration defaultCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonSerializerWithCustomMapper()))
                .disableCachingNullValues()
                .entryTtl(Duration.ofSeconds(ttl));
    }

   // GenericJackson2JsonRedisSerializer custom
    private GenericJackson2JsonRedisSerializer jackson2JsonSerializerWithCustomMapper() {
        ObjectMapper objectMapper = new ObjectMapper()
            .registerModule(new JavaTimeModule()) // 날짜 데이터 캐시 가능
            .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
            .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
            .disable(
                DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) //캐시에 자바모델로 매핑 할 수 없는 속성이 있어도 오류가 발생하지 않도록 함.
            .activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                //String, Boolean, Integer, Double을 제외한 모든 유형의 모든 배열에 기본 타이핑이 사용된다.
                JsonTypeInfo.As.PROPERTY
            );
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }
}

 

* CachingConfigureSupport 를 extends 함으로써 spring cache 설정에 도움을 받는다.

(KeyGenerator(Key 자동 생성), CacheManager, CacheErrorHandler 등의 설정을 지원)

 

1. RedisConnectionFactory를 통해 connection 생성

2. CacheManger를 지정함으로써 애플리케이션 전반에 걸쳐 캐시를 구성하고 관리하는 역할을 담당

3. CacheManger를 지정할 때, connection과 configuration을 통해 어떤 캐시를 어떤 connection과 구성할지 설정

4. RedisProperties를 통해 application.yaml의 spring.redis 밑의 설정들을 불러옴 (없을 때는 default 값 제공)

5. Ttl 설정을 통해 데이터 저장 주기 관리 가능

2번 레디스 연결

1번 레디스에서는 RedisProperties를 통해 기본 설정을 불러오지만, 2번 레디스는 기본 설정이 없으므로 application.yaml에 spring.redis위치가 아닌 다른 이름을 지정하여 설정을 가져온다. (ex) spring.sub-redis) 
2번 레디스를 연결할 때는 1번 레디스 설정과 비슷하나 기본 CacheManager는 1번 레디스에 설정해줬으므로 2번은 다른 이름으로 CacheManager를 설정해준다.

@Configuration
@RequiredArgsConstructor
public class SubRedisConfig {
    public static final String CACHE_NAME_SUB = "sub";
    private final SubRedisProp properties;
    @Value("${spring.redis.ttl}")
    private int ttl;

    @Bean
    public RedisConnectionFactory subRedisConnectionFactory() {
        RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(properties.getHost(), properties.getPort());
        standaloneConfiguration.setPassword(properties.getPassword().isEmpty() ? RedisPassword.none() : RedisPassword.of(properties.getPassword()));
        standaloneConfiguration.setDatabase(properties.getDatabase());
        return new LettuceConnectionFactory(standaloneConfiguration);
    }

	@Bean(name = "subCacheManager")
    public RedisCacheManager subCacheManager() {
        Map<String, RedisCacheConfiguration> subCacheConfigurations = new HashMap<>();
        subCacheConfigurations.put(CACHE_NAME_SUB, RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(popupRedisProp.getTtlDuration())
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonSerializerWithCustomMapper()))
                .disableCachingNullValues());

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(subRedisConnectionFactory()).cacheDefaults(subCacheConfiguration())
                .withInitialCacheConfigurations(subCacheConfigurations).build();
    }

    public RedisCacheConfiguration subCacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues()
                .entryTtl(Duration.ofSeconds(ttl));
    }

    private GenericJackson2JsonRedisSerializer jackson2JsonSerializerWithCustomMapper() {
        ObjectMapper objectMapper = new ObjectMapper()
                .registerModule(new JavaTimeModule()) // 날짜 데이터 캐시 가능
                .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
                .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
                .disable(
                        DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) //캐시에 자바모델로 매핑 할 수 없는 속성이 있어도 오류가 발생하지 않도록 함.
                .activateDefaultTyping(
                        LaissezFaireSubTypeValidator.instance,
                        ObjectMapper.DefaultTyping.NON_FINAL,
                        //String, Boolean, Integer, Double을 제외한 모든 유형의 모든 배열에 기본 타이핑이 사용된다.
                        JsonTypeInfo.As.PROPERTY
                );
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }
}

 

레디스 Cache 사용

1번 레디스 사용

아래 코드를 통해 CacheService의 getEntity를 호출한다면 다음과 같은 로직이 일어난다.

  1. 1번 레디스의 CACHE_TEST::id 키의 Entity 데이터가 있는지 확인
    1. 있으면 해당 Entity 데이터를 return
    2. 없으면 해당 Entity 데이터를 repository에서 가져오기
      1. 1번 레디스의 CACHE_TEST::id 키에 repository에서 가져온 Entity 데이터 저장
@Service
@RequiredArgsConstructor
public class CacheService {
    private final Repository repository;
    
    @Cacheable(value = CACHE_TEST, key = "#id")
    public Entity getEntity(String id) {
        return repository.findById(id);
    }
}

2번 레디스 사용

2번 레디스를 사용할때는 기본 cacheManager가 1번 레디스로 지정되어있기 때문에 cacheManager를 어떤 것을 사용할지 지정해줘야 한다. 2번 레디스를 설정할 때 bean에 등록해준 cacheManager 이름을 지정해준다.

아래 코드를 통해 CacheService의 getEntity를 호출한다면 다음과 같은 로직이 일어난다.

  1. 2번 레디스의 CACHE_TEST::id 키의 Entity 데이터가 있는지 확인
    1. 있으면 해당 Entity 데이터를 return
    2. 없으면 해당 Entity 데이터를 repository에서 가져오기
      1. 2번 레디스의 CACHE_TEST::id 키에 repository에서 가져온 Entity 데이터 저장
@Service
@RequiredArgsConstructor
public class CacheService {
    private final Repository repository;
    
    @Cacheable(value = CACHE_NAME_SUB, key = "#id", cacheManager="subCacheManager")
    public Entity getEntity(String id) {
        return repository.findById(id);
    }
}

 

결론

MSA를 사용하다보면 다양한 아키텍쳐를 구성하게 되는데 muti-redis에 대한 아키텍쳐 구성을 하게 되면서 redis를 여러 개 연결하는 것에 대한 글이 많지 않아 작성하게 되었다. Redis는 기본적으로 데이터 베이스를 분리하거나 schema를 분리하는 방식으로도 충분히 데이터 분리가 가능하기 때문에 muti-redis를 구성하는 일은 많지 않은 것으로 예상된다. 그러나, 운영상의 문제나 데이터 구조의 문제로 어쩔 수 없이 여러 개를 사용하는 경우가 필요하기도 하다.