brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Feb 17. 2019

Cache-Aside Pattern in Redis

Redis Sentinel 인프라 구축 및 스프링 부트 환경에서 연동

이 글은 캐싱 전략 중 Cache-Aside Pattern, Redis Sentinel 인프라 구축, 스프링 부트 환경에서 Redis Cache 엱동하기 에 대해서 작성한 글입니다. 


참고로, Redis 기본 내용(설치 방법, NoSQL 개념 등)은 다루지 않습니다. Redis 를 한번이라도 설치,운영을 해 본 개발자에 한해서 이 글을 읽어주시길 바랍니다. 설명이 매우 허접하기 때문에 Redis 를 전혀 모르신다면 이 글을 읽기 어려울 수 있습니다. 또한, 글을 다 쓰고 다시 읽어보니... 내용이 산으로 가버렸네요. Cache-Aside Pattern 에 대해서 글을 쓸려고 시작했습니다만, 글을 쓰고보니 Redis Sentinel 과 Spring Boot 에 대한 내용으로 확장이 되었습니다. 혹시라도 이 글을 읽으시는 개발자분들께서는 부담 없이 편하게 읽으시길 바라며, 글 내용에 잘못된 점이 있다면 피드백을 해주시길 바랍니다. 



Overview


"캐싱 전략"은 최근 웹서비스 환경에서 시스템 성능 향상을 위해 가장 중요한 기술입니다. 캐시는 메모리를 사용함으로 디스크 기반 데이터베이스보다 훨씬 빠르게 데이터를 반환할 수 있고, 사용자에게 더 빠르게 서비스를 제공할 수 있습니다. 이번 글은 "캐싱 전략" 중에서 많이 사용하는 "Cache-Aside Pattern"에 대해서 정리해보겠습니다. 또한, 필자에게 익숙한 스프링 부트 환경으로 샘플 코드를 작성할 예정이며, 레디스 인프라는 Master Node 1대, Slave Node 2대 총 3대의 Redis 노드로 구축해 볼 예정입니다. Redis 인프라는 Setinel 인스턴스를 3대 실행하여 Fail-Over 를 구현할 예정입니다. 친절하지 못한 설명이 많이 포함되어있지만, 부디 조금이라도 도움이 되길 바라며 글을 작성해보겠습니다. 



Aside Cache vs Inline Cache


캐싱전략에 대해서는 딱히 정답이 없다고 생각한다. 시스템 상황, 개발자 역량에 따라서 각자 구성하는 캐싱 전략은 전부 다를 것이다. 필자가 일반적으로 많이 사용하는 캐싱 전략은, 데이터를 캐시 스토리지에 미리 저장하고 필요할때마다 조회해서 사용하는 방식이다. Inline Cache Pattern 이라고 표현하겠다.  

필자의 표현이 이상하다면 제보 부탁한다. 위 방법이 좋기는 하지만, 만약 모든 데이터를 캐시에 저장하는 것이 비효율적인 상황이라면, 대안으로 Cache Aside Pattern 을 고려해보는 것도 좋다. 아래 필자의 그림을 보자. 

처음 사용자가 요청했을 때는 캐시 스토리지에는 아무 데이터도 없는 상황이다. 

1 .애플리케이션은 먼저 캐시 저장소에 데이터가 있는지 조회한다. 하지만 데이터가 없다.

2. 애플리케이션은 Contents DB 에서 데이터를 조회하고 사용자에게 제공한다. 

3. 애플리케이션은 Contents DB 에서 가져왔던 데이터를 캐시 저장소에 저장한다. 


다음 사용자가 요청했을 때는 이미 캐시 저장소에 데이터가 있는 상황이다. 

1. 애플리케이션은 먼저 캐시 저장소에 데이터가 있는지 조회한다. 캐시 저장소에 저장되어있는 데이터를 제공한다. 


[필자의 경험]

Cache Aside Pattern 으로 구현하는 경우에도, 

애플리케이션이 처음 실행될 때 초기 데이터를 캐시 저장소에 미리 넣어 두는 것도 좋은 방법입니다. 사용자 요청에 의해 많이 조회되는 데이터는 미리 캐시 저장소에 저장합니다. 



기억이 가물가물한... 사례연구


필자는 전회사(SK컴즈)에서 (입사~퇴사 )5년 동안 같은 업무만 했었는데, 네이트 포털 메인(첫페이지) 개발/운영 업무였다. 네이트 메인 업무는 딸랑 하나의 웹페이지였지만 매출이 가장 많이 나오던 가장 중요한 서비스였다. 필자의 기억에 5년 동안 서비스가 장애가 발생해서, 빈화면으로 나왔던 기억은 딱 한번밖에 없다. (아니면...몇번 장애가 있었지만 필자의 기억에서 지웠을수도 있다...) 어쩃든, 필자의 기억에 남는 단 한번의 장애는 IE8에서 발생했던 스크립트 오류였는데, 당시 스크립트 오류로 IE8 이하 사용자에게 한시간동안 화면을 제공하지 못했던 뼈아픈 장애였다. QA에서 꼼꼼히 확인했다면 하는 아쉬운 마음이 있었지만... 아무튼 장애가 절대 발생하면 안되는 서비스였기 때문에 캐싱에 대한 전략은 매우 중요하였다. 


필자에게 웹서비스를 개발/운영할 때 가장 중요한 기술이 뭔지 물어본다면, 필자는


캐싱 이라고 대답할 것이다. 


참고로 당시 네이트 포털 메인은 거의 모든 데이터를 Static 파일로 저장하여 구현이 되어있었는데, 심각한 시스템 장애가 발생해도 Static 파일로 서비스를 하기 때문에 사용자에게는 장애로 느껴지지 않도록 방어로직이 매우 잘 되어 있었다. 정적 호스팅 패턴이라고 표현할 수 있는데, 나중에 시간이 되면 다시 자세히 다루도록 하겠다. 옛날 개발자의 서비스 개발/운영 방법이라서, 최근 클라우드 환경과는 맞지 않을 수도 있다. 아무튼, 당시 네이트 메인 하단에 이슈 컨텐츠를 제공하는 서비스를 개발했었는데, 무한 스크롤을 제공해서 컨텐츠를 무한해서 제공해주는 서비스였다. 기억이 가물가물한데, 생각보다 구현하기 너무 까다로운 기능이었다. 현재는 해당 기능은 사라진 상태이다. 

http://www.nate.com

당시 필자가 캡쳐해놨던 화면이다. 컨텐츠를 조합하는 것도 어려웠고, 전반적으로 구현하기 살짝 어려웠던 기능으로 기억한다. 물론 정상적으로 서비스 오픈을 했고, 장애가 발생한 적은 없다. 

2015년 네이트 메인 하단 화면

사용자의 무한 스크롤을 통해서 제공되는 컨텐츠 데이터는 지속적으로 Contents DB 에 무한하게 쌓이게 되었고 사용자에게 빠르게 제공하기 위해서는 반드시 애플리케이션과 Contents DB 사이에 캐시 저장소가 필요한 상황이었다. 당시, 필자는 캐시 저장소를 도입하는 것에 대해서 설계 초기에는 생각 못했었는데 같이 개발하던 차장님께서 캐싱에 대한 좋은 의견을 알려주셨고, 당시 캐시 시스템을 관리하던 인프라팀의 협업으로, 사내 캐시 시스템을 연동하게 되었다. 당시 고려해야 할 내용은 아래와 같았다. 


캐시 데이터의 수명 : 모든 데이터를 지워지지 않고 평생 캐시 저장소에 저장하는 것은 효율적이지 못하다고 판단했다. 그래서, 캐시 만료 정책을 적절하게 설정하고 오랜 시간이 지난 데이터는 캐시 저장소에서 제거될 수 있도록 운영하였다. 

캐시 초기화 : 사용자에게 높은 빈도로 제공하는 컨텐츠는 사용자가 요청하기 전에 미리 캐시 저장소에 저장한다. 물론, 이 경우에도 캐시 데이터 수명 정책이 똑같이 적용 된다.

고가용성 : 서비스는 절대 죽으며 안된다. 캐시 저장소가 장애가 발생했을 때 장애가 전파되면 절대 안된다. 캐시 저장소가 죽으면, Contents DB 를 통해서 서비스를 여전히 제공해준다. 단, 캐시 저장소를 사용할때보다는 반응 속도가 느릴 것이다.

너무 오래전 기억이라서 자세히 더이상 기억이 나지 않지만, 어쨋든 서비스 장애가 나지 않도록 설계하는 것이 가장 중요한 이슈였다. 당시 많은 고민 끝에 Cache-Aside Pattern 으로 시스템을 구축하게 되었다. 물론 당시 Cache-Aside Pattern 이 뭔지도 모르던 시절이었다. 시간을 되돌려서 생각해보니 필자가 생각했던 시스템이 바로 Cache-Aside Pattern 이었던 것이다. Cache-Aside Pattern 에 대해서는 MSDN 을 읽어보자. MSDN 에 중요한 내용이 꽤 많다. 

https://docs.microsoft.com/ko-kr/azure/architecture/patterns/cache-aside

 

Spring cache


필자는 이 글에서 스프링 캐시 를 사용할 것이다. 필자가 작성한 이전 글에서 스프링 캐시를 다루는 글은 Hazelcast 관련 글이다. 하지만, 내용이 부실하여 추천하고 싶지는 않다. 

https://brunch.co.kr/@springboot/56

조금이라도 자세한 내용을 확인하고 싶다면, 공식 레퍼런스를 확인하자. 하지만, 스프링 레퍼런스는 아주 친절하지는 않다.

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html

어쨋든, 스프링 캐시 관련해서 자세한 내용은 이글에서 다루지 않겠다. 




Redis Clusterning vs Redis Master-Slave Sentinel 


이 글은 Redis Clusterning(클러스터링)에 대한 내용은 전혀 다루지 않는다. Redis 클러스터링의 장단점이 있는데, 비즈니스에 맞게 잘 판단하길 바란다. 일단, 필자의 경험으로는 클러스터링이 필요한 서비스는 아직 경험해본적은 없다. Master-Slave 로 구성하는 방법보다, 클러스터링으로 구성하는 방법이 조금 더 까다로울 수 있다. 필자가 클러스터링을 제대로 해본적이 없어서 판단하기 어렵지만, 굳이 클러스터링이 필요없는 비즈니스라면, Master-Slave 센티널 환경으로도 충분하게 가용성 높게 시스템을 구축할 수 있다. 또한, Redis 에는 크리티컬하게 매우 중요한 데이터는 저장하지 않도록 하였다. Redis 에 캐시되는 데이터는 백업 DB 에 반드시 존재하는 데이터로서, 갑작스럽게 캐시 데이터가 없어지더라도 큰 문제가 발생하지 않도록 구성하는 것이 좋다. 



Redis Sentinel 인프라 구축


레디스 관련해서는 필자가 이전에 글을 작성한 적이 있다. 

https://brunch.co.kr/@springboot/73

필자의 지난 지난 글에서는 Sentinel 에 대해서 자세하게 설명하지 않았었다. 이번 글에서는 Sentinel 설정을 자세하게 작성할 예정이다. 



Master 1, Slave 1, Sentinel 2 (Don't do this)


아주 간단한 방법은, 마스터 노드 1대, 슬레이브 노드 1대 총 2대의 Redis 노드와 Sentinel 인스턴스 2개로 구성하는 방법이다. 하지만, 이 방법은 추천하지 않는다. 아니 이렇게 하면 절대 안된다. 기본적으로 Sentinel 인스턴스는 최소 3개로 구성해야 한다. 또한 Sentinel 인스턴스 개수는 홀수로 구성해야 한다.


Master 1, Slave 2, Sentinel 3 


기본적인 구성이다. 필자는 이 글에서 해당 방법으로 진행하겠다. 일단 3대의 물리서버에 각각 Redis 를 설치하자. 설치 방법은 어떤 OS 를 사용하는지에 따라서 다른데, 자세한 설치 방법은 생략하겠다. 이 글은, 기본적으로 Redis 를 한번이라도 설치 및 서비스로 사용해본 개발자를 위한 글이다. 필자는, Ubuntu 에서 설치를 진행하였다. 모든 서버에 설치하는 Redis 는 기본 포트(6379) 를 그대로 사용한다. Sentinel 인스턴스 역시 기본 포트(26379)를 사용하겠다. 



#### 기본적인 Master - Slave 설정


1. Master, Slave 서버 모두 bind 0.0.0.0 을 설정한다.

1,2,3 서버의 redis.conf 파일에서 아래 구문을 수정한다. 

bind 0.0.0.0 


2. Master Redis 서버의 기본 인증 비밀번호를 설정한다. 

1번서버의 redis.conf 파일에서 아래 구문을 추가한다. 

requirepass password(패스워드)


3. Slave Redis 서버를 Master Redis 서버의 슬레이브 로 등록한다. 

2,3번 서버의 redis.conf 파일에서 아래 구문을 추가한다. 


masterauth master_redis_인증_비밀번호

slaveof master_ip 6379


(참고로, Redis 5.0.0 버전부터는 slaveof 대신 replica 라는 새로운 명령어를 사용하면 된다. slaveof 를 여전히 지원하기 때문에 5.0.0 에서 slaveof 명령어를 사용해도 되기는 하지만...)


해당 과정을 설정하고 Redis 를 재시작하면 아래와 같이 Master - Slave 의 Redis 인프라가 구축된다. 


1번 마스터 서버에서 나머지 2,3번 슬레이브가 정상적으로 등록되었는지 확인해보자. 


1번서버에서 

redis-cli -a 비밀번호  -->  info 명령어 실행


1번 서버에서 데이터를 저장하고, 2번 또는 3번 서버에서 데이터를 조회해보자. 정상적으로 데이터가 조회가 될것이다. 즉, 1번 마스터 서버에서 Write 한 데이터가, 2,3번 서버로 정상적으로 복사가 된 것이다. 


하지만, 센티널 인프라를 구축할 것이다. 마스터 서버가 죽었을 때, 즉 현재 1번 서버가 죽었을 때,  2번 또는 3번 서버가 마스터 서버로 전환이 되어야 할 것이다. 


Fail Over 기능이 되도록 구성해보자. 


#### 센티널 설정 추가

센티널 기본 권장사항은 Sentinel 3, Quorum 2 이다. 필자는 3대의 센티널 인스턴스를 실행할 것이다.


1.Redis 모든 서버에 인증 설정을 추가한다. 

requirepass password(패스워드)

masterauth master_redis_인증_비밀번호


센티널에 의해서 마스터-슬레이브 서버는 서로 전환이 될 수 있다. 즉, 마스터 서버가 죽으면 슬레이브 서버가 마스터 서버로 전환되어야 하기 때문에, 인증 관련 설정은 모든 서버 동일하게 설정하도록 하자. 


2.Sentinel.conf 에서 마스터 서버의 정보 설정

sentinel monitor mymaster 레디스_서버_마스터_IP 6379 2

sentinel auth-pass mymaster 비밀번호


FailOver 가 진행될때, 다수결(과반수)에 의해서 Master 노드를 선출하기 위해서, 동의해야 하는 다수결 값을 2로 설정한다. (Sentinel 홀수로 셋팅해야 하는 이유가 바로, 새로운 마스터의 선출 과정 때문이다.)

참고로, 해당 설정은 FailOver 가 실행되면 자동으로 수정이 될 것이다.



3.Sentinel.conf 에서 외부접속을 위한 bind 설정을 한다. 

bind 0.0.0.0


4.Redis Master 서버가 정상인지 확인하는 시간 주기 설정

sentinel down-after-milliseconds mymaster 5000

기본 설정은 3분이다. 필자는 5초로 설정하였다. 


모든 설정 과정을 진행하면 아래와 같이 Sentinel 인스턴스를 활용하여, 고가용성의 레디스 인프라를 구축하게 된다.  

필자는 집에서 사용하는 컴퓨타에 Vmware 를 활용하여, 3대의 가상서버를 구성하였다. 



Redis Sentinel Fail Over


Redis 마스터 노드가 갑자기 죽게 되면 어떻게 될까? 1번 서버의 Redis 를 강제로 중지시켜보자. 그리고, 센티널 로그를 확인해보자. 


+sdown master mymaster 1번서버_IP 6379

+odown master mymaster 1번서버_IP 6379 #quorum 2/2

+new-epoch 1

+try-failover master mymaster 1번서버_IP 6379

+vote-for-leader f85efb4f04902fa273d4a2187525f36dda618588 1

+config-update-from sentinel 5d44ade575760583aa8e6f098412359dced14982 3번서버_IP 26379 @ mymaster 1번서버_IP 6379

+switch-master mymaster 1번서버_IP 6379 2번서버_IP 6379

+slave slave 192.168.140.131:6379 3번서버_IP 6379 @ mymaster 2번서버_IP 6379

+slave slave 192.168.140.128:6379 1번서버_IP 6379 @ mymaster 2번서버_IP 6379

+sdown slave 192.168.140.128:6379 1번서버_IP 6379 @ mymaster 2번서버_IP 6379


1번 서버가 죽어서 Fail Over 를 실행하였고, 2번 서버가 마스터로 승격이 되었다. 현재, 1번 서버가 죽어있는 상황이라서, 마스터 노드 1대, 슬레이브 노드 1대 총 2대로 운영중인 상황이다. 1번 서버는 아직 죽어있기 때문에 +sdown 으로 로그에 찍힌다. 


참고로, Fail Over 과정이 수행되면서 redis.conf , sentinel.conf 등 핵심 컨피그 파일은 자동으로 수정이 된다. 만약, Redis 와 Sentinel 의 실행 권한이 서로 다르다면, 파일을 수정하지 못해서 Fail Over 가 정상적으로 진행이 안될 수 있다. 


1번 서버를 복구하면 1번 서버는 자연스럽게 새로운 마스터인 2번 서버의 슬레이브로 실행된다. 


FailOver 가 실패할 수도 있나??

Redis 3대 중 이미 1대가 죽어있는 상황에서, 즉 2대로 운영하고 있는 상황에서... 슬레이브가 먼저 다운되고, 마스터가 다운된 다음에, 슬레이브가 시작되면 이 서버는 마스터로 전환이 되지 않는다. 해결책은, Slave 서버의 slaveof 를 삭제하고 시작하면 된다. 자세한 내용은 레디스 공식 레퍼런스에 나와있다. 

http://redisgate.jp/redis/sentinel/sentinel.php


Spring Boot 연동하기


스프링 부트 환경에서 Redis Sentinel 를 연동해보자. 심플하게 레디스 연동을 하기위해 Spring-Data 프로젝트를 사용할 것이다. spring-data 프로젝트가 아니어도 상관없다. 이 글에서는 spring-data 에 대해서 자세히 다루지는 않겠다. Spring Data Redis 관련해서 궁금하다면, 아래 공식 레퍼런스 링크를 확인하길 바란다.

https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#redis:sentinel

필자는 스프링부트 1.5.19.RELEASE 버전에서 아래와 같이 디펜던시를 추가하였다. 

Redis 연동 정보를 설정 한다. 참고로 아래 redis 프로퍼티는 필자가 임의로 설정한 값이다. 스프링 부트에서 공식적으로 지원하는 설정이 아니다. 오해가 없기를 바라며...

그리고, ConnectionFactory 를 생성해야 한다. 

Sentinel 노드 정보를 모두 등록해주는 로직을 추가한다. 필자는 여기서 JedisConnectionFactory 를 생성해준다. 

스프링 부트 애플리케이션을 실행하면 아래와 같이 연결이 잘 되었다는 메시지가 표시된다. 

필자는 프로퍼티 설정에서, Redis 마스터 노드의 정보를 설정할 필요가 없었다. 왜냐면, 클라이언트인 스프링 부트 애플리케이션에서는 Sentinel 에 먼저 연결하면 된다. Sentinel를 통해서 가져온 Master Node 정보를 통해서 Redis에 연동이  될 것이다. 만약, 이 상황에서 Redis 의 FailOver 가 발생하면 어떻게 될까? Redis 의 마스터 노드를 중지시켜보자. 정상적으로 FailOver 가 실행이되면, Redis Sentinel 에 의해서 새로운 마스터가 선출되고, 스프링 애플리케이션에서는 새로운 Redis 마스터 노드에 커넥션을 자동으로 맺는다. 


참고로 JedisConnectionPool 을 설정하고 싶다면 JedisPoolConfig 를 생성해서 jedisConnectionFactory.setPoolConfig(jedisConnectionPool) 이런식으로 넣어주면 된다. 따로 설정하지 않으면 기본 풀이 생성될 것이다. 기본 커넥션풀의 설정값은 아래 캡처화면을 참고하자. 




캐시 데이터 저장, 조회


간단하게 데이터를 Redis 에 저장하고 조회하는 로직으로 데이터가 정상적으로 Redis 에 저장이 되는지 확인해보자. 잘 된다. 코드에 대한 자세한 설명은 생략한다. Spring-Cache 관련 내용은 구글링으로 찾아보면 꽤 많이 나온다. 

필자가 Sleep 구문으로 3초의 지연시간을 강제로 넣어봤다. 즉, 데이터를 조회하는데 3초 이상이 걸리는 것이다. 하지만 ,두번째 조회할 때는 지연 없이 바로 조회가 가능하다. CoffeeDao 의 findName 내부 로직을 수행하지 않고 CoffeeCacheComponent 의 findByName 메서드에서 Redis 데이터를 바로 리턴해준다. 


하지만 Cache-Aside Pattern 을 적용하기 위해서는 캐시 데이터에 대한 만료 시간을 설정해야 한다. 다음 장에서 Cache Expire Time 설정하는 방법을 알아보자. 



Cache Expire Time


일단, 방법은 여러가지가 있다. 가장 먼저 떠오르는 방법은, @CacheEvit 를 사용하는 것이다. 스케쥴을 걸어놓고 시간이 되면 @CacheEvit 어노테이션으로 구현된 메서드를 호출하면 Redis 캐시가 삭제될것이다. 하지만, 이 글에서는 @CacheEvit 에 대해서는 다루지 않겠다. CacheManager 를 커스터마이징 할 수 있는 방법을 알아보자. 스프링 부트에서 CacheManagerCustomizer 를 활용하면 캐시 만료 시간을 설정할 수 있다. 모든 캐시의 만료 시간을 5초로 설정하고 싶다면 아래와 같이 코드를 구현하면 된다. 

위 방법은 전체 캐시에 대한 만료 시간을 설정한 것이다. 만약 개별 캐시를 설정하고 싶다면 cacheManager.setExpires() 메서드를 사용하면 된다. 자세한 내용은 생략하겠다. 



Write to Master, Read from Replica


필자의 스프링 부트 환경에서는 안되는 것 같다. (확실하지 않다.) Spring 부트 기반에서는 Sentinel 구성이 되어있지 않은 환경에서만 가능하다고 알고 있지만, 왠지 찾아보면 방법이 있을 것 같기는 하다.

https://docs.spring.io/spring-data-redis/docs/current/reference/html/#redis:write-to-master-read-from-replica

 스프링 레퍼런스를 참고하길 바라며, 이 내용에 대해서는 필자가 나중에 공부를 더 해보도록 하겠다. 어쩃든, 가능하면 마스터 노드에는 Write를 하고, 슬레이브 노드에서 Read 를 하고 싶은데... 다른 팀에서는 Redisson 라이브러리와 Proxy 모듈을 사용해서 구성했다고 전해 들은바가 있기는 하지만... 나중에 시간이 많을 때 고민을 좀 해봐야할 것 같다. 



마무리


이 글에서는 캐싱 전략 중, Cache-Aside Pattern 에 대해서 알아보았고, Redis Sentinel 인프라 구축에 대해서 정리하였다. 또한, 스프링 부트 환경에서 Redis 캐시 를 연동하는 샘플 코드를 작성하였다. 진행을 해보니 여러가지 의문점이 생겼다. 수박겉핥기식 글이 되었지만, 아쉽지만 이정도로 글을 마치겠다. 필자가 다음 글에서는 Reactive Redis 관련 내용을 작성할 예정이다. 


https://github.com/sieunkr/spring-cache/tree/master/spring-cache-redis



레퍼런스


https://www.javacodegeeks.com/2019/01/spring-data-redis-high-availability-sentinel.html

https://content.pivotal.io/blog/an-introduction-to-look-aside-vs-inline-caching-patterns

https://docs.microsoft.com/ko-kr/azure/architecture/patterns/cache-aside

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari