Redis(Docker)를 통한 Cache전략 알아보기

엄태권·2023년 4월 9일
3

서론

Redis는 원격(Remote)에 위치하고 프로세스로 존재하는 In-Memory 기반의 Dictionary(Key-Value) 구조 데이터 관리 Server 시스템이다(Remote Dictionary Server의 약자)

사실 찾아보면 Redis에 대한 기본적인 설명은 너무 잘되어 있어, 이번글에서 기본적인 설명은 다루지 않으려 한다.(Redis관련된 기본적인 글은 아래 참고란에 링크를 걸어두었다) 그럼 Redis의 어떤부분을 정리할까 고민하다 보니, Redis Cache 전략이 눈에 들어왔다.

사실 Cache 전략이라는 용어 자체도 생소했고, 읽기와 쓰기 사이에서 많은 전략들이 존재했지만, 코드로 해당 부분을 찾아보기가 쉽지 않았다.(Git Hub에는 존재할지도 모르겠다..)

그래서 직접 Redis를 설치하고 사용해보면서 Redis의 Cache전략은 어떻게 쓸까를 정리하려 한다.

[Cache Strategy(캐시 전략)]
Redis의 캐시전략으로 서론에서 설명했지만, 사실 Cache Strategy는 Redis에 특화되어 있는 것만은 아니다.

Redis가 대표적이어서 많이들 Redis의 캐시전략이라고 설명할뿐, 사실상 Spring Cache, Local Cache등의 모든Cache Service에서 사용이 가능하다.

캐시를 활용하여 DB와 Application 간의 조회 및 쓰기 성능 등을 어떻게 향상 시킬지가 목표인 전략이라고 봐야 맞다고 생각한다.

보통 아래와 같이 읽기, 쓰기 캐시 전략이 나누어져 있으며, 몇가지 전략에 대한 정리를 하려한다.

[읽기전략]
Look(Read) Aside / Read Through(Inline Cache)

[쓰기 전략]
Write Back(Write Behind) / Write Through / Write Around

Start Redis

그럼 Redis를 먼저 시작해보자 각각의 개발 환경이 다르겠지만 해당 부분에선 윈도우 Docker-DeskTop 기준으로 Redis 설치부터 Spring Application연결까지를 해보려 한다.

앞으로의 모든 프로젝트들 중 별도의 Server를 띄우거나, 별도의 환경이 필요한 경우 Docker를 적극 활용 예정이며, 대부분이 Window 기반의 시스템으로 설명예정이다.(사실 MacBook살 돈이없다... 월급인상 요구)

Docker-DeskTop 설치방법
기본적으로 설치를 한번씩은 해봤거나, 해놓고 안쓰거나의 경우가 많겠지만, 처음부터 설치를 한다 생각하고 간단한 설치방법을 설명할 예정이다.(이미 설치가 되어 있으면 넘어가는게 좋다)

우선 Docker-DeskTop 설치 경로를 통해 설치를 진행한다. 진행하다 보면, 기본적으로 Docker-DeskTop을 Window 환경에서 설치를 할경우 'WSL 2'를 기본설정으로 설치가 진행 된다.
이 부분은 Window에서 Linux환경을 가상으로 제공하기 위한 것으로 Linux 환경이 더 익숙하다면, WSL 2를 이용하여 설치를 진행하고, 아니면, Hyper-V를 활용하여 설치하는 것이 좋다.

Tip!!
사실 Linux 환경이 익숙하여 WSL2 를 활용하여 설치를 해봤는데 기본 메모리를 4GB 가까이 먹기 때문에 게임과 개발환경을 동시에 즐기려는 나에게는 버거운 환경이라, 지우고 Hypter-V를 사용해 설치했다.(WSL 2를 사용하고 싶은경우 아래 '[WSL2 문서]'에 경로를 제공할테니 참고하면 된다.)

Redis 이미지를 Docker 컨테이너로..
그럼 Docker-DeskTop이 설치되었으니, Redis 이미지를 다운받아 설치해 보자 사실 너무 간단하기에 간단히 설명하도록 하겠다.
Docker에 대한 기본 적인 상식은 Docker Doc에서 확인 가능하다.

그냥 빨간색 네모 칸에 Redis를 검색하고 Images Tag를 누르면, 최상단에 Redis가 있으니 설치하면된다, 명령어로 별도 버전을 지정하는 것도 가능하지만, 난 가장 최신버전을 설치하였다.

Container 띄우기
그럼이제, 이미지도 설치되었으니, Container를 띄어보자. Docker의 경우 Container를 띄울때 Volumn을 통해 Local에 있는 파일과 Container에 있는 파일을 공유할 수 있는데, 아래 처럼 명령어를 주어 띄우면 가능하다.

docker run --name redis -p 6379:6379 -v [Local파일 경로] : [Container 파일경로]
docker run --name redis -p 6379:6379 -v C:\Users\###\!!!\@@@\redis\redis.conf:/usr/local/etc/redis/redis.conf redis redis-server /usr/local/etc/redis/redis.conf

해당 명령어는 우선 Docker Container를 띄울때 localhost 의 6379번 포트 해당 컨테이너 포트를 연결하겠다는 것이며, -v 이후는 Volumn에 대한 지정이다.
redis.config 파일은 바로 아래 Redis 관련 설명에서 나올 예정이니, 해당 부분을 보고 하면 좋다.

Tip!!
Docker Container의 경우 기본 default bridge 네트워크로 기동되며, 이러한 네트워크 설정이 이후에 Container간의 통신에 작용을 미친다. 네트워크 또한 custom 생성이 가능하며, 이러한내용은 한번씩 찾아보고 사용하는 것이 좋을거 같다 [Docker-NetWork Document]

Redis Start & Test
그럼 Container가 기동되었을 텐데 Docker-DeskTop의 Containers Tab 에서 아래와 같이 로그가 뜨는것을 확인할 수 있다.

이후 Terminal Tab을 보면 명령을 입력할수 있는데, 기본적인 Redis 실행 명령어는 아래와 같다.

 # redis-cli         // Redis Cli 실행
 # auth (password)  // auth 접근
 # set key value    // key 를 set한다 set [key값] [value값]
 # get key          // 해당하는 key를 조회한다. get [key값]

Tip!!
Redis에 대한 기본적인 명령어들은 설명을 보고 오는것이 이번 글을 이해하는데 좋을거 같다.
주로 Redis에 대한 기본적인 블로그들은 아래 [Redis 기본] 참고 링크를 확인 하면 된다.
기본적으로 Redis 도 Config 파일이 존재하나 Config 파일설정을 다 보고 할 수 없는 관계로 나는 password만 지정하는 설정을 진행했다. 설정파일에 대한 링크는 [Redis Config 파일] 을 참조 바란다.

[패스워드 설정]
requirepass redistest //주석 제거후 패스워드 지정하여 사용

Redis With SpringBoot Application

이제 Redis 설치까지 진행 및 테스트 했다면 Application과 연동 하는 작업을 진행해 보자.
프로젝트를 하나 만들어서 진행해볼 예정이며, 각자의 방법으로 프로젝트를 설정하면 된다.
나의 경우 Spring Initializr 를 통해 진행했다.

Spring Project Dependencies
나는 아래와 같이 dependencies를 등록했다. 우리는 redis를 사용할 예정이기 때문에 Srping에서 지원하는 data-redis를 등록하여 사용할 예정이다.

	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'

Tip!!
log4jdbc dependencies의 경우 이후에 있을 jdbc template의 batch update를 위한 로그 확인을 위해 추가했다.

Spring - Redis 연동
그럼 Spring에 Redis연동을 진행해 보자. 이전에 우리는 Docker를 활용하여 6379:6379로 포트 오픈을 했으며, 이는 localhost의 6379 Port를 통해 컨테이너로 접근이 가능하다는 뜻이다.
그럼 아래와 같이 Redis의 접속정보를 통해 접근해 보자.
우선 Yml파일의 설정과 Spring Configuration 설정을 보자.

test:
  data:
    redis:
      host: localhost
      port: 6379
      password: redistest

두 가지 방식(redisConnectionFactory / redisConnectionFactory2)으로 ConnectionFacotry생성이 가능하며, 1번은 우리가 Redis 설정시에 설정한 Password를 입력하여 Factory를 생성하고 2번의 경우 host와 port를 통해 접근하는 방식이다.
이후 RedisTemplate을 Bean등록하며 각 value의 기본 Serializer와 ConnectionFactory를 등록한다.
(여기서 알 수 있는 점은 RedisTempate의 Bean name을 여러개로 하여 각 상황에 맞는 RedisTemplate이 가능하다는 것이다.)

>RedisTemplate에 ConnectionFactory가 두번 쓰였네요 ㅠ 오타입니다!

Tip!!
보통 Redis는 대표적으로 2개의 Client가 있으며, 각각 Lettuce, Jedis가 있다. 둘 다 모두 유명한 오픈소스로, 각 Client의 ConnectionFactory를 통해 연결이 가능하다. 요즘은 Lettuce를 많이 쓰는 추세로 향로님의 글에서 그 이유를 확인해 볼 수 있다.

Redis Cache Strategy

이제 Spring Application과 Redis의 연결을 위한 기본 설정까지 진행했으니, 제일 중요한 Redis의 캐싱 전략에 대해 알아보도록 하자. 서론에서도 말했지만, Cache Strategy는 Redis를 위한 전략이 아니며, Cache를 활용하는 모든 서비스에 해당되는 전략이라는 점이 중요하다!

읽기전략

1. Look(Read)-Aside 패턴
일반적으로 '지연로딩'이라고 한다. 가장 일반적인 캐싱 패턴으로 해당 전략의 경우 Applcation이 먼저 캐시에 필요한 데이터를 요청하고, 만일 해당 데이터가 있다면, Cache에서 해당 값을 반환하지만, 없다면, DB에 해당 데이터를 요청하여, 해당 값을 다음 시도를 위해 Cache에 저장하고 호출자인(Client)에 반환하는 방식이다.

[With Code]

  • 1: PostConstruct를 통해 redisTemplate의 setValueSerializer를 통해 User.class를 등록했다.(해당 값을 이용하면 redisTemplate<String, User> 방식으로 사용이 가능)
    추가로 미리 임의의 User를 하나 등록했다.
  • 2: Service Method로 먼저 redisTemplate을 통해 userId로 저장되어 있는 Cache를 확인한다.
  • 3: 만일 캐시에 저장하고 있는 User가 없다면, 직접 DB에서 해당 UserId로 조회하며, 해당 값을 Redis Cache에 넣고있다.(이떄 duratio.ofSeconds(20)을 통해 20초의 TTL을 주었다.)

[결과]
우선 해당 서비스를 호출시 미리 등록한 User는 캐시에 없을 테니 먼저 DB조회를 하게되어있고, 이후에 해당 값을 캐시에넣어 20초동안 관리하기 때문에 20초안의 요청은 DB조회없이 동일한 캐시 결과 값을 받아 볼 수 있다.

-> 보면 우선 캐시에 저장되어 있는 값이 없어 DB 조회를 먼저 하는 것을 볼 수 있다.

-> 이후 20초내의 요청시 DB조회 없이 Cache에 있는 값을 가져오는 것을 확인할 수 있다.

-> 우리가 조회한 userId 1번을 키로하는 User정보를 Cache하고 있는 Redis를 확인할 수 있다.

Spring의 @Cacheable 어노테이션
Spring에서는 Cache를 위한 몇개의 어노테이션을 지원하는데 그중 Cacheable이라는 어노테이션을 활용하여 Look(Read)-Aside 패턴을 구성할 수 있다.
이 작업을 위해선 Configuration에 @EnableCaching 이 추가되어 있어야 한다.

-> 우선 방금전 Bean을 등록한 부분에 추가로 위와 같이 작성을 해준다.
-> Spring에서 제공하는 메소드로 RedisCacheManager를 체이닝 형식으로 등록할 수 있다.
-> 위의 경우라면 'userCahce'라는 CacheName을 통해 @Cacheable 사용이 가능하다.
-> 참고 : Spring Boot Cache with Redis

-> 매우 간단한 로직이다. 아까 정의한 userCahce를 통하며, Key 값엔 userId를 주도록 하고있다.
-> 로직은 단순 userRepository에서 find지만, RedisTemplate을 통해 확인하던 과정이 어노테이 션으로 옮겨갔다고 생각하면 좋을거 같다.
-> 특이점은 해당 값은 Redis에 Key 저장시 [CacheName]::[key] 형식으로 저장된다.

[결과]

-> 우선 이전과 동일하게 Cache에 없기때문에 먼저 DB조회를 진행한다.
-> 이후엔 Cache에 저장하기 때문에 따로 로그가 나오지않는다.
-> 여기서 Cached어노테이션은 해당값을 Cache하고 있을 경우 메소드로직 자체를 실행하지 않는다.

-> 위와 같이 정상적으로 저장된 것을 확인할 수 있으며, 위에 설명한 패턴으로 Key가 생성된다.

TIP!!
캐시가 되어 있는 상태에서 DB 데이터의 변경이 일어난다면, 캐시와 DB 사이의 데이터가 다른 문제가 생길수 있기때문에, TTL(Time To Live) 즉 만료 시간을 설정해 특정 시간 이후에는 캐시를 지우는 작업이 필요하다. 추가로, 조회시에 캐시에 없다면 DB를 조회하지만 만일 데이터가 큰 경우엔 성능저하로 이어질 수 있어, 미리 DB데이터를 Cache에 밀어넣어두는 작업인 Cache Warming을 필요로 하는 경우가 있다.
Cache Warming에 대한 코드 구현은 아래에서 진행 예정이다.

+) Redis CacheWarming
Redis CacheWarming이란 많은 CachedMiss로 인해 성능 저하가 일어날 수 있는 상황을 미연에 방지할 수있는 방식이다. 말 그대로 Cache를 미리 따뜻하게 해놓는 느낌으로 보통은 배치작업 등을 통해 DB Data를 Cache에 밀어 넣어 놓는다.
NHN의 경우 Redis 소개 영상에서 쿠폰등의 대량 데이터는 미리 Cache Warming을 통해 Cache 성능 관리를 한다고 한다.

[With Code]

-> 우선 스케줄러를 통해 User 정보를 미리 Cache에 넣어놓는 스케줄러 이다.

-> 해당부분에선 user의정보를 조회한 후 해당 데이터를 Redis에 Set하는 방식으로 미리 Cache Data를 생성하고 있다.(만료 시간 - 5초)

[결과]

-> 우선 스케줄러 동작으로 미리 DB에서 Select를 해와 Cache에 저장하는 로그를 확인할 수 있다.
-> 정해진 TTL(5초) 이후 다시 Redis에서 Key 조회를 해보면, 해당 Key가 사라진걸 확인 할 수있다.

2. Read Through 패턴

Look-Aside 패턴과 다르게 캐시에서만 데이터를 읽어오는 전략이다.
대부분은 Look-Aside 패턴과 비슷하지만, Cache에 저장하는 주체가 Application냐, DataBase이냐의 관점 차이가 있다. Look-Aside의 경우 Application에서 캐시 조회후 없으면 DB를 조회하고 해당 값을 Cache에 넣는 구조지만, Read-Through의 경우 없을경우 Cache에서 DataBase를 조회하고, 해당 값을 Cache에 넣는 구조이다.

Redis - RDB 조합에선 해당부분을 구현하기 조금 힘든 부분이있다. 대신 주로 데이터베이스 기반의 인라인 캐시인 DynamoDB와 DAX를 활용하여, 해당 전략을 구현이 가능하다. (AWS문서 참조)

[장점]

  • Cache Miss를 처리하기 위해 캐시 키를 관리하거나, 코드 구현이 필요없다.
  • Look-Aside 보다 DataBase와 높은 싱크의 Cache 적중률을 보여줄 수 있다.

[단점]

  • Cache(Redis)에 문제가 생겼을 경우 서비스 전체에 영향을 줄 수 있다.
  • 대응 방안으로 Replication 구성 또는 Cluster 구성으로 해결이 가능하다.

쓰기전략

  1. Write Back(Behind) 패턴
    Cache와 동시에 DataBase 동기화를 진행한다.
    Data insert시에, 바로 DataBase에 저장하는 것이아니라, Cache에 모아둔 상태에서 일정 시간 이후에 배치등의 스케줄링을 통해 DB에 일괄적으로 반영한다.
    보통은 쓰기 작업량이 많은 워크로드에 적합하다.

[장점]

  • DataBase 정합성 확보에 용이하다.
  • 쓰기 쿼리의 회수 비용과 부하를 줄일 수 있다.

[단점]

  • 자주 사용되지 않는 불필요한 리소스가 저장될 수 있다.(TTL을 사용하여 해결가능)
  • Redis에 문제가 생겼을 경우 데이터의 영구소실 위험이 있다.

[With Code]
아래의 방식은 Cache전략을 보여주기위한 코드입니다. 아래처럼 회원 가입시에 사용하는건 좋지 않습니다!

  • 저장로직 진행시, RedisTemplate에 해당 유저의 Name을 Key로 하여, User 객체를 저장한다.

  1. User의 Key Format을 지정하였으며, JDBC의 batchUpdat 활용을 위해 JdbcTemplate을 주입받는다.
  2. RedisTemplate의 Serializer를 User.class로 등록한다.
  3. SavleAll 메소드로 Redis에 저장되어 있든 UserKey 패턴을 통해 모든키를 조회후 저장한다.
  4. 실제 해당 키로 조회한 User를 List로 만들어 JdbcTemplate의 BatchQuery를 실행한다.

Tip!!!
JPA의경우 For문 Query시에 하나씩 여러번이 나가는 형식이기 때문에 사실 Write-Back 패턴의 장점인 DB Insert 회수 비용 및 부하를 줄이기에 적절하지 않아 JDBC의 batchUpdate를 사용하였다. (참조)

일정시간 이후 실제 로직을 수행하도록 Scheduler를 걸었으며, 10개의 배치 Query가 나가는 것을 확인할 수 있고, Redis에 해당 값들이 저장되어 있는걸 확인 할 수 있다.

2. Write-Through 패턴
Write-Back패턴과는 조금은 다른 개념으로 DB와 캐시에 동시적으로 데이터를 저장하는 전략이다.
데이터를 저장시에 캐시에 저장 후 바로 DB에 저장한다.

[장점]

  • DB와 캐시가 항상 동기화 되어있어 Cache의 데이터를 최신의 상태로 유지가 가능하다.
  • 데이터 유실이 발생하면 안되는 상황에 유리한 조건이다.

[단점]

  • Write-Back과 마찬가지로 불필요한 리소스를 가지고 있을 수 있다.(TTL 사용)
  • 매요청마다 2번의 쓰기(Cache & DB)가 발생되어 수정사항이 많이 발생하는 서비스에서는 성능 이슈가 발생할 수 있다.

[With Code]

우선 저장 로직(writeThroughSave)을 User정보가 들어오는 바로 RedisTemplate을 통해 유저 정보를 저장하고, DB에 저장하는 것을 볼 수 있다.
이후 저장시 insert쿼리가 항상나가고, DB에도 정상적으로 쌓이는것을 볼 수있으며, Cache에도 해당 데이터가 있음을 확인할 수 있다.

3. Write-Around
우리가 기존에 하던 방식과 동일하다.(DB 서버에 직접적으로 삽입)
Cache를 따로 갱신하진 않으며, Data를 조회시 캐시에 없을 경우 DataBase와 캐시에 데이터를 저장한다.

[단점]

  • DB와 캐시 내의 데이터가 다를 수 있음.
  • DB에 저장된 데이터에 변동이 있을때 마다 Cache 동기가 되지 않아 TTL시간을 짧게 조정하는 대처가 필요하다.

중요!!!
모든 쓰기전략의 경우 해당 글처럼 User가입조건에는 사용하기 맞지 않는 전략이다. 추가로, 캐시저장 방식또한 UserName등으로 Key를 저장하는 것은 좋지 못한 방법이며, 위 예시와 같이 저장시에 Write-Back의 경우 가입 순서가 지켜지지 않는 문제도 있다. 현재는 전략패턴이 어떻게 돌아가는지를 코드로 보여주기 위해 위와 같이 작성하였다는 것을 참조해야한다!

정리

Redis를 학습하면서, Cache전략에 대한 구현코드를 확인하기 어려워 직접해보는 시간을 가지게 되었다. 사실 Cache에 대한 전략이 여러가지가 있으며, 각 상황에 맞는 전략을 썼을 경우 성능에 대한 향상을 가져올 수 있다는 점에 알고 써야한다 라는 생각이 더 들었다. 사실 내가한 방식 말고도 다른 많은 Cache 전략에 대한 구현방법이 있을거 같아 이후에 더 찾아보려 한다. 다음엔 Redis를 활용한 Lock에 대해서도 학습을 해봐야 겠다...

  • 모든 코드는 제 GitHub에 있으며 생각이 날때마다 작업 및 수정 예정입니다! -> GitHub

참조

profile
https://github.com/Eom-Ti

0개의 댓글