티스토리 뷰

Redis 성능 향상을 위한 Redis 세션 저장소와 캐시 저장소의 분리

Overview

현재까지 진행된 프로젝트 상황을 살펴보면 세션과 캐시를 Redis에서 관리하고 있다.

지금은 세션과 캐시 구분없이 하나의 Redis 서버에서 관리가 되고 있다. 그러나 앞으로 프로젝트 규모가 커지고 캐싱되는 데이터가 많아질수록 서버 한 대가 많은 요청을 처리해야 할 것이다. 이에 따라 응답 속도가 저하될 것으로 예상이 되기 때문에 부하 분산의 필요성을 느끼게 되어 용도에 따라 Redis 서버를 분리하기로 했다.

 

세션 저장소와 캐시 저장소를 분리하면 어떤 점이 좋을까?

위에서 언급한 것처럼 세션 저장소와 캐시 저장소를 분리하면 서버 한 대에 집중되어 있던 부하가 두 대의 서버로 나누어지기 때문에 성능이 좋아지게 된다. 이외에도 서버를 분리하면 좋은 점이 한 가지 더 있다. 바로 Redis의 처리 능력을 높일 수 있다는 점이다.

 

 

Redis는 싱글 스레드로 운영된다. 그래서 Redis 서버의 모든 자료구조는 atomic 하며 이로 인해 데이터의 정합성이 보장된다. 그러나 CPU를 하나 밖에 쓰지 못한다는 단점이 있다. Redis가 메모리에서 운영되기 때문에 CPU에서 생기는 병목현상은 드문 편이지만 어떤 명령어에 대한 작업이 끝나기 전까지 다른 명령어들은 대기해야 한다는 사실은 변함없다. 만약, keysflushall 등 수행 시간이 키의 개수에 비례하는 명령어를 입력하게 된다면 대기 시간은 더 길어질 것이다. (물론, 해당 명령어들은 가급적 사용하지 않는게 좋지만.) 따라서, 목적에 따라 Redis 서버를 분리한다면 그만큼 CPU를 더 많이 사용할 수 있으므로 처리 능력을 높일 수 있게 된다.

 

지금까지 잘 동작하던 서버를 분리한다면 문제가 생기지는 않을까? 만약 Redis에 저장된 데이터들이 RDBMS에 저장되는 데이터들처럼 서로 연관되어 있다면 문제가 생겼을 것이다. RDBMS에 생성된 테이블들은 보통 서로 다양한 관계로 이어져 있기 때문에 테이블 별로 서버를 분리했다가는 데이터를 가져올 때마다 과도한 네트워크 트래픽으로 인해 성능이 더 저하될 것이다. 하지만 Redis에 저장된 세션 데이터와 캐시 데이터 간에는 연관성이 없기 때문에 데이터를 분산시켜도 문제가 없을 것이다.

 

Refactoring: Redis 서버 분리

로컬 PC에 이미 Redis가 설치되어 있었기 때문에 새로운 Redis 서버는 Docker로 띄웠다. 이 글에서는 Docker를 사용하는 방법을 다루지 않기 때문에 이에 대한 자세한 과정은 생략한다. 다만 가장 중요한 것은 Docker에서 Redis에 실행할 때 로컬 PC에 설치된 Redis가 사용하는 포트와 다른 포트를 지정해야 한다는 것이다. (이 부분은 로컬에서 개발하는 사람들에게만 한정되는 부분이고 실제로 배포할 때는 따로 서버를 띄우기 때문에 해당 과정이 불필요하다.)

docker run -d -p <PORT_OF_YOUR_CHOICE>:6379 redis

 

Redis 서버와 연결을 하려면 RedisConnectionFacory 빈이 필요한데 현재는 기존 Redis 서버와 연결하는 RedisConnectionFacory 빈 밖에 없다. 따라서, 분리한 캐시 서버로 연결하는 RedisConnectionFactory 빈을 별도로 생성한다.

@Configuration
public class CacheConfig {

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

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

    @Bean
    RedisConnectionFactory redisCacheConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(hostName);
        redisStandaloneConfiguration.setPort(port);
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
        return lettuceConnectionFactory;
    }
}

 

그러나 위의 코드 그대로 빈을 생성한다면 아래의 RedisCacheManager 빈을 생성할 때 에러가 발생한다.

@Configuration
public class CacheConfig {

    ...

    @Bean
    # NoUniqueBeanDefinitionException 에러 발생
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map<String, RedisCacheConfiguration> redisCacheConfigMap = new HashMap<>();
        redisCacheConfigMap.put(CacheNames.POST, defaultConfig.entryTtl(Duration.ofHours(1)));
        redisCacheConfigMap.put(CacheNames.FEED, defaultConfig.entryTtl(Duration.ofSeconds(5L)));

        RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
                .withInitialCacheConfigurations(redisCacheConfigMap)
                .build();

        return redisCacheManager;
    }
}

왜냐하면 RedisCacheManager 빈을 생성할 때 필요한 RedisConnectionFactory 빈을 주입하려고 할 때 동일한 타입의 빈이 두 개이기 때문이다.

 

@Autowired 를 통해 빈을 주입할 때는 명시한 타입과 일치하는 빈을 먼저 검색한다. @Autowired 단독으로 빈을 주입하면 같은 타입의 빈이 여러 개가 존재하는 경우 위와 같은 문제가 발생한다. 그래서 이를 보완하기 위한 방법이 몇 가지 있다.

  • @Primary : 동일한 타입의 빈이 여러 개가 존재하는 경우, 해당 어노테이션이 붙은 빈을 우선적으로 주입
  • @Qualifier : 동일한 타입의 빈이 여러 개가 존재하는 경우, 지정된 조건과 일치하는 빈을 주입
  • @Resource : @Autowired 와 달리 명시한 빈의 아이디와 일치하는 빈을 먼저 검색하여 주입

우리는 새로 분리한 Redis 캐시 서버로 연결해주는 RedisConnectionFactory 빈이 필요하다. 그러나 @Primary 는 하나의 빈을 특정해주기 보다는 해당 빈의 우선순위만 높여주는 것이기 때문에 지금 상황에서는 적합하지 않다고 생각했다. 또한 @Resource 는 필드 주입과 파라미터가 하나인 수정자 주입만 가능했기 때문에 @Qualifier 를 선택했다.

 

@Qualifier 로 개선한 코드는 다음과 같다.

@Configuration
public class CacheConfig {

...

    @Bean
    public RedisCacheManager cacheManager(@Qualifier("redisCacheConnectionFactory") RedisConnectionFactory connectionFactory) {

        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        Map<String, RedisCacheConfiguration> redisCacheConfigMap = new HashMap<>();
        redisCacheConfigMap.put(CacheNames.POST, defaultConfig.entryTtl(Duration.ofHours(1)));
        redisCacheConfigMap.put(CacheNames.FEED, defaultConfig.entryTtl(Duration.ofSeconds(5L)));

        RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory)
                .withInitialCacheConfigurations(redisCacheConfigMap)
                .build();

        return redisCacheManager;
    }
}

 

위와 같은 과정을 거쳐 세션 저장소와 캐시 저장소를 분리하여 부하를 분산하고 성능을 향상한 어플리케이션 운영환경을 구축할 수 있었다.

 

 

 


진행 프로젝트

github.com/f-lab-edu/sns-itda

 

f-lab-edu/sns-itda

Contribute to f-lab-edu/sns-itda development by creating an account on GitHub.

github.com

 

댓글