Redis를 활용한 공유 자원 동시성 처리

Sangho Lee
VUNO SW Dev
Published in
9 min readFeb 15, 2021

안녕하세요, 뷰노 SW 개발팀의 이상호 입니다.

뷰노의 VUNO Med ® 제품은 웹 브라우저에 로그인해서 서비스를 사용하는 클라우드 방식을 지원하며, 이 제품군 중에는 그룹에 포함된 사용자들 간에 특정 자원을 공유할 수 있는 기능이 존재합니다.

예를 들어 특정 그룹에 어떤 파일이 업로드되는 경우, 그룹 내 모든 사용자들이 이를 공유할 수 있으며, 그룹 내 한 사용자가 해당 파일을 삭제를 한다거나, 파일에 대한 정보를 수정하는 경우에도, 그룹 내 모든 사용자가 동일한 결과를 가질 수 있어야 합니다.

이러한 특징 때문에 공유 자원에 대한 동시성 처리를 위한 장치가 필요했으며, 이 글에서는 공유 자원의 데이터 동기화를 위한 장치로, Redis를 활용하여 락을 구현하는 과정과 문제 해결 방법에 대해 소개하고자 합니다.

구현 과정에 대한 소개에 앞서, Redis란 무엇이며 어떤 특징을 가지고 있는지 간략히 알아보도록 하겠습니다.

Redis

Redis(“REmote DIctionary System”)는 NoSQL DBMS의 한 종류로, Key-Value 기반의 인-메모리 데이터 저장소 입니다.

Redis 주요 특징

  1. Single-thread 기반이며, Key-Value 구조의 형태로 데이터를 관리합니다.
  • Single-thread 기반이라는 의미는, 명령어의 실행과 이벤트를 처리하는 코어 부분이 single-thread로 구성되어 동작하여 Atomic 처리를 보장한다는 것을 말하는 것으로, 별도의 시스템 명령들을 사용하는 전용 sub-thread가 존재하며 이는 버전별로 상이할 수 있습니다.
  • 따라서 Redis에 존재하는 모든 Key를 조회하는 Keys 명령어와 같이, Long-Time 명령을 수행하여 다른 명령어들이 처리될 수 없는 상태를 유의해야 합니다.

2. 모든 데이터가 메모리에 상주하여 처리되므로, 속도가 상당히 빠릅니다.

  • 평균 읽기 또는 쓰기 속도가 1ms 미만으로, 서버의 환경에 따라 다르겠지만 일반적으로 초당 수십만 건의 작업을 지원한다고 합니다.

3. 데이터의 안전한 백업과 복구를 위해 다른 서버의 메모리에 실시간으로 복사본을 남기거나, 설정에 따라 아래와 같은 방식으로 디스크에 저장하고 복구할 수 있는 방법도 제공합니다.

  • RDB (Snapshot) 방식
  • AOF (Append On File) 방식

4. Pub/Sub 모델 지원으로 특정 채널을 구독한 구독자들에게 메시지를 전달할 수 있습니다. 단, 전달한 메시지는 보관되지 않음에 유의해야 합니다.

5. 100개가 넘는 오픈소스 클라이언트를 사용할 수 있으며, 다수의 언어가 지원됩니다.

  • Java, Python, PHP, C, C++, JavaScript, Node.js, Ruby, R, Go 등등

6. 국내는 물론 세계 여러 기업들에서 사용하고 있는 BSD 라이선스 기반의 오픈소스 입니다.

Redis 주요 데이터 타입

제한적인 데이터 구조를 제공하는 단순한 Key-Value 저장소와 달리, 애플리케이션의 요구 사항을 충족할 수 있도록 다양한 데이터 구조를 지원합니다.

기본적으로 Key-Value 형태로 데이터가 저장되며, String은 Key-Value가 일 대 일 관계이고, 나머지는 일 대 다 관계가 가능합니다.

  1. String
  • 텍스트 문자열, 숫자(정수 또는 부동소수점), 이미지와 같은 바이너리 데이터도 사용이 가능하며 최대 길이는 512MB로 제한 됩니다.

2. List

  • 추가된 순서가 유지되는 String의 집합으로, Linked-List 라고 생각하시면 되며 주로 큐(Queue)와 스택(Stack)으로 사용됩니다.
  • List Index는 0부터 시작되는데, LPUSH는 Index 0쪽으로 value를 삽입하고 RPUSH는 Index last쪽으로 value를 삽입하게 됩니다.

3. Set

  • 추가된 순서가 유지되지 않는 String의 집합으로, 중복된 데이터는 하나로 처리되며 다른 Set 간의 교차, 통합 및 비교가 가능합니다.

4. Sorted Set

  • Set과 같은 구조이지만, 데이터 저장 시 Score 라는 가중치 값을 같이 저장하고, 이 값에 따라서 데이터의 순서가 정렬되어 저장됩니다.
  • Score가 동일할 경우 Value의 사전 편집 순으로 정렬되며, Value의 중복은 허용되지 않지만, Score는 업데이트 될 수 있습니다.

5. Hash

  • 저장되는 Value가 또 다른 Key-Value(field / value)의 목록으로 구성되는 데이터 구조로, Key-Value(field / value)의 구조를 여러 개 가진 Object 타입을 저장하기에 좋습니다.

Implementation

앞서 소개한 Redis를 활용하여 공유 자원의 동시성 처리를 위해 락을 구현하는 과정을 살펴보도록 하겠습니다.

  1. 락의 획득

락을 획득한다는 것은 사용자가 특정 자원에 대한 접근을 시도한다는 것을 의미하며, 해당 자원에 대해 락이 존재 하는지 확인하고, 존재하지 않는다면 락을 획득하는 과정으로 이루어집니다.

따라서 다른 사용자가 동일한 자원에 대한 락을 획득 하려고 했을 때, 락을 획득할 수 없다면 다른 누군가가 해당 자원에 대해 이미 점유하고 있다는 것을 말합니다.

이를 Redis의 Key-Value 관계에서 설명하자면, 공유 자원에 대한 Key를 생성하고, 해당 Key에 대한 Value로 사용자가 로그인 했을 때의 Session ID를 기록하여, 해당 자원에 대해 누가 점유하고 있는지를 나타냅니다.

아래 Sequence Diagram은 락을 획득하는 과정에 대해 설명합니다.

2. 락의 해제

락을 해제하는 과정은 비교적 간단하며, 사용자가 특정 자원에 대한 점유를 더 이상 유지하지 않겠다는 것을 의미합니다. 이는 Redis에서 해당 자원에 대한 Key를 삭제하여, 다른 사용자들에게 락을 획득할 수 있는 기회를 부여하게 됩니다.

3. 락의 영속성 문제 해결

위 두 가지 사항을 통해 공유 자원에 대한 기본적인 락의 획득 및 해제를 쉽게 확인할 수 있었습니다. 하지만 여기에는 사용자가 특정 자원을 영원히 점유하는 경우에 대한 대책이 빠져있습니다.

예를들어 사용자가 특정 자원을 점유 후 자리를 비운다거나 예상치 않은 프로그램 종료와 같은 상황이 발생하는 경우, 다른 사용자들은 이미 점유된 자원들에 대한 락을 획득하지 못하고 락이 해제되기 만을 기다라다 결국 서비스 장애로 이어질 수 있습니다.

때문에 일정 시간이 지나면 락이 만료될 수 있도록 처리를 해야하며, Redis에서는 expire time을 설정하여 일정 시간이 지나면 해당 데이터가 삭제될 수 있도록 지원하고 있습니다.

하지만 expire time이 경과되면 데이터가 자동으로 삭제 되므로, 어떤 데이터가 삭제된 것인지 확인하기 위해서는 추가적인 방법이 필요합니다. 기존의 Key 값에 ‘shadowKey:’ 라는 prefix를 붙인 별도의 Key를 만들고, 이 데이터에 expire time을 설정하여 원본 데이터를 유지 할 수 있도록 하였습니다.

Ex) 10번 그룹의 1번 사용자가 로그온 상태이며, 이 사용자는 그룹 내 공유되는 2번 파일과 3번 파일을 점유하고 있다고 했을때, Redis의 상황을 그림으로 표현하면 아래와 같다고 할 수 있습니다.

또한 Redis의 특징 중 하나인 Pub/Sub 모델을 사용하여 Key가 expire time에 따라서 만료된 경우, 이 이벤트를 메시지로 전달받을 수 있도록 구독하여 F/E에 어떤 자원에 대한 락이 해제 되었는지 정확하게 전달할 수 있도록 하였습니다.

4. 로그아웃 / 앱 종료 / 새로고침에 따른 문제 해결

위의 1, 2, 3번의 상황을 통해서 사용자가 명시적으로 특정 자원에 대한 락을 획득 / 해제하거나, 획득한 락이 영속되는 상황을 피할 수 있는 방법까지 살펴보았습니다.

마지막으로 사용자가 특정 자원들에 대한 락을 획득한 상태에서, 아래와 같은 상황이 발생했을 때의 대책이 추가적으로 필요합니다.

  • 로그아웃
  • 앱 종료
  • 브라우저 새로고침

위 세 가지 상황은 서로 다른 사용자 액션을 통해 발생하지만, B/E(Server) 의 입장에서는 사전에 F/E(Client)와 연결된 WebSocket 의 연결이 모두 끊기는 상황으로 취합될 수 있습니다.

따라서 위와 같은 상황에서는 연결이 끊기는 사용자의 Session ID를 확인하고, 이 Session ID가 가지고 있는 자원에 대한 모든 락을 해제하는 방식으로 문제를 해결할 수 있습니다.

Reference

--

--