
지난 글에서는 동시성을 제어하기 위한 방법들 중 Application 락과 DB 락에 대해 포스팅하였다. 이번 글에서는 지난 글에 이어서 redis를 활용한 방법에 대해 알아보고자 한다.

Key, Value 형태의 비정형 데이터를 관리하는 오픈소스 기반의 비관계형 데이터베이스 관리 시스템이다. 저장소, 캐시, 메세지 브로커 등의 이유로 사용되며 보통 캐시나 세션 저장소로 많이 사용된다.
데이터베이스가 있는데 굳이 이걸 쓰는 이유가 무엇일까?
데이터베이스는 데이터를 물리적인 디스크에 저장하므로, 서버에 문제가 생겨도 데이터는 안전하게 보호된다. 그러나 이렇게 디스크에 직접 접근해야 하기 때문에 사용자가 많아지면 부하가 커져서 전체 시스템이 느려질 수 있다.
서비스의 규모가 작거나 사용자가 아직 많지 않은 초기 단계에서는 웹 서버, 애플리케이션 서버, 데이터베이스로 구성된 기본적인 아키텍처로도 충분하다.
그러나 사용자가 늘어나면서 데이터베이스에 부하가 증가하게 되면, 이때 캐시 서버를 도입하여 부하를 줄이는 방법을 고려하게 된다. 이럴 때 사용할 수 있는 캐시 서버가 바로 Redis다.
캐시는 한 번 읽어온 데이터를 임시 저장공간에 보관하므로, 같은 요청이 여러 번 들어올 경우 매번 데이터베이스를 거치지 않아도 된다. 첫 번째 요청 이후에는 캐시 서버에서 저장된 결과값을 바로 반환할 수 있다. 이렇게 함으로써 데이터베이스의 부하를 줄이고, 서비스의 응답 속도도 향상시킬 수 있다.
Redis는 싱글 스레드라고 많이 알려져 있는데, 어디서는 멀티 스레드라고 한다. 과연 무엇이 맞는 것일까?
Redis 6 rings in a new era: while it retains a core single-threaded data-access interface, I/O is now threaded.
여러 블로그와 정보를 찾으며 알게된 것은 버전별로 특징이 있다는 것이고, 결론은 다음과 같다.
Redis는 부분적으로 멀티 스레드이고 명령의 실행 자체는 싱글 스레드이다. 따라서 Redis의 특징인 원자성은 싱글 스레드를 유지함으로써 여전히 보장이 되는 것이다.
Redis를 왜 사용하지 알게되었다. 그렇다면, Redis의 특징은 어떤 것이 있을까?
Key, Value 구조이기 때문에 쿼리를 사용할 필요가 없다.
데이터를 디스크에 쓰는 구조가 아니라 메모리에서 데이터를 처리하기 때문에 속도가 빠르다.
String, Lists, Sets, Sorted Sets, Hashes 자료 구조를 지원한다.
String : 가장 일반적인 key - value 구조의 형태다.
Sets : String의 집합이다. 여러 개의 값을 하나의 value에 넣을 수 있다. 블로그의 태그 같은 곳에 사용될 수 있다.
Sorted Sets : 중복된 데이터를 담지 않는 Set 구조에 정렬 Sort를 적용한 구조로 랭킹 보드 서버 같은 구현에 사용할 수 있다.
Lists : Array 형식의 데이터 구조다. List를 사용하면 처음과 끝에 데이터를 넣고 빼는 건 빠르지만 중간에 데이터를 삽입하거나 삭제하는 것은 어렵다.
Race Condition(경쟁 상태)가 거의 발생하지 않는다.위에서 배운 내용을 요약해보면, 결국 Redis 내부에서 핵심 동작들은 싱글 스레드로 동작하게 되는 것이므로, 원자성 유지할 수 있다. 그러면 어떻게 Redis는 싱글 스레드로 어떻게 수백만명의 유저들이 빠른 속도로 Redis를 이용할 수 있는 것일까? 애초에 정말로 가능한걸까? 라는 생각이 들었다.
하나의 데이터에 대해서 수백만 명이 요청을 날리게 된다면, 그걸 어떻게 몇 ms로 응답을 할 수 있을 것인가?
StackOverflow - how Redis do concurrentI/O?
Tistory - Redis 동시성, 고립성
결론은 Concurrency(동시성)과 Parallelism(병렬성)의 차이이다.
두 가지 이상의 작업이나 요청이 시간 순서에 따라 동작하는 것. 예를 들면 한 점원이 두 줄의 손님에게 대응하는 것(손님은 순서대로 주문하게 된다.)
두 가지 이상의 작업이 동시에 진행 되는 것. 예를 들면 두줄의 손님이 두 명의 점원에게 주문하는 것(담당 점원이 존재하여 동시에 주문을 받게 된다.)
Redis는 동시성은 있지만 병렬성은 없는 서비스이다.
그렇다면, 동시성으로 인한 문제가 발생할 수 있다는 것인데, Redis에서는 이를 어떻게 해결할까?
Redis 공식 문서를 보면, Redis는 안전한 Transaction을 위해 여러 명령어를 제공한다.
MULTI, EXEC, DISCARD, WATCH 명령어를 제공한다. MULTI와 EXEC를 이용해서 Transaction을 실행하게 되는데, 단순히 이 명령어들만 사용하게 되면 동시성 문제가 발생하게 된다.
그래서 비관적 락을 사용하기 위해 WATCH 명령어를 사용하고, 해당 명령어가 안전한 Transaction을 보장하게 된다고 한다.
그렇다면 Spring에서 Redis를 연결하기만 하면 해당 문제를 예방할 수 있을까?
그렇지 않다. Spring에서 공식적으로 사용하는 Redis Client인 Jedis와 Lettuce에서는 PlatformTransactionManager 구현체를 제공하지 않기 때문에, 이들 클라이언트에서 제공하는 트랜잭션 기능을 이용해야 한다.
또는 SessionCallback을 사용하여 직접 Redis 명령어를 사용하여 Transaction을 설정할 수 있다. 이렇게 하면 Redis 클라이언트에서 제공하는 트랜잭션 기능을 직접 사용할 수 있다.
또는 SessionCallBack을 사용하여 직접 Redis 명령어를 사용하여 Transaction을 설정할 수 있다고 한다.
이 방법에 대해서는 설명이 잘 나와있는 wildeveloperetrain님의 블로그, jjeda님의 블로그, 젊은오리님의 블로그를 참고하길 바란다.
이번 학습에서는 아쉬운 점이 많다. 실습을 해보며 Spring에서의 Redis 사용방법을 더 알아보고 싶었는데 면접과 코테를 준비하느라 학습에 시간을 많이 쏟지 못한 부분이 아쉽다.
추후에 급한 일이 끝나면 이에 관한 실습을 진행하고 해결 과정을 적는 글을 작성하고자 한다.
참고자료
Jihoon Oh님의 블로그
wildeveloperetrain님의 블로그
jjeda님의 블로그
젊은오리님의 블로그