ReSeller 프로젝트
는 사용자의 로그인 정보를 담은 session
과 자주 조회되는 캐시 데이터
들을 하나의 Redis 서버로 관리하고 있다. 그러나 프로젝트 규모가 커지고 캐싱 되는 데이터가 많아질수록 응답 속도가 저하될 것으로 예상이 된다.
이번 포스팅에서는 아래와 같은 주제로 진행된다.
세션 저장소와 캐시 저장소를 분리하여 부하를 분산
시켰을 때 얻을 수 있는 이점을 살펴보자.
Docker
를 사용하여 세션 저장소와 캐시 저장소를 분리해 보자.
메모리 관리
는 Redis를 사용하는 데 있어서 가장 중요하다.
메모리 관리 여부
에 따라 Redis 사용이 오히려 마이너스 요소로 작용할 수 있다.
현재 진행하고 있는 ReSeller 프로젝트
는 저장소를 아래와 같은 목적으로 운영하고 있다.
그러나 프로젝트 규모가 커지고 사용자가 증가함에 따라 캐시 저장소에 등록되는 데이터
, 로그인 정보를 담은 세션
, 인증번호
또한 증가하게 될 것으로 예상한다.
레디스는 In-memory Data Stroe
이기 때문에 Physical Memory
이상을 사용하게 되면 swap
이 발생한다. swap이 발생하게 되면 메모리 page 접근 시마다 읽고 쓰기 때문에 성능이 크게 저하된다. 보통 레디스가 갑자기 느려졌다 하면 해당 이슈일 확률이 높다. 자세한 내용은 해당 [우아한테크세미나] 191121 우아한레디스 by 강대명님영상을 참고하길 바란다. 세션 저장소와 캐시 저장소를 분리하여 사용하면 효율적인 메모리 관리가 가능하다.
위 그림처럼 세션 저장소와 캐시 저장소를 분리하면 서버 한 대에 집중되어 있던 부하가 두 대의 서버로 나누어지기 때문에 성능이 좋아지게 된다.
Redis
는 싱글 스레드
로 동작하는 만큼 데이터의 atomic(원자성)
함을 보장해 주기 때문에 데이터의 일관성을 보장
하며 동시성 문제가 발생하지 않는다
는 장점이 있다.
그러나 CPU를 하나 밖에 쓰지 못한다는 단점이 있다. Redis가 메모리에서 운영되기 때문에 CPU에서 생기는 병목현상은 드문 편
이지만 어떤 명령어에 대한 작업이 끝나기 전까지 다른 명령어들은 대기
해야 한다.
또한 Redis는 싱글 스레드로 동작하기 때문에 o(n) 이상 명령어 사용 시 더 많은 대기시간이 발생하게 되는데 이러한 경우에도 서버를 나누어서 사용하고 있다면 대기시간을 줄이는 데 도움이 될 수 있다.
따라서 레디스 서버를 목적과 기능별로 분리해서 사용한다면 분리된 서버의 수만큼 CPU를 더 많이 사용할 수 있으므로 처리 속도를 향상시킬 수 있다.
현재 Redis 서버에 이미 6379 포트로 레디스 서버를 사용하고 있다. 따라서 추가적인 Redis 서버는 docker를 이용해 설치해 보자.
docker pull redis
위 명령어를 입력하여 redis로 등록된 image를 가져온다.
docker run --name 이름 -d -p 포트 번호:6379 redis
위 명령어를 입력하면 redis 컨테이너가 실행된다. 이름은 굳이 안 넣어줘도 된다.
이름 : ex) ho
포트 번호 : ex) 6388
docker ps
위 명령어를 입력해서 redis 컨테이너가 정상적으로 실행되었는지 확인할 수 있다.
먼저 위와 같이 application.yml
또는 application.properties
에서 사용할 port 번호와 host를 설정하자. 아직 local 개발 중이므로 cache - host를 도커 머신 IP로 지정해 줬다.
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 이름
을 명시적으로 지정해서 빈 주입 시 충돌이 일어나지 않도록 하자.
@Autowired
: 명시한 타입과 일치하는 빈을 먼저 검색, 동일한 타입의 빈이 여러 개가 존재하는 경우 문제가 발생
@Primary
: 동일한 타입의 빈이 여러 개가 존재하는 경우, 해당 어노테이션이 붙은 빈을 우선적으로 주입
@Qualifier
: 동일한 타입의 빈이 여러 개가 존재하는 경우, 지정된 조건과 일치하는 빈을 주입
@Resource
: @Autowired와 달리 명시한 빈의 아이디와 일치하는 빈을 먼저 검색하여 주입
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("redisCacheConnectionFactory")
이와 같이 @Qualifier
를 통해 주입할 빈을 명시적으로 처리해야 한다.
docker exec -i -t 이름 redis-cli
마지막으로 위 명령어를 입력해서 redis-cli를 실행해 캐시 데이터가 정상적으로 저장되었는지 확인해 보자.