본문으로 바로가기

프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다.


Shoe-auction 프로젝트에서는 사용자의 로그인 정보를 담은 session자주 조회되는 캐시 데이터들을 하나의 Redis 서버로 관리하고 있다. 이번 포스팅에서는 캐시 저장소세션 저장소를 분리해야 하는 이유와 적용 과정을 소개하려고 한다.


💡 이용자 수가 늘어남에 따라 발생할 수 있는 병목현상

1. 메모리 관리

Redis를 사용하는 데 있어서 가장 중요한 부분은 '메모리 관리' 이다. 메모리 관리 여부에 따라 Redis를 프로젝트에 사용하는 것이 득이 될 수도 있지만 오히려 마이너스 요소로 작용할 수도 있다.

 

현재 진행하고 있는 프로젝트에서는 로그인 세션 정보와 회원가입 시 필요한 인증번호를 레디스에 저장하고 , 상품 조회, 브랜드 조회에 캐싱을 적용하여 레디스를 사용하고 있다.

 

이는 사용자가 증가함에 따라 캐시 저장소에 등록되는 데이터도 함께 늘어나게 될 것이며, 마찬가지로 레디스에 서버에 저장되는 로그인 정보를 담은 세션과 인증번호의 건수 또한 늘어나게 될 것이다.

 

레디스는 In-memory Data Stroe이기 때문에 Physical Memory 이상을 사용하게 되면 swap이 발생하게 되는데, swap이 한번 발생하게 되면 Redis가 계속해서 디스크를 읽게 된다. 따라서 레디스의 성능이 크게 저하된다. 즉, 성능 향상을 위해 레디스를 사용하고 있는데 그 이점이 사라지게 되는 것이다.

 

이러한 이유로 세션을 저장하는 서버와 캐시 데이터를 저장하는 서버 분리하여 사용한다는 조금 더 효율적인 메모리 관리가 가능하다.

 

2. 처리 속도 향상

Redis는 Single Thread로 동작한다. 싱글 스레드로 동작하는 만큼 데이터의 atomic 함을 보장 해주기 때문에 데이터의 일관성을 보장하며 동시성 문제가 발생하지 않는다는 장점이 있지만, 하나의 CPU로 동작하기 때문에 한 번에 하나의 커맨드만 실행할 수 있다. 평균적으로 초당 10만 개의 get/set이 가능하지만, 하나의 명령이 실행되고 있는 동안에 다음 명령은 무조건 대기해야 한다.

 

또한 레디스는 싱글스레드로 동작하기 때문에 o(n) 이상의 명령어 사용 시 더 많은 대기시간이 발생하게 되는데 이러한 경우에도 서버를 나누어서 사용하고 있다면 대기시간을 줄이는 데 도움이 될 수 있다.

레디스에서 O(n) 이상의 명령어는 가급적 사용하면 안된다. 특히 KEYS, FLUSHALL, FLUSHDB, Delete collections, Get All Collections 등은 치명적이다.

따라서 레디스 서버를 목적과 기능별로 분리해서 사용한다면 분리된 서버의 수 만큼 CPU를 더 많이 사용할 수 있으므로 처리 속도를 향상시킬 수 있다.

 

3. 목적에 따른 분리

로그인 정보를 담은 세션을 레디스 서버에 저장해서 사용하는 이유는 Scale-out 방식으로 서버를 늘렸을 때 발생할 수 있는 세션의 정합성 문제따른 인증, 인가 문제를 해결하기 위함이다.

 

반면에 상품 조회와 브랜드 조회를 레디스 서버에 저장해서 사용하는 이유는 자주 조회되는 데이터들을 DB에서 조회하지 않고 캐시 저장소에서 조회함으로써 애플리케이션의 성능을 증진시키기 위함이다.

 

이렇게 사용 목적에 따라 분리함으로써 조금 더 쉽게 레디스를 관리할 수 있게 된다.

 


💡 Redis 설치하기

현재 Redis 서버에 이미 6379 포트로 레디스 서버를 사용하고 있다. 따라서 추가적인 Redis 서버는 docker를 이용해 설치해보자.

docker pull redis

위 명령어를 입력하여 redis로 등록된 image를 가져온다.

docker run --name [이름] -d -p [포트번호]:6379 redis

위 명령어를 입력하면 redis 컨테이너가 실행된다.

docker ps

위 명령어를 입력해서 레디스 컨테이너가 정상적으로 실행되었는지 확인할 수 있다.

 


💡 Redis 서버 나누기

먼저 위와 같이 application.yml 또는 application.properties에서 사용할 port번호를 설정하자.

public class CacheConfig {

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

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

    @Bean(name = "redisCacheConnectionFactory")
    public RedisConnectionFactory redisCacheConnectionFactory() {
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisHost,
            redisPort);
        return lettuceConnectionFactory;
    }

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisHost,
            redisPort);
        return lettuceConnectionFactory;
    }

세션 저장소로 사용할 RedisConnectionFactory와 캐시 데이터 저장소로 사용할 RedisConnectionFactory를 분리해준다.

여기서 주의할 점은 RedisConnectionFactory 타입의 Bean이 2개가 존재하기 때문에 반드시 @Qualifier@Bean의 이름을 명시적으로 지정해서 빈 주입시 충돌이 일어나지 않도록 해야한다.

public class CacheConfig {    

    //.. 생략

    @Bean
    public CacheManager redisCacheManager(
        @Qualifier("redisCacheConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
        RedisCacheManager redisCacheManager = RedisCacheManager.RedisCacheManagerBuilder
            .fromConnectionFactory(redisConnectionFactory)
            .cacheDefaults(redisCacheDefaultConfiguration())
            .withInitialCacheConfigurations(redisCacheConfigurationMap()).build();
        return redisCacheManager;
    }
}

위와 같이 @Qualifier를 통해 주입할 빈을 명시적으로 처리해야 한다.

docker exec -i -t loving_blackwell redis-cli

모든 과정을 완료하고 위 명령어를 입력해서 redis-cli를 실행해 캐시 데이터가 정상적으로 저장되었는지 확인해보자.

 


참고자료 : www.youtube.com/watch?v=mPB2CZiAkKM