코드 저장소.

[CoffiesVol.02] Redis의 세션과 캐시 서버를 분리하기. 본문

포폴/Coffies Vol.02

[CoffiesVol.02] Redis의 세션과 캐시 서버를 분리하기.

slown 2024. 6. 12. 00:18

목차

1.문제점

2.분리를 했을시의 이점

3.적용

 

1.문제점

현재 진행이 되고 있는 프로젝트(Coffies Vol.02)에서는 현재 하나의 Redis서버를 사용해서 Session과 Cache를 사용하고 있습니다. 하지만 댓글에 리뷰평점기능을 현재 redis를 사용해서 평점을 적고 있는데 평점이 많으면 많을수록 댓글 평점을 매번 SQL을 사용해서 적으면 성능에 저하가 일어날것이라고 예상이 되어서 이를 개선하고자 합니다.

2.분리를 했을시의 이점

우선은 Redis를 서버와 캐시 서버로 나누면 다음과 같은 이점이 있습니다.

  • 성능 최적화
    • 분리된 자원 관리: 인증 기능과 캐시 기능이 각각 독립된 환경(docker,localhost)에서 실행됨으로써, 각 기능이 필요로 하는 리소스를 개별적으로 최적화할 수 있습니다. 이는 전체 성능을 향상시킬 수 있습니다.
    • 부하 분산: 인증과 캐시를 분리함으로써, 한 쪽 기능에 부하가 집중될 때 다른 기능에 미치는 영향을 최소화할 수 있습니다. 예를 들어, 인증 요청이 급증하더라도 캐시 성능에 영향을 주지 않게 됩니다.

3.적용

현재 진행되고 있는 프로젝트에 적용하기 위해서는 Redis설정 클래스과 설정파일을 수정을 해야 한다.

 

@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {

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

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

    //redis 설정
    @Bean
    public RedisConnectionFactory redisConnectionFactory(){
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }
}

 

위의 설정 코드는 Redis-Session을 사용하기 위한 설정 클래스입니다.

 

@EnableRedisHttpSession 은 Redis-Session을 사용하기 위해서 설정을 한 어노테이션입니다.

 

Redis와 연결을 하기 위해서는 RedisConnectionFacory 빈이 필요한데 현재는 기존 Redis 서버와 연결하는 RedisConnectionFactory 빈 밖에 없습니다. 따라서, 분리한 캐시 서버로 연결하는 RedisConnectionFactory빈을 별도로 새성을 해야 합니다.

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisHost);
        redisStandaloneConfiguration.setPort(redisPort);
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public RedisTemplate<?,?> redisTemplate() {
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        RedisTemplate<byte[],byte[]> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        redisTemplate.setStringSerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory){

        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .disableCachingNullValues()
                .entryTtl(Duration.ofSeconds(CacheKey.DEFAULT_EXPIRE_SEC))
                .computePrefixWith(CacheKeyPrefix.simple())
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map<String,RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();

        redisCacheConfigurationMap
                .put(CacheKey.USER,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.USER_EXPIRE_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.BOARD,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.BOARD_EXPIRE_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.NOTICE_BOARD,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.NOTICE_BOARD_EXPIRE_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.LIKES,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.LIKES_EXPIRED_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.PLACE,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.LIKES_EXPIRED_SEC)));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory)
                .cacheDefaults(cacheConfiguration)
                .withInitialCacheConfigurations(redisCacheConfigurationMap)
                .build();
    }

}

 

위에 코드는 Redis Cache에 사용된 설정 클래스입니다. 

 

하지만 위와 같은 코드로 작성을 하고 서버를 작동하면 NoUniqueBeanDefinitionException이라는 에러가 발생을 했는데 이 에러가 발생을 한 원인은 RedisCacheManager 빈을 생성할 때 필요한 RedisConnectionFactory 빈을 주입하려고 할 때 동일한 타입의 빈이 두 개이기 때문에 위와 같은 에러가 발생을 하는 것입니다. 

 

이러한 에러를 해결하기 위해서는 스프링에서 제공을 하는 빈을 주입을 하는 어노테이션 중에 @Qualifier를 사용해서 지정된 조건과 일치하는 빈을 주입을 하는 방식으로 하기로 했다. @Qualifier를 넣은 수정된 코드는 다음과 같다.

 

@Configuration
@EnableCaching
@RequiredArgsConstructor
public class RedisConfig {

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

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

    @Bean(name="redisCacheManager")
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisHost);
        redisStandaloneConfiguration.setPort(redisPort);
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public RedisTemplate<?,?> redisTemplate() {
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        RedisTemplate<byte[],byte[]> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());
        redisTemplate.setStringSerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.setEnableTransactionSupport(true);

        return redisTemplate;
    }

    @Bean
    public RedisCacheManager redisCacheManager(@Qualifier("redisCacheManager") RedisConnectionFactory connectionFactory){

        RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration
                .defaultCacheConfig()
                .disableCachingNullValues()
                .entryTtl(Duration.ofSeconds(CacheKey.DEFAULT_EXPIRE_SEC))
                .computePrefixWith(CacheKeyPrefix.simple())
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map<String,RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();

        redisCacheConfigurationMap
                .put(CacheKey.USER,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.USER_EXPIRE_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.BOARD,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.BOARD_EXPIRE_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.NOTICE_BOARD,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.NOTICE_BOARD_EXPIRE_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.LIKES,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.LIKES_EXPIRED_SEC)));
        redisCacheConfigurationMap
                .put(CacheKey.PLACE,
                        RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(CacheKey.LIKES_EXPIRED_SEC)));

        return RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(connectionFactory)
                .cacheDefaults(cacheConfiguration)
                .withInitialCacheConfigurations(redisCacheConfigurationMap)
                .build();
    }

}

 

이렇게 작성을 하면 캐시와 세션저장소를 나눠서 부하를 분산하고 성능을 향상한 어플리케이션 운영환경을 구축할 수 있게 되었습니다.