Redis라는 이름은 많이들 들어봤을 것이다. 하지만, Redis의 특징은 아는데 이걸 어떻게 활용해야 할지 모르겠는 사람들이 꽤 많을 것이다. 그래서, 이번에는 내가 활용해보고 조사해본 실제 개발에서 Redis의 활용 방안에 대해서 다뤄보고자 한다.
일단, Redis 활용 방안을 알아보기전에, Redis의 특징에 대해 알아야한다.
- key -value 형식으로 저장되는 NoSQL Datebase이다
- 인 메모리(In-Memory) 방식으로 저장되기 때문에, 디스크에 접근해야되는 RDB에 비해 굉장히 빠르다
- 메모리에 저장되기 때문에 종료시 데이터가 사라진다. 그래서 이를 방지하기 위해 백업을 지원한다
- 싱글 스레드로 작동한다. (엄밀히 말하면 살짝 아니긴 하지만)
- TTL(Time to live)를 제공한다. 그래서 일정 시간이 지나면 데이터가 사라지게 할 수 있다
- value에 string, list, set 등 다양한 자료구조를 지원한다
제일 중요한 부분은 메모리에 저장된다와 싱글 스레드로 작동한다이다. 그러면 이 특징들을 가지고 활용할 수 있는 방안을 몇가지 소개해보고자 한다. 또한 아직 작성하진 못했지만, 추후 Spring에서 이를 구현하는 방법에 대해서도 다룰 예정이다. (일단 현재는 임시로 관련 글 링크를 첨부했다.)
1. Refresh Token 저장
Refresh Token을 활용하기 위해서는, DB에 어떤 유저가 어떤 Refresh Token을 부여받았는지 저장할 필요가 있다. JWT 토큰을 설계해봤다면 다들 알다싶이, 보통 Access Token의 기간을 15~30분처럼 굉장히 짧게 잡고, Refresh Token은 기간을 길게 잡는 편이다. Access Token 기간이 짧기 때문에, DB에 Refresh Token을 확인하기 위해서 굉장히 자주 접근할 것이다.
하지만, 만약 우리가 RDB에 저장해둔다면, 접근하는데 오래걸리기 때문에 병목현상이 발생할 것이다. 반면에, Redis에 저장한다면 아주 빠르게 접근할 수 있다.
또한, user_uuid - refeshtoken 이런 형식으로 DB에 저장될 것인데, Redis의 key -value 형식과도 아주 잘 맞는다. Redis의 TTL 기능을 이용하여 시간이 지난 refreshtoken은 자동으로 사라지게도 할 수 있어 관리가 용이해진다.
마지막으로, 만약 갑작스럽게 서버가 종료되면 Redis의 데이터가 사라지므로 굉장히 치명적일 수 있는데, refresh token은 사라지더라도 그다지 타격이 없는 데이터이다.
아래 글은 Spring에서 Redis를 이용하여 Refresh Token 저장을 구현한 글이다. 참고하면 좋을 것 같다.
2. 로그아웃 시, Access Token 블랙리스트
유저가 로그아웃을 했다. 그러면, 그 유저의 AccessToken은 만료시켜야 정상이다. 로그아웃을 했는데도 해당 유저의 JWT 토큰 가지고 접근할 수 있다면, 이는 잘못된 접근이다. 따라서, Redis에 AccessToken을 Key로, value는 logout string으로 이런식으로 저장해두고, accesstoken 블랙리스트를 구현할 수 있다. 추가적으로, 여기서 TTL을 먹여서 일정시간이 지나면 휘발되도록 설정까지 할 수 있다.
3. Race Condition 예방
우리가 조회수 증가하는 기능을 만들었고, 이를 RDB에 저장해야된다 가정해보자. 만약에 우리가 단순히 RDB로 부터 값을 읽고 +1하는 방식으로 구현했다한다면, 조회수같이 동시에 굉장히 많은 사람들이 접근했을 때 Race Condition이 발생할 것이다.
따라서, 이를 예방하기 위해서 몇가지 방법이 있는데 그 중 하나가 Redis를 활용하는 것이다. Redis는 싱글 스레드에서 작동하기 때문에 Race Condtion을 예방할 수 있다. 따라서, 조회수가 증가할 때 임시로 Redis에 저장해놨다가 Spring의 Scheduled 어노테이션 등을 이용하여 주기적으로 일정시간마다 RDB에 반영하는 방식으로 하면 된다.
Redis 이외에도 Lock을 건다던지, 트렌젝션 격리 수준을 엄격하게 한다던지 방법도 있긴하다.
4, 인기 검색어 및 랭킹 시스템 구현
Redis의 자료구조 중에서 Sorted-set이라고 존재한다. 일단, 다들 Hashset같은 개념이 무엇인지 알 것이다. 그래서 views라는 key 안에 value로 게시글-조회수 이런식으로 set들이 들어간다.
여기서, sorted set은 이름부터 알 수 있듯이, 이제 이걸 내부적으로 정렬해준다. 그러면 게임에서 유저 점수에 따른 랭킹이나, 인기 검색어 순위 등을 아주 쉽게 구현할 수 있을 것이다.
만약에 우리가 이걸 RDB같은 곳에 저장한다면? 일단 접근하는데도 오래걸리고, 계소갷서 oreder by를 치면서 실시간으로 계속 찾아봐야 할 것이다.
5. 캐시
유저들이 자주 접근하는 데이터는 캐싱해두는 것이 좋다. 예를 들어서, 공지사항 목록을 구현한다고 해보자. 공지사항은 잘 바뀌지도 않을텐데 유저들이 해당 게시판에 접속할 때마다 매번 RDB에서 목록을 읽어오는 짓은 매우 비효율 적이다.
따라서, Redis 같은 곳에 캐싱해두고, 똑같은 요청이 들어오면 빠르게 Redis에서 뽑아가면 된다.
Spring에서는 Spring Cache 라이브러리로 캐싱을 잘 제공해주는데, Default는 메모리에 저장이다. 이걸 Redis에 대신 저장하는 식으로 사용이 가능하다.
6. Api Rate Limiter 개발
Api Rate Limiter란, 말 그대로 Api 사용량을 제한한다는 의미이다. 만약 Rate Limiter가 없다면? A라는 사람이 악의적으로 우리 AI 모델에게 1분에 100만번의 요청을 보낼 수 있다. 그러면 우리 AI Token은 모두 사라진다. B라는 사람은 Spring 비즈니스 서버에 1초에 1000만번 요청을 날릴 수 있다. 그러면, 역시 서버가 터질 것이다.
따라서, 대부분 우리가 이름 들어본 서비스들은 악의적인 요청을 막기 위해 Rate Limiter가 구현이 되어있다. 이 기능을 구현할 수 있는 알고리즘이 여러개 있는데, 일단 대표적으로 가장 쉽고 널리 쓰이는 Token Bucket Algorithm을 다뤄보고자 한다.
Token Bucket Algorithm이란, 사용자마다 Token을 담을 수 있는 Bucket이 존재하고, 각각 Bucket에는 Token을 최대 가질 수 있는 Token Bucket Capacity가 정해져 있다. 그리고 Bucket 위에 수도꼭지를 틀어놓은 것 처럼, 일정시간마다 Token이 리필이 된다. 이 Token 리필은 Bucket Size가 꽉찼을때는 되지 않는다. 그리고, 사용자가 API 요청을 보낼 때 마다 Bucket에서 일정량의 Token을 꺼내간다. 만약, 꺼낼 Token이 없다면, Status 429를 보내준다. (Too Many Requests) 이외에도 출 버킷 알고리즘, 고정 윈도 카운터 알고리즘, 이동 윈도 로깅 알고리즘, 이동 윈도 카운터 알고리즘 등이 존재한다.
그럼 여기서 Reids가 어떤 여할을 하는가? Redis에 이 Token을 기록해두는 것이다. Spring Cloud Gateway에서도 RateLimiter가 Redis를 이용하여 구현이 되있다. Redis에 {사용자 UUID}.token = 20 이런식으로 기록해두고, 이 값을 조정하는 방식이다. Redis는 인메모리 DB이기 때문에 매우 빠르다. 또한, 싱글 스레드에서 실행되기 때문에, 다중 사용자의 요청에도 일관되게 처리할 수 있다. 즉, Race Condition을 예방할 수 있다.
하지만, Redis 내부에서는 싱글 스레드로 실행되지만, 이는 Redis의 Value 값을 증가시키거나 이런 일련의 하나의 행위에만 해당하는 일이다. 실제 코드를 작성할 때는, (Redis로 부터 값을 읽옴) → (읽어온 값을 보고 Redis에 값을 처리) 이런 로직으로 진행된다. 이 과정은 Spring 코드 내에서 실행될 것인데, 그럼 만약에 동시에 99명의 사용자가 Redis로 부터 값을 읽으면, 모두가 같은 값을 읽어간다. 즉, Redis로 부터 읽고, 읽어온 값을 보고 Reids에 다시 값을 반영하는 이 일련의 행위가 하나의 덩어리로 이루어져야 한다는 말이다. 아닐경우 결국 Race Condition 발생합니다. 그래서 Redis 2.6버전 부터는 내장된 Lua Script Engine으로 Lua Script를 실행할 수 있다. 이 일련의 행위를 Lua Script로 짜서 Redis 내부에서 실행하면 Atomic하게 실행될 것이고, Race Condition을 방지할 수 있다.
이와 관련해서는 추후 더 자세히 다뤄볼 것이다.
7. 유저의 활동 기록 저장
Redis에는 List 자료구조도 지원한다. List에 key를 유저 id, 그리고 value에는 유저의 활동을 담은 list를 만들고, 거기에 계속 유저의 활동 기록을 추가하는 식으로 구현하면 된다.
또한, Redis를 통해서 현재 접속 유저인지 파악하고 중복 로그인을 방지할 수 있도록 구현이 가능하다. 또한, 결국 유저가 접속중인지 판단할 수 있으므로 Session Storage로도 사용이 가능하다.
마지막으로, 유저의 최근 검색어를 보여주는 기능도 구현이 가능하다. 이 역시 Redis의 List 자료구조를 이용해서 쉽게 구현이 가능하다.
8. Message Queue로서의 역할
MSA 구조에서 고가용성을 보장하기 위해서 비동기적으로 처리하는 Kafka와 같은 Message Queue의 역할이 매우 중요하다.
Reids에도 Redis Stream을 이용하여 Kafka와 같은 Message Queue로서 사용할 수 있다.
다만, 내가 아직 사용해보진 않아서 이게 Kafka에 비해서 무슨 이점이 있는지 잘 모르겠다. 아무리봐도 그냥 Kafka 사용하는게 좋아보이는데... 이건 추후 공부를 더 해보고 사용해본 뒤, 글을 수정해보겠다.
9. 선착순 쿠폰 이벤트
이것도 앞서 이야기 한 것과 비슷한 결이다.
만약, 우리가 어떤 서비스에서 선착순 쿠폰 이벤트를 진행한다고 가정해보자. 그러면 한 순간에 엄청난 트래픽이 몰릴 것이다. 따라서, 매번 RDB에서 쿠폰 수량을 확인하기는 어려울 것이다. 또한, Race Condition문제는 당연히 따라올 것이다.
그래서 Redis의 Set 자료 구조를 이용하여 빠르게 현재 나간 쿠폰 수를 알 수 있고, 인당 1개의 쿠폰만 가져갈 수 있도록 조정도 가능하다. 게다가 Redis에서도 Transaction을 지원하는데, Redis는 싱글스레드에서 실행되기 때문에 이 Transaction 한 덩어리가 한 스레드에서만 실행된다. 따라서, Race Condition까지 예방이 가능하다. (Transaction말고도 Lock을 걸 수도 있고, Lua Script를 실행해서 Atomic을 보장할 수도 있을 것이다.)
실제로, 여기어때에서 선착순 쿠폰 이벤트를 Redis를 이용하여 구현하였다. 아래 여기어때 기술 블로그를 참조 바란다.
'BackEnd > DB' 카테고리의 다른 글
[DB] ElasticSearch의 Search API (1) | 2024.11.19 |
---|---|
[DB] 백엔드 개발자를 위한 효율적인 데이터베이스 선택 가이드 (1) | 2024.11.17 |
[DB] CSV 파일 데이터 DB에 업로드 (8) | 2024.11.13 |
[DB] ElasticSearch Index 설정과 텍스트 분석 (0) | 2024.11.08 |
[DB] Elastic Search의 기초 (9) | 2024.11.07 |