Redis에 대한 이해와 분산 락에 대하여 (Redis를 활용한 분산 락을 쉽게 이해하는 법)

J쭈디·2025년 3월 1일
0

이제 우리의 서비스는 V4를 목전에 두고 있다. 나는 그 중에 V3에서 미처 구현하지 못 했던 엘라스틱 서치 적용도 해보고, 레디스를 이용한 분산 락 동시성 제어와 레디스를 이용한 검색 기능 개선도 해 볼 생각이다.

그래서 일단 내가 Redis를 모르기 때문에 먼저 문서화를 하고 내 머릿속에서 정리하는 시간을 가져보려고 한다.

1. 디스크 기반과 인메모리 기반의 차이

Redis는 인메모리 기반 데이터 구조 저장소로 Key,Value 구조로 이루어진 비관계형 데이터베이스 관리 시스템이다.

  • 레디스는 비관계형 데이터 베이스인 NoSQL의 일종이다. 이는 데이터 볼륨이 크고 지연시간이 짧아야하며 유연한 데이터 모델이 필요할 때 최적화 되어있다.
  • 반대 개념으로는 관계형 데이터 베이스도 있다. 이는 SQL이라고도 하며 데이터를 행렬 형식으로 저장하는 DB이며 행에는 데이터 값이, 열에는 속성이 포함된다. NoSQL 보다는 데이터 일관성 제한이 일부 있는 편이다.

데이터 베이스가 이미 있는데, 왜 굳이 인메모리 기반의 Redis를 쓰느냐?
DB와 인메모리 기반의 자세한 특징을 보면 왜 사용하게 되는 지 알 수 있다.

1. 디스크란?

  • 물리적으로 회전하는 플래터와 기계식 헤드로 데이터를 읽고 쓰는 하드디스크 드라이버(HDD)

  • 반도체를 이용해 데이터를 저장하며 HDD보다 빠른 성능의 솔리드 스테이트 드라이브(SSD)

  • 디스크를 쓰는 이유

    • 데이터의 영구 저장: 전원이 꺼져도 데이터가 유지됨.
    • 대용량 저장 가능: 메모리(RAM)는 용량이 제한적이지만, 디스크는 TB(테라바이트) 단위 저장 가능.
    • 용량 대비 가성비가 높음: 같은 용량 기준으로 RAM보다 저렴.
  • 디스크의 단점

    • 속도가 느림: RAM보다 데이터 접근 속도가 훨씬 느림(특히 HDD)
    • Input/output 병목 발생: 디스크에서 데이터를 읽고 쓰는 과정에서 속도 저하가 발생.
      랜덤 액세스 성능 저하: 디스크는 순차 접근이 빠르고 랜덤 접근이 느림
    • I/O 병목 현상이나 랜덤 액세스 성능이 낮은 특징 때문에 DB에서 QueryDSL 등을 이용한 검색을 할 때 느릴 수 밖에 없는 것이다.

2. Redis란?

Redis는 데이터를 메모리(RAM)에 저장하는 인메모리 기반 데이터 저장소
키-값(Key-Value) 형태로 데이터를 저장하며, 이로 인해 조회 성능이 빠름

  • Redis를 사용하는 이유
    • 초고속 데이터 접근: 디스크가 아닌 RAM에서 데이터를 가져오기 때문에 읽기/쓰기 속도가 매우 빠름.
    • 캐싱(Cache) 기능: 자주 사용하는 데이터를 캐싱하여 DB 부하를 줄이고, 응답 속도를 향상시킴.
    • 세션 관리 가능: 로그인 세션 정보를 빠르게 저장/조회.
    • 메시지 큐: Pub/Sub 기능을 활용해 실시간 메시징 가능. 메시지 큐에서 Queue는 저번에 List, set, map 공부하면서 문서화했던 개념중에서 Queue(큐) 자료구조의 FIFO(First-In, First-Out) 원리를 기반으로 한다.
      즉, 메시지를 넣으면 먼저 들어온 메시지가 먼저 처리되는 방식이며, 삽입과 삭제가 각각 정해진 방향에서만 가능하다.

이를 활용해 메시지 큐 시스템(RabbitMQ, Kafka, Redis Pub/Sub 등)은 비동기 처리, 분산 환경에서의 데이터 전달, 트래픽 부하 분산 등의 기능을 수행할 수 있다."

  • 분산 시스템 지원: 여러 개의 Redis 노드를 묶어 사용이 가능하다. 이를 통해 묶인 노드들은 하나의 논리적 저장소처럼 동작하게 되는데 이러한 방식을 클러스터링이라 한다. 이는 데이터 분산 저장, 장애 대응, 확장성의 확보를 가능케한다.

  • Redis의 단점

    • 데이터가 휘발성일 수 있음: 기본적으로 RAM에 저장되므로 서버가 재부팅되면 데이터가 사라질 위험이 있음, 하지만 AOF(Append-Only File)와 RDB(Snapshot) 기능을 활용하면 영구 저장 가능하므로 개선이 가능한 위험성이라고 할 수 있다.
    • 비용이 높음: RAM은 디스크보다 비싸므로 대용량 데이터를 저장하는 데 부담이 될 수 있음.
      복잡한 데이터 쿼리 제한: SQL을 지원하지 않으며, 관계형 데이터베이스처럼 복잡한 조인을 지원하지 않음.

관계형 데이터베이스에서는 DB를 사용할 때 쿼리문을 사용하면, 쿼리문이 동작하면서 데이터 저장소에서 필요한 정보를 꺼내오게 된다. 이 때 데이터 저장소에서는 자주 불러오는 메모리만 저장을 하여 사용하고, 전체 메모리는 Disk에 주기적으로 백업이 되어있는 형태이다. 그래서 같은 조건에서 조회를 하더라도 데이터 베이스를 처음 검색할 때랑 두 번, 세번 검색했을 때랑 속도 차이가 나게 된다.

레디스 등 인메모리 기반은 RAM을 사용해서 빠르게 검색하는 특징이 있는데 아래와 같이 생각해 볼 수 있다.
여행을 가는데 캐리어에 대부분의 짐들을 넣어가고, 미니 배낭에 물, 손수건, 휴대폰 등 가볍고 자주 쓰는 물품을 넣는 것과 비슷한 맥락이라고 볼 수 있다. 미니 배낭에서는 물건을 빠르고 쉽게 꺼낼 수 있지만 어쩌다가 한 번 자주 안 쓰던 물건을 캐리어에서 급히 꺼내야한다면 시간이 오래 걸리게 된다. 여기서 캐리어는 디스크에 해당하고, 미니 배낭은 RAM인 것이다.

2. 레디스를 활용하여 성능을 개선하는 방법

일단, 낙관적 락과 비관적 락은 그동안 해 왔던 일련의 과정(

1. 분산 락이란?

분산 락이란, 분산 환경(다중 스레드 환경)에서 여러 프로세스나 서버가 동시에 같은 리소스(컴퓨터 내 자원)를 변경하는 문제를 방지하는 기술이라고 한다.
어, 혹시 이거... 깃허브 컨플릭트와 비슷한 거 아닌가??? 내가 개발한 부분과 남이 개발한 부분이 겹칠 때, 남의 코드가 먼저 올라오고 내 코드를 그냥 올리면 풀리퀘 전에 컨플릭트 표시(머지가 안 된다는 표시)가 뜨는데, 분산 락이 컨플릭트 표시의 역할을 해주는 모양이다.

예를 들어 물품 판매를 위한 사이트에서 재고 관리를 위한 두 개의 서버가 존재할 때 두 서버가 동시에 한 상품의 서버를 변경시킨다면 조회수 동시 카운트 시 값이 누락되는 것 마냥 재고의 이상이 생길 수 있다는 것이다.

이러한 것을 방지하기 위해 서버에 락을 분산해서 걸어서 한 프로제스만 특정 리소스를 변경할 수 있게 제한을 두는 게 분산 락의 개념이라고 한다.

  • Redis를 분산 락으로 사용하는 이유
    Redis는 빠르고, 네트워크를 통해 접근 가능하며, 원자적 연산(Atomic Operation)을 제공하기 때문에 분산 락 구현에 적합하다고 한다. 여기서 원자적 연산은 원자성을 지키면서 동작하는 연산을 말한다.

    • 원자성이란 작업이 시작과 종료까지 도중에 멈추지 않고 완전히 진행되거나, 아니면 아예 작업이 시작되지 않은 상태가 되는 것을 말한다. 요컨데 만들다 만 작업물로 인해 데이터가 꼬일 일이 없어지는 특징이라고 볼 수 있다. 원자성을 지킨다는 것은 단순히 시작과 종료가 논스톱인 게 아니라, 작업 상 수 틀리면 아예 실행 전의 상태로 유지되는 것까지 포함된 개념이다.
    • 완벽하지 실행되지 않을 거면 차라리 실행 전 상태를 유지하는 것이 원자성의 개념인 것이다.
  • Redis를 이용한 분산 락의 장점

    • 빠른 락 획득/해제: 인메모리 기반이라 디스크 기반 DB보다 속도가 빠르다.
    • 원자적 연산 지원: SET NX(SET if Not eXists)와 EXPIRE을 조합하여 원자적으로 락을 설정할 수 있다.
      • NX란 레디스에서 데이터를 SET 할 때(데이터 베이스에 update-set의 맥락에서의 set) 쓸 수 있는 옵션인데, 추가하려는 데이터의 key값이 존재하지 않을 때만 데이터 SET이 가능케 한다.
      • 이 옵션으로 인해 데이터는 유일한 key값을 가지게 되고, key값만으로도 명확히 구분할 수 있게 된다.
      • EXPIRE란 말그대로 만료설정과 관련된 개념인데, 분산 락에서 활용될 수 있다. EXPIRE로 락의 시간제한을 설정해둔다면 락이 걸린 도중에 서버 다운 등의 문제로 관리자가 락을 제어할 수 없게 되어버린다 해도 시간만 지나면 락이 자동으로 해제되도록 할 수 있는 것이다.이는 애초에 레디스가 인메모리 기반이라 한정적 저장소를 사용하기 때문에 사용되는 개념이기도 하다.
  • 유효시간(Time-To-Live, TTL) 설정 가능: EXPIRE 개념에서 나온 제한 시간에 대한 설정을 TTL설정이라고 한다.

  • 분산 환경에서 사용 가능: 여러 서버에서 동일한 Redis를 바라보면 하나의 중앙화된 락 시스템을 만들 수 있다.

    • 서버는 여러 개이지만 단 하나의 RAM에 있는 Redis를 이용하게 되면 한 컴퓨터에서 락을 다 조절할 수 있게 되는 것이다.

내 개인적인 생각이지만 락을 분산 환경에서 사용하게 되면 중앙집중화 되서 편리하지만 한편으로는 단점도 존재할 거 같다. 만약 중앙에 해당하는 컴퓨터의 Redis가 다운 되어버리면 해당 Redis를 가리키고 있는 모든 서버에 락이 다운되는 것 아닌가? 하는 의문이다.

그리고 단점을 조사해보니 아니나 다를까 이미 Redis의 단점 중에 내가 생각한 단점이 존재했다. ㅋㅋ

  • Redis 분산 락의 단점
    Redis 장애 발생 시 문제 발생: Redis 서버가 다운되면 락이 풀리는 문제가 생길 수 있음.
    분산 락을 위해 추가구현 필요: 기본적인 SET NX 방식만으로는 완벽한 락이 아니다.
    • 이를 해결하기 위해 RedLock 알고리즘을 사용한다.
    • RedLock 알고리즘이란 단일 서버에서의 단일 장애 지점(SPOF) 문제를 방지하기 위한 알고리즘인데, 이는 락이 이게 왜 불상사냐 하면 작업이 하나인데 그게 잠긴다는 건 결국 작업 전체가 잠긴다는 문제의 개념이기 때문이다.

RedLock을 사용해야 하는 이유를 쉽게 설명해보자면, 아래와 같은 비유로 가능할 거 같다.

2. 게임으로 레디스와 분산 락을 이해해보기

<쉬운 이해를 위한 비합리적인 1인 스터디룸 게임>
질서관리를 위한 게임관리자 1명이 있고, 사용자 여러 명인 환경이다.
스터디룸은 이중문인데, 서로 멀리 떨어진 바깥 문이 여러개 존재하지만 내부에서는 단 하나의 스터디룸만 존재한다.
관리자는 사용자 한 명이 들어갔으니 잠가달라고 하면 전광판에 사용자 있음 표시를 하게 한다. 왜냐하면 동시에 단 하나의 스터디룸에만 사람을 들여보내고 문을 잠가야 하기 때문이다.

  • 스터디룸의 문은 항상 열려있고, 관리자만 잠그고 열 수 있다.
  • 사용자는 스터디룸 사용 전 관리자에 의해 문이 잠겨지고, 공부 후 열어주면 다음 사용자가 이용이 가능하다.

    이런 느낌이랑 비슷하다. (그림 출처는 GPT)
  • 이 때, 관리자가 문을 열어놓은 채 사정이 생겨서 더 이상 관리를 못 하게 된다면?
    • 여러 사용자끼리 다툼 끝에 스터디룸을 한 명씩 사용이 가능할 테고, 다툼에 밀린 사용자는 불쾌해하며 스터디룸을 이용조차 안하고 귀가할 것이다.
    • 결국 스터디룸을 전체가 사용하지 못하고 일부만 사용하게 되는 결론이 나버린다.
    • 이 케이스가 바로 단일 레디스가 다운되어서 락이 해제되어 버리는 케이스인데, 한 명의 관리자만 존재하므로 그 관리자가 없으면 단일 장애 지점(SPOF)이 되어버리는 것이다.
  • 반대로 관리자가 이용자가 들어갔을 때 문을 잠그고 열어주지 않은 채 사정상 관리를 지속하지 못하게 된다면?
    • 사용자는 이용 중에 갑자기 스터디룸에 갇히게 된다.
    • 관리자가 재출근하기 전에는 꼼짝없이 갇힌 채 관리자만 기다려야 하는 호러같은 상황이 발생할 것이다.
    • 다음 사용자 또한 그 호러같은 상황으로 인해 스터디룸을 사용하지 못하게 된다.
    • 이 상황이 바로 데드락인 것이다. 말 그대로 죽음의 락인 것이다.
  • 또 다른 문제로 바깥 문에서 한 명이 들어갔다고 하고, 관리자에게 잠금을 요청했는데 사용자의 사정으로 전광판 알림을 켜기 전에 또 다른 한 명이 거의 동시에 방에 들어왔다고 잠가달라고 한다면?
    • 최종 스터디 룸은 하나인데 두 사람이 들어가서 바깥 문만 잠겨있는 상황이 발생한다.
    • 결국 스터디룸 내부에서는 한 명만 사용이 가능해서 문제가 발생하게 된다.
    • 이것은 분산 서버에서 동시에 락을 요청했을 때 두 개 이상의 서버에서 동시에 락이 걸려버리는 발생하는 락 충돌 되시겠다.
      -> 틀린 개념이라 추후 공부 예정

그리고 이 게임에서 두 상황을 방지하기 위한 두 가지 방지책을 제시할 수 있을 것이다.
하나는 아예 관리자를 여러 명으로 늘리는 것이다.
그리고 또 하나는 문이 잠긴 후 어느정도 시간이 지나면 관리자가 없더라도 문이 저절로 열리는 시스템을 적용하는 것이다.
여기서 전자는 레디스를 여러 노드로 분산시키는 것이고, 후자는 TTL 설정에 해당된다.
근데 그러면 바깥 문이 잠겼는데 또 다른 문에 사용자가 들어가서 잠갔을 때의 문제는? 관리자를 늘렸을 때, 관리자 과반수 이상이 동일한 바깥 문을 잠가야지만 최종적으로 문이 잠기게 되는 시스템을 도입하면 된다. 이게 바로 RedLock의 개념인 것이다.

이 때 여러 명의 관리자는 참 비합리적인 스터디룸 답게 각자 무전기를 사용하는데 무전기의 전파가 완벽하지 않고 가끔 늦게 정보가 전달되기도 한다. 이로 인해 과반수의 동의를 얻은 내용을 듣지 못한 관리자는 문이 아직 안 잠긴 줄 알고 들어간 사람의 문이 잠겼는지 지속적으로 모니터링하게 된다. 그러한 뻘짓을 방지하기 위해서 한 사람이 들어가고 너무 오랜 시간 잠기지 않았다는 판단이 될 시에는 계속 기다리지 않게 하는 로직이 필요하다.
이러한 개념은 네트워크 지연으로 인한 락의 무한로딩 문제를 방지해주고, 락이 안 걸리고 무한로딩이 될 거 같으면 포기하고 재시도 하는 것이다.

중요
그럼에도 Redis는 완벽하지 않다.
1. AOF로 개선이 가능하다 해도 기본적으로 휘발성 구조를 가져서 데이터 손실 위험이 있다.

2.Redlock 자체도 완벽하지 않은 개선책이다.

  • 과반수의 Redis 노드(관리자)가 동의해야 락이 확정되지만, 락이 걸리기까지 시간이 변동될 수 있다.만약 네트워크 지연으로 인해 일부 노드가 동의하기 전에 다른 노드가 락을 포기하면, 최종적으로 락이 걸리지 않을 수도 있다.
    • 최악의 경우까지 생각해보면 관리자가 5명일 때, 한 명의 이용자가 스터디룸에 들어갔고 두 명의 허가 하에 문이 잠기기 직전이었다. 하지만 나머지 3명은 통신장애로 인해 이 정보를 받지 못한 채 너무 오래 기다렸다고 판단하고 락을 포기해버리는 일이 발생할 수 있다.
  • TTL에도 헛점이 존재한다. 작업이 오래 걸려서 작업시간보다 TTL 시간이 먼저 지나버리면 데이터 충돌이 날 수 있기 때문이다.
    • 스터디가 끝나기도 전에 자동으로 문이 열려버려서, 새로운 이용자가 들어와 문을 잠가달라고 요청할 수 있다. 이 경우 안에서 공부하던 사람은 아직 시간이 남았는데도 문이 열린 걸 보고 당황할 것이고, 새로 들어온 사람은 자기가 정상적으로 예약했는데 안에서 다른 사람이 버티고 있는 걸 보고 분노할 것이다.
  • 레디스에서는 락 재진입이 불가능하다. 원래라면 동일 서버에서 락을 걸고, 같은 서버가 다시 락을 요청하면 획득할 수 있어야 하는데, 같은 프로세스라 해도 무조건 새로운 락으로 인식한다.
    • 이게 문제가 되는 이유는 프로세스가 요청을 하고 락이 걸려서 작업을 하는 도중에 다시 한 번 같은 요청을 했을 때 다음 작업 요청이 무한 대기해버릴 수 있기 때문이다. 이걸 스터디룸에 비유하자면 이미 공부 중인 사람이 공부 연장으로 1회 더 하겠다고 한 것이다. 그렇다면 그냥 이어서 스터디를 하면 되는 것인데, 스터디룸 안에서 공부하면서 저 연장할게요~ 하는 사람을 보고 밖에 있는 관리자는 응~ 이미 사람 있어요. 거부할게요 이러는 게 무한반복 된다는 얘기다.
    • 이 문제를 해결하기 위해서는 락을 관리하는 방식이 바뀌어야 한다고 한다. 예를 들자면 사용자에게 ID카드를 부여하고 스터디룸 내부에서 해당 카드를 인식시키면 관리자는 그 ID 카드로 내부의 사람이 누구인지 구분할 수 있게 되고, 같은 사람의 연장신청을 받아주게 되는 것이다. 이러한 방식은 락의 횟수를 카운팅하여 연장할 수 있게 해 주는 구조라고 한다.
    • 이 방식은 락을 획득한 사용자를 명확하게 구분하는 방법이며, 락 횟수를 카운팅하여 연장할 수 있게 해 준다. 이를 Redis에서는 owner ID를 저장하고 요청이 동일 ID라면 TTL을 연장하는 방식으로 구현할 수 있다.

일단 나는 이정도 공부했으면 나름 이해한 거라 생각이 되기 때문에, 이제는 본격적인 분산 락을 위한 구현방법을 찾아보기로 했다.

<출처>
https://wildeveloperetrain.tistory.com/21
https://aws.amazon.com/ko/compare/the-difference-between-relational-and-non-relational-databases/
https://velog.io/@choidongkuen/서버-메세지-큐Message-Queue-을-알아보자
https://velog.io/@orcasuit/Atomic-Operation
https://sjh836.tistory.com/178
https://candypoplatte.github.io/TIL/2018/12/21/higher_order_function.1/
https://velog.io/@haron/Redis-Expire-를-어떻게-관리할까
https://codingdog.tistory.com/entry/redis-키-값을-일정-시간이-지나면-expire-시켜-봅시다
https://medium.com/sjk5766/redis가-제공하는-redlock을-알아보자-2feb7278411e
https://mangkyu.tistory.com/311
https://velog.io/@sweet_sumin/단일-장애-지점이란-무엇인가요-SPOF

profile
언제 어느 위치에 있더라도 그 자리의 최선을 다 하는 사람이 되고 싶습니다.

0개의 댓글