목차
이 글은 Redis7, SpringBoot2.7 기준으로 작성되었습니다.
Redis 란?
레디스는 손쉽게 사용할 수 오픈소스 인메모리 저장소이다. 높은 성능을 가지고 다양한 곳에 활용할 수 있다. 관계형 데이터베이스와 다르게 테이블 구조가 아닌 String, Set, Map, Sorted Map 등 여러 데이터 저장소를 가지고 있어 유연성이 좋아 많은 기능들을 구현할 수 있고, 현대적인 서버 구조에서 세션 관리나 캐시는 빠질 수 없는 구성요소로 많이 이용된다. 많은 서비스들이 점점 더 속도가 빨라져야하고, 많은 유저의 트래픽을 감당 해야하는 상황에서 분산환경에서 캐시, 세션관리가 필수이고, 개발자는 개발하기 위한 기능에만 집중하고 데이터 저장과 읽기는 쉽게 구현하고, 문제가 생길경우 계층이 구분되어 있기에 문제가 생긴 부분만 확인해보면 된다는 이점이 있다.
Redis 는 Remote Dictionary Server 의 약자로 원격지에 딕셔너리 방식으로 데이터를 저장하는 서버라는 뜻이다. 데이터 관점에서는 스토리지이고, 영속성 관점에서는 전통적인 DBMS 역할을 수행한다. 또한 미들웨어로 볼 수도 있는데, 최종적인 서비스를 제공하는 어플리케이션이 아니라 어플리케이션이 자신의 서비스를 제공하기 위해 이용하는 중간적인 기능을 가진 소프트웨어이다. 레디스는 단순하게 데이터를 저장하고 읽어오는 데이터 저장소이지만, 기능들이 다양하여 미들웨어라고도 볼 수 있을것이다.
레디스는 Key-value 저장소로 키를 이용해서 연관된 값을 저장하는 구조로 키를 가지고 데이터 조회, 수정, 삭제를 할 수 있으며 가장 단순한 데이터 저장 방식이여서 빠르고 성능이 좋다.
기존에는 개발 방식이 RDB 를 사용해서 복잡한 관계를 정의해서 저장하는 방식이었다면, 최근에는 데이터는 단순화하고, 분산을 많이 할 수 있는 방식으로 변화하고 있다. 이런 환경에서 키밸류 스토어의 활용성이 커지고, 레디스가 많이 사용되는 이유라고 할 수 있다.
In-memory Database 란?
데이터를 디스크에 저장하지 않고, 휘발성인 RAM 에 저장하며, 디스크와 비교해서 빠른 속도를 가지지만, 가격이 비싸다.
Key-value 구조의 장점
- 단순해서 사용하기가 쉬움
- 해시를 이용해서 값을 바로 읽으므로 속도가 빠름 (추가 연산이 필요없다)
키가 밸류의 위치를 찾아내는 방식은 해싱을 사용한다. 해싱은 해싱 알고리즘을 사용하면 어떤 값을 해시값으로 변환할때 복잡한 연산없이 아주 빠른 속도로 수행할 수 있다. - 분산환경에서의 수평적 확장성 (스케일 아웃)
Key-value 구조의 단점
- 키를 통해서만 값을 읽을 수 있음 (데이터로 검색을 할 수 없다)
- 범위 검색 등의 복잡한 질의가 불가능
Redis 활용범위
- 아주 빠른 데이터 저장소로 활용가능
- 분산된 서버들간의 커뮤니케이션 (동기화, 작업분할 등)
분산된 서버들간에 데이터를 공유할 수 있다. 예를들면 레디스는 외부 저장소이기에 세션과 같은 데이터는 복제된 서버들간에 공유가 되어야하는 데이터인데, 레디스는 이러한 데이터를 서버들간에 공유하는데 자주 사용된다. - 내장된 자료구조를 활용한 기능 구현
레디스의 내장된 자료구조를 사용해서 여러 기능을 쉽게 구현할 수 있는데 예를들면 Sorted set 을 이용한 랭킹 시스템을 쉽게 구현할 수 있다. 지금도 레디스에 많은 기능들이 계속 추가되고 있고, 카프카에서 제공하는 pub/sub 이나 stream 과 같은 기능들이 추가되고 있다.
레디스는 세션 스토어, 캐시, Rate limiting(API 요청량 제한), Job queue(여러 서버들간에 task job 을 저장해두었다가 한 쪽에서는 소비해가는 작업) 이러한 것들을 레디스를 사용하면 쉽게 구현이 가능하다.
휘발성 성질을 가진 레디스를 어떻게 사용하는가?
가장 간단한 사용방법은 레디스에 세션 데이터와 같은 단기적인 데이터를 사용하는 것이다.
DB에 들어가는 데이터는 영속성이 절대적으로 필요한 데이터, 높은 무결성을 필요로 하는 데이터가 들어가고, 세션 데이터와 같은 것들은 사용자의 임시적인 로그인 상태를 나타내는 것이기에 만약 레디스에 장애가 발생해서 세션 데이터가 사라지더라도 사용자에게 미치는 영향은 로그인 상태가 해제되어 재로그인이 필요로 하게 되는 정도이다. 물론 이 또한 발생해서는 안되지만 DB의 장애보다는 비교적 영향력이 덜 하다. 즉, 레디스로 사용하기 적절한 세션 데이터와 같은 것들은 라이프사이클이 짧은 데이터라고 볼 수 있다.
세션 데이터 외에도 캐시로 사용할 수도 있는데 캐시는 어플리케이션과 DB의 중간에서 디스크의 접근 횟수를 줄이기 위해 DB에 가기전에 먼저 레디스에 저장해두고 동일한 데이터 요청이 왔을때 레디스에서 읽어가는 것이다. 이럴때도 레디스에 장애가 발생했을때 사라지는 것은 캐시 데이터이고, 원천 데이터는 디스크에 있어 DB에서 다시 읽어올 수 있다. 그리고 장애가 복구된 후에 레디스에 다시 캐시 데이터를 넣을 수 있다. 이런 장애가 일시적으로 서비스의 속도를 늦출수는 있지만 환경에 따라 사용자가 크게 체감할 수 있는 장애상황은 아닐것이다.
또한, 레디스도 어느정도 영속성을 가질 수 있다. 레디스가 제공하는 백업방식을 이용하면 장애가 생겨도 재시작 되었을 때 기존에 가지고 있던 데이터를 디스크에 저장해두었다가 다시 읽어가는 방식으로 어느정도 영속성을 가지고 있다. 하지만 DBMS 만큼은 아니기 때문에 이 부분도 절충을 통해 절대적인 무결성이 필요한 데이터는 DBMS 에서 관리하는 것이 나을 것이다. 또한, 높은 안정성을 얻기 위해서는 어느정도 속도의 희생이 필요하기에 비효율적이고 영속성을 위해서만 사용하는 경우는 없을 것이다.
정리하면 용도에 맞게 DB와 레디스를 사용하고, 혼합해서 사용하기에도 좋고, 레디스 백업 기능으로 영속성을 확보할 수도 있다.
Redis 데이터 타입의 이해
Strings
- 가장 기본적인 데이터 타입으로 제일 많이 사용됨 (키, 밸류형태)
- 바이트 배열로 저장됨 (binary-safe)
- 바이너리로 변환할 수 있는 모든 데이터를 저장 가능 (jpg 와 같은 파일 등)
- 최대 크기는 512mb
모든 문자는 여기에 들어갈 수 있다. null 도 자체로 문자 코드가 있어 가능하다. 즉, 바이트로 표현했을 때 어떠한 값이 있을것이고, 이러한 경우는 모두 저장이 가능하다. 심지어 jpg 와 같은 파일도 넣을 수 있다. 컴퓨터로 표현할 수 있는 데이터는 바이트로 표현이 가능하여 어떠한 데이터도 스트링으로 넣을 수 있다. 주로 많이 사용하는 것은 캐시와 같은 것인데, 웹 브라우저에서 표시되는 웹 컨텐츠 html 엘리먼트로 이루어진 것들을 저장하기도 하고, 이런 문자열 데이터 타입의 최대 크기는 512 mb 이다.
Strings 주요 명령어
incr 과 decr 은 동시처리를 해도 중복되거나 누락되는 일 없이 최종 결과는 항상 동일하게 나온다. 실제로 게시물 좋아요 수를 실시간으로 카운팅하고 공유하고 싶을 때 서버를 분산시켜 여러 서버에서 한 레디스로 incr 와 decr 를 빈번하게 호출할 것이고, 이 연산은 연산결과가 잘못되지 않고, 중복이나 누락 없이 정확한 결과가 저장이 되어 손쉽게 카운터를 구현할 수 있다.
단순히 set, get 으로 작은 사이즈를 여러번 호출하게 되면 네트워크 비용이 높아지는데, mset, mget 은 한 번에 통신해서 많은 데이터를 처리할 수 있게 하여 성능 향상에 큰 도움이 될 수 있다.
Lists
- Linked-list 형태의 자료구조 (인덱스 접근은 느리지만 데이터 추가, 삭제가 빠름)
- Queue 와 Stack 으로 사용할 수 있음
리스트는 키 밸류 형태에서 밸류안에 하나의 값이 아닌 여러 개의 값이 집합으로 들어가있는 형태인데, 자료구조는 linked-list 형태를 띄고 있다.
링크드 리스트는 데이터들이 일종의 포인터로 연결된 상태로 중간이나 양끝에 데이터를 삽입, 삭제하는게 성능이 아주 좋은 특성이 있다.
array 와 비교를 해서 설명할 때 array 는 인덱스를 통해서 빠른 접근을 할 수 있지만, 데이터 삽입, 삭제가 성능면에서 불리하다. 링크드 리스트는 인덱스로 바로 접근은 할 수 없지만 데이터 추가, 삭제가 빠르다.
또한, queue 와 stack 으로 사용할 수도 있다. 앞, 뒤 데이터를 넣고 빼는 방식으로 앞에서 넣고 앞에서 빼면 stack 이 되고, 앞에서 넣고 뒤에서 빼면 queue 가 된다.
Lists 의 주요 명령어
LRANGE 0 은 시작점, -1 은 끝점(가장 오른쪽), -2 는 가장 오른쪽에서 바로 왼쪽이다.
Sets
- 순서가 없는 유니크한 값의 집합
- 검색이 빠름
- 개별 접근을 위한 인덱스가 존재하지 않고, 집합 연산이 가능 (교집합, 합집합 등)
하나의 set 안에 같은 값을 여러 번 넣더라도 중복으로 여러 개가 들어가지 않고, 하나로만 들어가는 특성이 있다. 이 데이터 구조를 사용하는 이유는 검색이 빨라서 특정 값이 set에 포함되어 있는지 아닌지를 빠르게 알아낼 수 있기 때문이다. 그래서 어떤 인덱스를 통해서 특정 위치에 있는 값에 접근할 수 는 없고, 개별값에 대한 존재여부를 체크하거나 집합연산이 가능하다.
Sets 의 주요 명령어
웹페이지에서 서비스가 특정시간동안 유효한 쿠폰을 발급한다고 하면 사용자들은 딱 한 번씩만 그 쿠폰을 발급 받을 수 있다. 그러면 쿠폰을 발급 받을 수 있게 처리를 시작하기 앞서서 그 사용자가 지금 시간대에 쿠폰을 발급 받지 않았는지를 빠르기 확인을 해야하는데, 이럴 때는 set에 사용자 아이디를 저장해 놓으면 빠르게 확인이 가능하다. 그래서 모든 사용자의 요청에 set 에서 그 사용자가 포함되어 있는지, 즉 쿠폰을 이미 받았는지 여부를 저장해놓고 빠르게 확인하는 활용이 가능하다.
SISMEMBER 는 이 set 에 데이터가 몇 개가 들어있던지 상관없이 동일한 수행속도를 보장한다. 그래서 활용도가 커진다.
Hashes
- 하나의 key 아래에 여러 개의 field-value 쌍을 저장
- 여러 필드를 가진 객체를 저장하는 것으로 생각할 수 있음
- HINCRBY 명령어를 사용해 카운터르 활용 가능
스트링으로도 해시의 구조처럼 만들 수 있다. user1 이라는 키에 name 과 age 를 json 형태로 만들어서 넣어도 되고, 이렇게 하면 필드의 일부만 접근하려고 해도 전체 스트링을 불러와서 파싱을 해서 사용해야한다.
하지만 해시를 사용하면 특정 필드를 지정해서 값을 가져올 수 있어 접근성이 좋다. 다만 여러 필드에 동시에 접근해야 한다면 해시를 사용하면 조금 불편해진다. 이 때는 스트링으로 json object 로 만들면 한 번에 불러올 수 있어 편의성이 좀 더 높아진다.
Hashes 주요 명령어
HINCRBY 명령어를 사용하면 숫자를 int 로 취급해서 카운터 등으로 사용할 수 있다. 기능수, 클릭수 등 각각의 필드에 접근해서 변경하는 등 활용이 가능하다.
Sorted sets
- Set 과 유사하게 유니크한 값의 집합
- 각 값은 연관된 score 를 가지고 정렬되어 있음
- 정렬된 상태이기에 빠르게 최소, 최대값을 구할 수 있음
- 순위 계산, 리더보드 구현 등에 활용
Sorted sets 주요 명령어
ZADD 의 경우 동일한 값이 있으면 스코어를 변경한다.
Bitmaps
- 비트 벡터를 사용해 N개의 Set 을 공간 효율적으로 저장
- 하나의 비트맵이 가지는 공간은 4,294,967,295 (2^32-1)
- 비트 연산 가능
비트맵을 사용하면 공간을 굉장히 효율적으로 사용할 수 있다. 예를들어 특정일에 어떤 사이트의 사용자들의 방문현황을 저장해야한다고 할 때 인덱스는 유저 번호를 나타내고, 0과 1로 방문현황을 구분할 수있다. 42억명의 방문현황을 확인해도 4바이트 (integer 크기) 밖에 안된다.
오늘 방문현황을 today visit, 어제의 방문현황을 yesterday visit 비트맵을 만들어서 어제와 오늘 방문한 사용자의 수를 구하고 싶을 경우 위 두 비트맵을 and 연산하여 둘 다 1인 비트만 결과로 나와서 활용이 가능하다.
Bitmaps 주요 명령어
HyperLogLog
- 유니크한 값의 개수를 효율적으로 얻을 수 있음
- 확률적 자료구조로서 오차가 있으며, 매우 큰 데이터를 다룰 때 사용
- 18,446,744,073,709,551,616 (2^64) 개의 유니크 값을 계산 가능
- 12KB 까지 메모리를 사용하며 0.81% 의 오차율을 허용
비트 카운트와 비슷한 용도를 가진다. 100% 정확한 값을 보장하지는 않고, 약간의 정확도를 포기함으로써 더 큰 효율성을 얻는 자료구조이다.
비트맵에서는 방문자수를 카운트할 때 특정 오프셋을 가지고 해야했기 때문에 사용자 아이디와 같은 숫자에 매핑시킬 수 밖에 없었다. 하지만 하이퍼로그로그를 사용하면 어떤 문자열이든 사용을 할 수 있기에 이름이나 브라우저 아이디 혹은 PC 하드웨어의 아이디를 그대로 사용할 수 있어 자유도가 높다.
용도는 정확히 카운팅을 위한 것이고 데이터가 많을 때 효과가 좋다.
그리고 값을 넣을 때 내부에 데이터를 저장하지 않고, 확률적 데이터 구조를 내부적으로 가지고 있다.
HyperLogLog 주요 명령어
PFMERGE 를 사용하면 두 개의 데이터가 합쳐진 형태로 중복은 걸러지고, 유니크한 값들의 개수를 얻어낼 수 있다.
Redis 연동
Redis 라이브러리 사용
- Lettuce 는 가장 많이 사용되는 라이브러리로 Spring data redis 에 내장되어 있음
- Spring data redis 는 Redis template 이라는 레디스 조작의 추상 레이어를 제공함
레터스가 스프링 데이터 레디스에 내포되어 있는데, 추후에 레터스가 변경되어도 어플리케이션은 변경이 필요하지 않다. 스프링 데이터 레디스라는 레이어를 통해 연동 구현을 했기 때문이며, 이러한 점이 추상 레이어의 장점이다.
분산환경에서 Session store 만들기
Session
- 네트워크 상에서 두 개 이상의 통신장치간에 유지되는 상호 연결
- 연결된 일정시간동안 유지되는 정보를 나타냄
- 적용 대상에 따라 다른 의미를 가짐
웹 로그인 세션
- 웹상에서 특정 사용자가 로그인했음을 나타내는 정보
- 브라우저는 쿠키를, 서버는 해당 쿠키에 연관된 세션정보를 저장함
- 사용자가 로그아웃하거나 세션이 만료될때까지 유지되어 사용자에 특정한 서비스를 가능하게 함
웹 로그인 과정
분산환경에서의 세션 처리
- 서버는 세션 정보를 저장해야 함
- 서버가 여러 대라면 최초 로그인한 서버가 아닌 서버는 세션 정보를 알지 못함
- 세션 정보를 서버간에 공유할 방법이 필요함 (Session clustering)
분산환경에서 RDB 를 사용한 세션 처리
- 관계형 데이터 모델이 필요한가?
- 영속성이 필요한 데이터인가?
- 성능 요구사항을 충족하는가?
분산환경에서 Redis 를 사용한 세션 처리
- 세션 데이터는 단순 key-value 구조
- 세션 데이터는 영속성이 필요 없음
- 세션 데이터는 변경이 빈번하고 빠른 액세스 속도가 필요함
Springboot 에서 세션 관리
- 세션 생성: 요청이 들어왔을 때 세션이 없다면 만들어서 응답에 set-cookie 로 넘겨줌
- 세션 이용: 요청이 들어왔을 때 세션이 있다면 해당 세션의 데이터를 가져옴
- 세션 삭제: 타임아웃이나 명시적인 로그아웃 API 를 통해 세션을 무효화 함
Http Session
- 세션을 손쉽게 생성하고 관리할 수 있게 해주는 인터페이스
- UUID 로 세션 ID 를 생성
- JSESSIONID 라는 이름의 쿠키를 설정해서 사용
// Springboot 에서 HttpSession 사용 예제
@GetMapping("/hello")
public String hello(HttpSession session){
session.setAttribute("user", "user1");
session.getAttribute("user");
session.removeAttribute("user");
return "hello";
}
Redis 를 사용한 Session clustering
서비스 속도를 높이는 캐시 레이어 만들기
Caching 이란?
- 캐시는 성능 향상을 위해 값을 복사해놓고 사용하는 임시 기억 장치
- 캐시에 복사본을 저장해놓고 읽음으로써 속도가 느린 장치로의 접근 횟수를 줄임
- 캐시의 데이터는 원본이 아니며 언제든 사라질 수 있음
캐시는 원천 데이터에 접근하는 것보다 조금 더 비용이 적게들고, 빠르게 접근하기 위해 사용한다. 하지만 캐시에 저장하는 데이터는 복사본이어서 데이터 일관성 문제도 생길 수 있는데 이 부분을 신경써야한다.
Cache 적용
웹에서는 정적인 파일들인 이미지나 자바스크립트 소스 파일들을 캐시로 자주 사용한다. 웹 브라우저에서 새로고침을 해도 변경사항이 바로 나타나지 않은 경우는 캐시때문이다.
서버간 통신에서 캐시를 사용하는 경우는 변경사항이 적거나 있더라도 최신 데이터를 즉각적으로 보여줄 필요가 적으면서, 속도나 요청을 할 서버에 부하를 줄이기 위해 사용하면 효율적이다. 데이터 변경사항이 생기면 다시 데이터를 가져와 캐시에 저장해서 사용할 수 있다.
요즘 서버들은 무상태라고 해서 데이터라는 상태를 가지지 않고, 서버를 여러개로 스케일 아웃하여 트래픽에 대응을 한다. 하지만 데이터베이스는 데이터라는 상태를 가지고 일관성을 맞추어야하기에 유연하게 스케일아웃이 쉽지않다. 그렇게 되면 데이터베이스에 병목이 생기고, 이런 경우에 캐시로 병목을 줄일 수도 있다.
꼭 네트워크상에서 다른 호스트간에 캐시를 적용하는 것 뿐만아니라 하나의 호스트 내에서도 최근에 사용했던 데이터를 내부적으로 가지면서 속도 향상을 위해 사용하기도 한다.
Caching 관련 개념
- 캐시 적중(Cache hit) 는 캐시에 접근해 데이터를 발견함
- 캐시 미스(Cache miss) 는 캐시에 접근했으나 데이터를 발견하지 못함
- 캐시 삭제 정책(Eviction policy) 는 캐시의 데이터 공간 확보를 위해 저장된 데이터를 삭제
- 캐시 전략: 환경에 따라 적합한 캐시 운영방식을 선택할 수 있음(Cache-aside, Write-through 등)
캐시의 적중률이 높으면 캐시를 효율적으로 사용하고 있는 것이다. 캐시는 비싸기에 삭제 정책이 필요한데, 어떤 데이터를 삭제해서 공간을 만들어야할지 정해야한다. 또한, 읽기와 쓰기가 많이 일어나는 환경과 요구사항에 맞게 적합한 캐시 운영전략을 사용하는 것이 좋다.
Cache 운영 전략
Cache-aside (Lazy loading)
- 항상 캐시를 먼저 체크하고, 없으면 원본에서 읽어온 후에 캐시에 저장함
- 장점: 필요한 데이터만 캐시에 저장되고, 캐시 미스가 있어도 치명적이지 않음
- 단점: 최초 접근은 느리고, 업데이트 주기가 일정하지 않기 때문에 캐시가 최신 데이터가 아닐 수 있음
가장 일반적으로 많이 사용되는 전략이다. 읽기 시도가 된 데이터만 캐시에 저장되어서 자주 사용되는 데이터만 캐시에 올라갈 확률이 높아진다. 캐시는 부가적인 성능향상을 위한 기능이다. 캐시에 존재하는 시간을 짧게 주고 만료하는 등의 정책을 통해 최신성 유지가 가능하다.
Write-through
- 데이터를 쓸 때 항상 캐시를 업데이트하여 최신 상태를 유지함
- 장점: 캐시가 항상 동기화되어 있어 데이터가 최신임
- 단점: 자주 사용하지 않는 데이터도 캐시되고, 쓰기 지연시간이 증가함
캐시를 먼저 쓰고 이후에 DB에 쓰기에 데이터 일관성에 대한 고민없이 사용이 가능하다.
Write-back
- 데이터를 캐시에만 쓰고, 캐시의 데이터를 일정 주기로 DB에 업데이트 함
- 장점: 쓰기가 많은 경우 DB 부하를 줄일 수 있음
- 단점: 캐시가 DB에 쓰기 전에 장애가 생기면 데이터 유실 가능
해당 캐시 운영전략은 로그 데이터에 활용해볼 수도 있다.
Cache 데이터 제거 방식
- 캐시에서 어떤 데이터를 언제 제거할 것인가?
- Expiration: 각 데이터에 TTL (Time to live) 을 설정하여 시간 기반으로 삭제
- Eviction algorithm: 공간을 확보해야할 경우 어떤 데이터를 삭제할지 결정하는 방식
- LRU (Least Recently Used): 가장 오랫동안 사용되지 않은 데이터를 삭제
- LFU (Least Frequently Used): 최근에 사용되었더라도 가장 적게 사용된 데이터를 삭제
- FIFO (First In First Out): 먼저 들어온 데이터를 삭제
Spring caching 기능 이용하기
- CacheManager 를 통해 일반적인 캐시 인터페이스 구현 (다양한 캐시 구현체가 존재함)
- 메소드에 캐시를 손쉽게 적용 가능
- @Cacheable : 메소드에 캐시를 적용함 (Cache-aside 패턴 수행)
- @CachePut : 메소드의 리턴값을 캐시에 설정함
- @CacheEvict : 메소드의 키값을 기반으로 캐시를 삭제한다.
@Cacheable
public int getUserAge(String userId){
...
}
Redis 를 활용할 경우 쉽게 구현이 가능한 예시
게임 리더보드(Leaderboard) 만들기
리더보드는 게임이나 경쟁에서 상위 참가자의 랭킹과 점수를 보여주는 기능이고, 레디스는 순위로 나타낼 수 있는 다양한 대상에 응용이 가능하다.(최다 구매 상품, 리뷰 순위, 최다 뷰, 최다 구매상품, 최다 댓글 등) 또한, 리더보드와 같은 기능은 많은 사용자들이 볼 수 있는 특성이 있고, 빈번히 요청되기에 빠른 업데이트와 빠른 조회가 필요하다.
이를 RDB 를 통해 구현하려면 업데이트, 데이터 정렬, 카운트 등의 집계 연산을 수행해야 하므로 데이터가 많아질수록 속도가 느려진다.
레디스를 사용하게 되면 얻는 장점은 다음과 같다.
- 순위 데이터에 적합한 Sorted set 의 자료구조를 사용하면 score 를 통해 자동으로 정렬됨
- 용도에 특화된 오퍼레이션(set 삽입, 업데이트, 조회)이 존재하므로 사용이 간단함
- 자료구조의 특성으로 데이터 조회가 빠름 (범위 검색, 특정 값의 순위 검색)
- 빈번한 액세스에 유리한 인메모리 DB 의 속도
리더보드의 동작을 API 관점에서 보면 다음과 같다.
- 점수 생성/업데이트 => ex: SetScore(userId, score)
- 상위 랭크 조회(범위 기반 조회) => ex: getRange(1~10)
- 특정 대상 순위 조회(값 기반 조회) => ex: getRank(userId)
Redis pub/sub 을 이용한 채팅방 구현
pub/sub 패턴의 이해
- 메시징 모델 중의 하나로 발행(Publish)과 구독(Subscribe) 역할로 개념화 한 형태
- 발행자와 구독자는 서로에 대한 정보 없이 특정 주제(토픽 or 채널)를 매개로 송수신함
메시징 미들웨어 사용의 장점
- 비동기: 통신의 비동기 처리
- 낮은 결합도: 송신자와 수신자가 직접 서로 의존하지 않고 공통 미들웨어에 의존함
- 탄력성: 구성원들간에 느슨한 연결로 인해 일부 장애가 생겨도 영향이 최소화됨
동기 방식은 메시지를 받을 서버가 항상 실행중이어야 하고, 메시지가 몰렸을 때 부하분산을 적절히 할 수 없는 단점이 있다. 하지만 메시지 미들웨어를 사용하더라도 자체에 장애가 생긴다면 단일 실패점이 될 수도 있다. 이런 부분들은 분산화 통해 해결할 수도 있다.
Redis 의 pub/sub 특징
- 메시지가 큐에 저장되지 않음
- 카프카의 컨슈머 그룹같은 분산처리 개념이 없음
- 메시지 발행 시 push 방식으로 subscriber 들에게 전송
- subscriber 가 늘어날수록 성능이 저하됨
레디스의 펍섭 특징은 현재 온라인으로 실행중인 구독자들에게만 메시지가 전송된다. 발행자가 메시지 발행 시 레디스는 메시지를 받고 구독자에게 바로 전송한다.
Redis 의 pub/sub 의 활용
- 실시간으로 빠르게 전송되어야 하는 메시지
- 메시지 유실을 감내할 수 있는 케이스
- 최대 1회 전송(at-most-once) 패턴이 적합한 경우
- 구독자들이 다양한 채널을 유동적으로 바꾸면서 한시적으로 구독하는 경우
큐에 메시지를 저장하지 않기에 유실을 감내할 수 있어야한다. 즉 메시지의 라이프사이클이 짧아야 하고, 중복 메시지는 전송하지 않고, 유실되거나 최대 1회 전송이 되는 패턴을 가진다.
Redis 의 pub/sub 을 이용한 채팅방 구현
Redis streams 을 이용한 Event-driven 아키텍처
- 분산 시스템에서의 통신 방식을 정의한 아키텍처로 이벤트의 생성/소비 구조로 통신이 이루어짐
- 각 서비스들은 이벤트 저장소인 Event-broker 와의 의존성만 가짐
이벤트 브로커로 레디스 스트림즈를 사용할 수도 있고 카프카를 사용할 수도 있다.
Event-driven 아키텍처의 모습
각 서버들은 이벤트 브로커에 이벤트를 생산/소비함으로써 통신한다. 핵심은 서버들이 직접 호출하지 않는 것이다.
Event-driven 아키텍처의 장점
- 이벤트 생산자와 소비자 간의 결합도가 낮아짐
공통적인 Event-broker 에 대한 결합만 있음 - 생산자와 소비자의 유연한 변경
서버 추가, 삭제 시에 다른 서버를 변경할 필요가 적어짐 - 장애 탄력성
이벤트를 소비할 일부 서비스에 장애가 발생해도 이벤트는 저장되고 이후에 처리됨
예를들어 이벤트 기반 아키텍처를 사용하면 위 아키텍츠 그림에서 알림서버를 삭제한다고 해도 결제서버에서 알림서버를 호출하는 부분이 없기에 결제서버를 변경할 필요가 없다.
Event-driven 아키텍처의 단점
- 시스템의 예측가능성이 떨어짐
느슨하게 연결된 상호작용에서 기인함 - 테스트의 어려움
- 장애 추적의 어려움
시스템이 어떻게 동작하는지 파악하기가 어렵다. 예를들어 기존 방식의 아키텍처에서는 주문서버가 어떤 서버를 호출하는지 등의 부분을 알아보기가 쉬운데, 이 아키텍처에서 주문서버는 주문 이벤트 발행하는 로직만 확인할 수 있고 이 이벤트를 어떤 서버들이 소비를 하는지 알기가 어렵다.
예측가능성이 떨어지기에 테스트도 어렵다. 시스템이 유연하기에 정형화된 테스트를 하기가 어렵고, 유연성과의 트레이드오프를 가진다.
비슷한 이유로 장애 추적도 어렵다. 정형화된 메시지 흐름이 바로 보이지 않기에 문제가 생겼을때 하나 하나 추적을 해가야하는데 어려움이 있을 수 있다.
Redis streams 의 이해
- append-only log 를 구현한 자료구조
- 하나의 key 로 식별되는 하나의 stream 에 엔트리가 계속 추가되는 구조
- 하나의 엔트리는 entry ID + (key-value 리스트) 로 구성
- 추가된 데이터는 사용자가 삭제하지 않는 한 지워지지 않음
append-only log 는 AOF 파일을 표현할 때 사용되는 구조이기도 하는데, 이는 일반적인 소프트웨어 공학의 용어로 사용된다. 추가된 데이터가 지워지지 않으면서 로그 파일처럼 끝에 계속 추가만이 가능한 데이터 구조이다.
위 그림에서 스트림안에 엔트리 각각은 로그 한 줄이라고 생각하면 된다. 엔트리 안에는 여러 개의 field 와 value 쌍들이 있다.
Redis streams 의 활용
- 센서 모니터링 (지속적으로 변하는 데이터인 시간 별 날씨 수집 등)
- 사용자별 알림 데이터 저장
- 이벤트 저장소
Redis streams 명령어
- XADD: 특정 key 의 stream 에 entry 를 추가한다. (해당 key 에 stream 이 없으면 생성함)
XADD [key] [id] [field-value]
- XRANGE: 특정 ID 범위의 entry 를 반환한다.
XRANGE [key] [start] [end]
- XREAD: 한 개 이상의 key 에 대해 특정 ID 이후의 entry 를 반환한다. (offset 기반, 동기 수행 가능)
XREAD BLOCK [milliseconds] STREAMS [key] [id]
- XGROUP CREATE: consumer group 을 생성한다.
XGROUP CREATE [key] [group name] [id]
- XREADGROUP: 특정 key 의 stream 을 조회하되 특정 consumer group 에 속한 consumer 로 읽는다.
XREADGROUP GROUP [group name] [consumer name] COUNT [count] STREAMS [key] [id]
Consumer group
- 한 stream 을 여러 consumer 가 분산 처리할 수 있는 방식
- 하나의 그룹에 속한 consumer 는 서로 다른 entry 들을 조회하게 됨
Redis streams 를 이용한 event-driven 통신 개발
HTTP 를 이용한 동기 통신 방식
각 서비스는 필요한 서비스를 직접 호출한다.
Event-broker 를 이용한 메시지 기반 통신
각 서비스는 미리 정의된 이벤트를 소비/생성함으로써 통신한다.
주문, 결제, 알림 3 개의 독립된 서버가 있을 경우 레디스 스트림을 사용하게 되면, 각 서버는 자체로서 자기 역할만 수행하면 된다. 주문이 발생하면 event broker 에 order-events 로 메시지를 넣고, 결제서버는 해당 메시지를 비동기로 결제를 끝내고 payment-events 에 메시지를 보내고, 알림서버는 order-events 와 payment-events 에서 비동기로 메시지를 받아 주문과 결제에 맞는 알림을 전송하게 된다.
여기서 결제서버가 죽는 문제가 생겼을 경우 결제와 결제에 대한 알림은 보류된다. 이후 결제서버가 다시 복구되어서 정상적으로 실행이 된다면, 레디스 스트림에서 주문에 대한 스트림을 받아 결제를 처리하게 되고, 결제 완료에 대한 이벤트를 레디스 스트림으로 보내고 알림서버는 그걸 받아서 결제 알림을 처리할 수 있어 장애회복에 대한 부분도 좋다.
이런 상황에서 서버를 추가하거나 제거하는 것 또한 유연하게 대처가 가능하고, 회복 탄력성이 좋고 일종의 로드 밸런싱 역할도 수행할 수 있다. 주문에 대한 결제서버를 직접 분산처리를 하지 않아도 레디스 스트림즈의 컨슈머 그룹을 활용하면 결제서버의 인스턴스를 여러 개 늘리면서 분산 효과를 주고 로드 밸런스 효과도 준다.
레디스를 이용하면 메시지 브로커를 구현할 수 있고, 카프카와 비교도 된다. 하지만 레디스의 스트림 기능은 아직 초창기이고 활용도는 많이 낮은편이다. 다만 카프카에 비해 환경설정이나 셋팅이 쉽기 때문에 간단하게 스트림즈 기능을 사용할 유스케이스가 있으면 사용하기에 좋을 것이다.
'IT > 대용량 데이터&트래픽 처리' 카테고리의 다른 글
Kafka 개념, 구조 (0) | 2023.06.06 |
---|---|
외부 서비스 연동 시 비동기 처리에 대해서 (0) | 2023.01.27 |
Mysql 정규화, 인덱스, 트랜잭션, Lock, 동시성에 대해서 (0) | 2023.01.19 |