F-Lab
🚀
상위 1% 개발자에게 1:1로 멘토링 받아 성장하세요

대규모 처리 시 Redis 연산의 Atomic을 보장하기

writer_thumbnail

F-Lab : 상위 1% 개발자들의 멘토링

안녕하세요. 중급 개발자를 양성하는 상위 1% 개발자들의 멘토링 F-Lab의 대표 멘토 Fitz 입니다.

 

Redis는 여러 규모있는 회사에서 사용하는 인메모리 데이터베이스입니다.

 

인메모리 데이터베이스인만큼 스케일 아웃을 적용했을 때 공통으로 쓰는 값들을 저장하기도 하고, 이러한 특성을 이용하여 분산 락을 적용하기도 하는 등 꽤 쓸모가 많은 툴입니다.

 

하지만 공유하는 값에 동시 연산을 하게 되면 동시성 문제가 발생하여 의도치 못한 값을 얻게 되어 버그를 유발할 수 있는데요, 물론 트래픽이 적을 경우도 문제가 발생하지만 특히 동시에 여러 연산이 자주 일어나는 대규모 시스템에선 문제가 발생할 가능성이 더 커집니다.

 

이 글에서는 레디스로 Atomic한 연산을 보장할 수 있게하여 안전한 어플리케이션을 구현하는 법을 공유합니다.

 

 

[목차]

  1. Atomic한 연산이란?
  2. 레디스의 연산은 기본적으로 Atomic
  3. 레디스에서 Atomic이 깨지는 경우
  4. 레디스에서 Atomic을 보장하기 위해 제공하는 기능
  5. 어디서 쓰고 있을까?
  6. 마무리

 

 

 

Atomic한 연산이란?

먼저 Atomic한 연산이란 무엇일까요?

 

연산이 Atomic하다는 것은 말 그대로 더 이상 쪼개질 수 없는 연산이라는 말입니다.

 

평소에 자주 쓰는 연산자인 i++ 를 예로 들어보겠습니다. 이는 Atomic한 연산일까요?

 

정답은 “아니다” 입니다. i++ 는 아래와 같은 단위로 이루어집니다.

 

  1. 메모리에서 i 라는 변수에 들어있는 값을 읽는다.
  2. 읽은 값에 1을 더한다.
  3. i 라는 변수에 결과값을 덮어씌운다.

 

갑자기 떠오르는 의문이 있습니다. 결국 쪼개지는 것은 맞는데 어떨 때, 왜 문제가 되는걸까요? 🤔

 

i라는 변수에 0이 들어있는 상황을 가정해보겠습니다. 그리고 2개의 스레드가 동시에 i++ 연산을 수행했습니다. 그럼 아래와 같은 상황이 벌어집니다.

 

 

이렇게 했을 때 우리가 예상하기론 i에는 2가 들어가야 합니다. 하지만 정말 2가 들어갈까요?

 

이에 대한 답 또한 “아니다” 입니다. 정답은 “아무도 모른다” 입니다.

 

많은 경우의 수 중 하나를 예시로 들어드리자면, 스레드들의 동작이 동시에 진행된다는 상황을 가정하여 아래의 그림처럼 표현할 수 있습니다.

 

(1) 각 스레드의 메모리에는 i에서 읽은 0이 들어있습니다

 

(2) 각 스레드 내부에서 읽은 값에 1을 더합니다

 

(3) 두 스레드가 동시에 i에 1이라는 값을 덮어 씌웁니다.

 

 

이처럼 우리는 i++ 가 2번 일어나므로 2가 될 것을 기대했지만, 결과로 돌려받는 값은 1이 되었습니다.

 

이렇게 여러 스레드가 접근하는 상황에서 우리가 예상하는 답과 실제 답이 일치하지 않을 때 “스레드 세이프하지 않다” 라고 표현합니다.

 

만약 저 값을 우리가 의도한대로 2가 나오게 하려면 synchronized 블록으로 감싸야합니다. 하지만 이는 성능을 크게 저하시키기에 다른 방법을 사용해야 합니다. 이를 다루기엔 너무 길어지니 이러한 내용은 다른 글에서 다루도록 하겠습니다.

 

위와 같은 상황은 서버가 1대일 때에도 공유 변수가 있다면 가능한 시나리오이며, 여러 서버가 참조하고 있는 전역 변수인 레디스의 값에도 적용되는 내용입니다.

 

 

 

레디스의 연산은 기본적으로 Atomic이 보장됩니다.

만약 위의 i++ 연산을 레디스에서 하려면 어떻게 해야할까요? 기본적으로 아래와 같이 할 수 있을 것 같습니다.

 

  1. get 명령어로 레디스에 저장된 값을 읽어온다.
  2. 1을 더한다.
  3. set 명령어로 값을 덮어쓴다.

 

하지만 마찬가지로 연산이 이렇게 진행된다면 여러 서버에서 동시에 레디스에 요청을 날리게 되었을 때 똑같이 잘못된 값이 보여지는 문제가 발생할겁니다.

 

이러한 문제를 대비해서 레디스는 연산의 Atomic을 보장하는 명령어를 제공하고 있습니다.

 

예를 들자면 위와 같은 목적으로 데이터를 변경하기 위해서 INCR 명령어를 제공합니다.

 

이 명령어는 여러 서버가 동시에 명령을 날려도 synchronized 키워드를 쓴 것처럼 명령어가 묶여서 실행되기에 Atomic이 보장됩니다.

 

아래는 레디스 공식 문서의 INCR 설명입니다.

 

시간 복잡도가 O(1)이기 때문에 성능이 빠릅니다. (대규모 데이터를 다룰 땐 잘못된 명령어 한 번에 레디스가 뻗을 수 있기 때문에 시간 복잡도는 잘 보고 연산하셔야 합니다.)

 

이렇게 공식 문서를 검색해보면 우리가 의도하는 연산을 제공하고 있을 가능성이 높습니다.

 

또한 레디스에서 제공하는 명령어를 쓰면 이렇게 묶여서 연산되기에 필요한 연산이 있다면 레디스 공식문서를 뒤져보는 습관을 기르는 것이 좋습니다.

 

(레디스 공식 문서 링크 : https://redis.io/commands/)

 

 

 

레디스에서 Atomic이 깨지는 경우

위에서 얘기드린 바와 같이 레디스의 명령어들은 Atomic이 보장됩니다. 하지만 그럼에도 Atomic이 깨지는 경우가 있습니다.

 

아마 눈치채셨을 수도 있지만 아까 INCR 명령어를 쓰지 않고 i++ 연산을 구현했던 것과 같이 여러 명령어를 조합해서 쓸 때 Atomic이 깨지게 됩니다. 그래서 여러 명령어를 조합해서 쓸 때에는 꼭 Atomic이 깨지지 않는지 유의해야 합니다.

 


아까 본 “공유 변수와 스레드” 그림에서 각각 “레디스와 서버”로만 바뀌었을 뿐 연산에 대한 것은 똑같습니다.

 

 

 

 

레디스에서 Atomic을 보장하기 위해 제공하는 기능

그렇다면 레디스는 이 문제를 해결하기 위해 어떤 도구들을 제공할까요?

 

Lua Script

레디스는 Lua 언어를 통해 명령어 조합을 스크립트로 사용할 수 있습니다.

 

EVAL 명령어를 통해 사용할 수 있으며, 이 스크립트는 레디스 레벨에서 연산의 Atomic이 보장되기에 안전하게 사용할 수 있습니다.

 

예시로 i++ 와 같은 동작을 하는 스크립트는 이렇게 작성해볼 수 있습니다.

(가독성을 높이기 위해 줄바꿈을 했으며, \를 생략했습니다.)

EVAL "
	local i = redis.call('get', 'i'); 
	i = i + 1;
	redis.call('set', 'i', i);
"

 

 

자세한 내용과 응용은 공식 문서를 읽고 적용해보시면 됩니다

(공식문서 링크 : https://redis.io/docs/interact/programmability/eval-intro/)

 

 

 

Transaction

레디스에서는 트랜잭션 기능 또한 제공합니다. (DB를 쓰면서 많이 보신 그 개념이 맞습니다.)

 

스크립트를 쓰는 방식과 다르게 명령어 형태로 트랜잭션의 범위를 지정할 수 있습니다. 예시를 들자면 아래와 같이 사용해볼 수 있습니다.

 

MULTI
... 조합할 명령어들
EXEC

 

 

마찬가지로 자세한 내용은 아래의 공식문서 링크를 참고해보실 수 있습니다.

 

(공식문서 링크 : https://redis.io/docs/interact/transactions/)

 

[퀴즈]
  공식 문서에 따르면 레디스의 트랜잭션이 실행될 땐
  "다른 클라이언트"의 요청을 처리하지 않는다고 합니다.
  
  레디스는 "다른 클라이언트"를 어떤 방식으로 식별할까요?

 

 

 

 

어디서 쓰고 있을까?

예전 2019년 하이퍼커넥트 재직 당시 분산 락에 대한 블로그 글을 하이퍼커넥트 테크블로그에 기고한 적이 있습니다.

(링크 : https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html)

 

이때 첨부했던 RedissonLock 을 예시로 보여드릴 수 있습니다.

 

이렇게 오픈소스에서도 여러 명령어 조합을 Atomic하게 만들어 버그를 방지하면서 성능을 높일 수 있도록 하고 있습니다.

 

Lua Script 사용 예시

 

❗️ 분산 락은 꼭 필요할 때만 써야 합니다.

최근 취준생분들의 포트폴리오에 분산락이 많이 보입니다.
학습용으로 사용해 본건 좋지만 문제 해결 관점에서
분산락을 어필하기란 쉽지 않을겁니다.

락을 건다는 것은 병목 지점을 만드는 것입니다.

물론 손 쉽게 동시성 문제를 해결할 수 있지만,
락을 걸지 않을 방법이 있다면 쓰지 않는게 낫습니다.

개인적으론 난이도와 성능의 트레이드오프라 생각합니다.

 

 

 

 

마무리

동시성 이슈는 어려우면서도 정확하게 동작하는 어플리케이션을 구현하기 위해 꼭 필요한 개념입니다.

 

규모있는 환경에서 동시성을 보장할 수 있는 방법을 찾고 계신 분들에게 이 글이 도움이 되셨으면 좋겠습니다.

 

긴 글 읽어주셔서 감사합니다.

 

 

 


 

 

 

 

 

사수가 없어 성장하기 힘드신가요?

F-Lab에서 빅테크 기업 타이틀과 실력을 겸비한 멘토님들께 실력 향상을 위한 멘토링을 받을 수 있습니다.

 

개발 경험이 있는 취준생이거나 7년 이하 경력 개발자라면 충분히 멘토링을 받아 뛰어난 개발자로 성장하실 수 있습니다.

 

👉 F-Lab에 대해 알아보기

 

 

 

 

ⓒ F-Lab & Company

이 컨텐츠는 F-Lab의 고유 자산으로 상업적인 목적의 복사 및 배포를 금합니다.

조회수

멘토링 코스 선택하기

  • 코스 이미지
    Java Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    Node.js Backend

    아키텍처 설계와 대용량 트래픽 처리 능력을 깊이 있게 기르는 백앤드 개발자 성장 과정

  • 코스 이미지
    Python Backend

    대규모 서비스를 지탱할 수 있는 대체 불가능한 백엔드, 데이터 엔지니어, ML엔지니어의 길을 탐구하는 성장 과정

  • 코스 이미지
    Frontend

    기술과 브라우저를 Deep-Dive 하며 성능과 아키텍처, UX에 능한 개발자로 성장하는 과정

  • 코스 이미지
    iOS

    언어와 프레임워크, 모바일 환경에 대한 탄탄한 이해도를 갖추는 iOS 개발자 성장 과정

  • 코스 이미지
    Android

    아키텍처 설계 능력과 성능 튜닝 능력을 향상시키는 안드로이드 Deep-Dive 과정

  • 코스 이미지
    Flutter

    네이티브와 의존성 관리까지 깊이 있는 크로스 플랫폼 개발자로 성장하는 과정

  • 코스 이미지
    React Native

    네이티브와 의존성 관리까지 깊이 있는 크로스 플랫폼 개발자로 성장하는 과정

  • 코스 이미지
    Devops

    대규모 서비스를 지탱할 수 있는 데브옵스 엔지니어로 성장하는 과정

  • 코스 이미지
    ML Engineering

    머신러닝과 엔지니어링 자체에 대한 탄탄한 이해도를 갖추는 머신러닝 엔지니어 성장 과정

  • 코스 이미지
    Data Engineering

    확장성 있는 데이터 처리 및 수급이 가능하도록 시스템을 설계 하고 운영할 수 있는 능력을 갖추는 데이터 엔지니어 성장 과정

  • 코스 이미지
    Game Server

    대규모 라이브 게임을 운영할 수 있는 처리 능력과 아키텍처 설계 능력을 갖추는 게임 서버 개발자 성장 과정

  • 코스 이미지
    Game Client

    대규모 라이브 게임 그래픽 처리 성능과 게임 자체 성능을 높힐 수 있는 능력을 갖추는 게임 클라이언트 개발자 성장 과정

F-Lab
소개채용멘토 지원
facebook
linkedIn
youtube
instagram
logo
(주)에프랩앤컴퍼니 | 사업자등록번호 : 534-85-01979 | 대표자명 : 박중수 | 전화번호 : 0507-1315-4710 | 제휴 문의 : info@f-lab.kr | 주소 : 서울특별시 강남구 테헤란로63길 12, 438호 | copyright © F-Lab & Company 2024