테크

2026. 05. 12

Redis Lua 스크립트로 분산 환경의 원자성 문제 해결하기

GET과 INCRBY 사이, 아무것도 끼어들지 못하게

Redis Lua 스크립트로 분산 환경의 원자성 문제 해결하기Redis Lua 스크립트로 분산 환경의 원자성 문제 해결하기

이커머스 플랫폼을 운영하다 보면, 신제품 론칭과 기획전 등 단기간에 트래픽이 몰리는 이벤트가 잦습니다. 그만큼 이상거래 탐지는 피할 수 없는 숙제입니다. 물론, Z세대 전문 커머스 플랫폼 ‘에브리유니즈(everyuneez)’도 예외가 아닙니다. 지금부터 에브리유니즈에서 실시간 주문 모니터링을 구현하면서 마주친 동시성 문제와, Redis Lua 스크립트로 이를 해결한 과정을 공유해보고자 합니다.



1️⃣ 요구사항: 무엇을 감지해야 하는가

요구사항 자체는 단순합니다. “동일 고객 정보로 1분 내에 주문이 N회 이상이면 알림을 보낸다.” 시간 윈도우 안에서 카운터를 누적하고 임계값을 체크하면 됩니다. 결제 금액도 같은 구조로 감시합니다. 단, 알림은 임계값을 처음 넘는 순간 딱 한 번만 발송합니다.


고객 "abc123" — 최근 1분간 주문:
  1회 → 2회 → 3(임계값 3 최초 도달, 알림 1회)4회 → 5(이미 초과한 상태, 알림 없음)


단일 서버라면 어렵지 않습니다. 문제는 분산 환경입니다.



2️⃣ 문제: 분산 환경에서 race condition이 생기는 구조

에브리유니즈는 여러 웹서버 인스턴스로 운영됩니다. 인기 상품에 주문이 몰리면, 수십 개의 프로세스가 동시에 같은 Redis 카운터를 올리고 임계값을 체크하게 됩니다.


                     ┌─── Web Server A ───┐
주문 요청 ──────────▶│  GET → INCRBY → 비교  │──▶ Redis
                     └────────────────────┘
                     ┌─── Web Server B ───┐
주문 요청 ──────────▶│  GET → INCRBY → 비교  │──▶ Redis  (동시 접근)
                     └────────────────────┘
                     ┌─── Web Server C ───┐
주문 요청 ──────────▶│  GET → INCRBY → 비교  │──▶ Redis  (동시 접근)
                     └────────────────────┘


각 인스턴스가 GETINCRBY를 따로 보내는 한, 그 찰나의 순간에 다른 인스턴스의 명령이 끼어들 여지가 항상 존재합니다. 서버 A가 읽은 값을 기준으로 판단하는 사이에 서버 B가 값을 바꿔버리면, 집계가 어긋나고 의도치 않은 버그로 이어집니다.


단순한 순차 명령 조합이 마주하게 되는 레이스 컨디션(Race Condition)을 두 가지 케이스로 살펴보겠습니다.


Case 1. ’처음 넘는 순간’ 감지 실패

// ❌ "처음 넘는 순간" 감지 불가
async function incrAndCheck(key, amount, threshold) {
  const current = await redis.incrby(key, amount);  // (1) 증가
  if (current >= threshold) {                         // (2) 비교
    return { current, exceeded: true };
  }
  return { current, exceeded: false };
}


단순히 임계값을 넘었는지만 확인한다면 INCRBY가 반환하는 증가된 값만 확인하면 됩니다. 하지만 우리의 요구사항은 처음 넘는 순간을 감지하는 것입니다. 따라서 증가 전 값(prev)과 증가 후 값(current)을 모두 알아야 합니다.


// ❌ race condition 발생
async function incrAndCheck(key, amount, threshold) {
  const prev = Number(await redis.get(key)) || 0;   // (1) 이전 값 조회
  const current = await redis.incrby(key, amount);   // (2) 증가
  if (current >= threshold && prev < threshold) {     // (3) 경계 통과 판정
    return { current, firstCross: true };
  }
  return { current, firstCross: false };
}


GETINCRBY 사이에 다른 요청이 끼어들면 어떻게 될까요? 두 요청이 완전히 동일한 prev 값을 읽게 되어, 둘 다 자신이 처음 경계를 넘었다고 오판하는 상황이 발생합니다.


시간 →
            prev=99
요청 A: GET ──────────────── INCRBY → 100  (99 < 100, 100 >= 100 → 최초!)
요청 B:     GETprev=99 ─────── INCRBY → 101  (99 < 100, 101 >= 100 → 최초!)
                  ↑ 같은 prev를 봄


결과적으로 알림이 중복 발송됩니다. 이는 단순한 로직 버그가 아니라, 분산 환경의 구조적 한계입니다.


Case 2. 환불·취소 시 매출 차감 손실

매출을 실시간으로 누적하다가 주문 취소나 환불이 발생하면 해당 금액을 차감해야 합니다. 이를 단순하게 구현하면 두 가지 함정에 빠지게 됩니다.


// ❌ 두 가지 함정
const current = Number(await redis.get(key)) || 0;
await redis.set(key, current - amount);   // (1) TTL 제거 (2) 음수 가능


함정 ① 기존 TTL 증발: Redis에서 SET 명령어는 기본적으로 기존 키의 TTL을 제거합니다. INCRBY는 TTL을 보존하지만 SET은 그렇지 않기 때문에, 1분 뒤에 만료되어야 할 카운터 키가 차감 로직을 거치며 영구 키로 남아버려 다음 집계에까지 악영향을 미칩니다.


함정 ② 차감분 손실(Lost Update): 두 건의 환불이 거의 동시에 들어오면, 둘 다 같은 current 값을 읽고 각자 계산한 결과를 덮어씁니다.


시간 →
누적값 = 100
환불 A (금액 50): GET → 100 ─────── SET 50
환불 B (금액 70):      GET → 100 ─────── SET 30
                        ↑ 같은 current        ↑ A의 결과 50을 덮어씀
                                                (최종 30, A의 차감분 50원 손실)


최종 결과가 30원이 되면서, A의 차감분 50원은 허공으로 사라집니다.


두 케이스는 겉보기엔 다르지만 본질적인 원인은 같습니다. ‘읽기→판단→쓰기’의 과정을 여러 개의 Redis 명령으로 나누어 실행하는 한, 그 틈새를 파고드는 동시성 이슈를 완벽하게 막아낼 방법은 없습니다.



3️⃣ 해결책: 네 가지 대안을 검토한 결과

"읽기 → 판단 → 쓰기" 과정에서 다른 요청이 끼어드는 것을 막기 위해, 흔히 세 가지 대안을 떠올립니다.


대안 1. INCRBY + SET NX 플래그

const current = await redis.incrby(key, amount);
if (current >= threshold) {
  // 성공 시 'OK', 이미 키가 있으면 null
  const setResult = await redis.set(`${key}:alerted`, '1', 'EX', ttl, 'NX');
  if (setResult === 'OK') {
    sendAlert();  // SET NX가 원자적이므로 한 번만 성공
  }
}


첫 번째 대안은 INCRBY로 카운터를 올린 뒤, 별도의 플래그 키로 "이미 알림을 보냈는지"를 체크하는 것입니다. SET ... NX 자체는 원자적이라 알림 중복은 방지됩니다. 하지만 윈도우마다, 임계값마다 :alerted 같은 별도 플래그 키를 관리해야 하며 네트워크 라운드트립이 2회 발생합니다.


무엇보다 이 패턴은 "알림을 보냈는지" 같은 부울(Boolean) 상태만 관리할 수 있어, 케이스 2처럼 읽은 값을 계산해 다른 값을 쓰는 구조는 표현할 수 없습니다.


대안 2. WATCH / MULTI (낙관적 잠금)

await redis.watch(key);
const prev = Number(await redis.get(key)) || 0;
if (prev >= threshold) {
  await redis.unwatch();
  return { firstCross: false };  // 이미 넘은 상태
}
const multi = redis.multi();
multi.incrby(key, amount);
const results = await multi.exec();
// results가 null이면 WATCH 이후 다른 쓰기 발생 → 재시도


Redis 트랜잭션을 사용하는 방법입니다. WATCH로 대상 키를 감시하다가, EXEC 전에 다른 클라이언트가 값을 변경하면 트랜잭션 전체를 취소시키는 낙관적 잠금(Optimistic Locking) 방식입니다.


하지만 이 방식에는 치명적인 한계가 있습니다. MULTI ****블록 안에서는 조건 분기가 불가능하기 때문입니다. 따라서 prev 값을 읽어 임계값을 판단하는 로직이나, 환불 시 max(0, current - amount)를 계산하는 로직은 모두 MULTI 밖(애플리케이션)에서 수행해야 합니다. 즉, WATCH가 경합을 감지해 실패시키면 다시 처음부터 시도하는 CAS(Check-And-Set) 구조로 동작하게 됩니다.


원자성 자체는 보장되지만, 결과적으로 다음과 같은 부담을 떠안게 됩니다.


✔️ 재시도 폭발: 동시 쓰기가 잦은 키에서는 충돌로 인해 EXECnil을 반환하는 일이 빈번해집니다.

✔️ 설계 부담: 이로 인해 재시도 루프의 상한선, 백오프(Backoff), 종료 조건 등을 애플리케이션 레벨에서 직접 꼼꼼하게 설계해야 합니다. (케이스 2의 매출 차감 로직도 동일한 설계 부담을 안게 됩니다.)

✔️ 성능 저하: WATCHGETMULTIEXEC으로 이어지는 흐름 탓에 네트워크 라운드트립이 3~4회로 늘어납니다.


대안 3. 분산 락(Distributed Lock)

Redlock 등으로 카운터 키에 락을 걸고 GET → INCRBY → 비교를 순차 실행하는 방법입니다. 원자성은 확실히 보장되지만, 초당 수백 번씩 접근하는 카운터 키에 매번 락을 거는 것은 성능 부담이 너무 큽니다. 작업 도중 락의 TTL이 만료되어 소실되는 위험도 존재합니다.


대안 4. Lua 스크립트(⭐️최종 선택)

앞서 설명한 세 가지 대안은 모두 여러 개의 명령어를 애플리케이션 레벨(외부)에서 조립하고 통제하려고 했기 때문에 네트워크 왕복이 늘어나고 충돌 처리가 복잡해진다는 공통된 한계가 있었습니다.


-- 간략화 예시: ARGV[1]=증가값, ARGV[2]=임계값
local prev = tonumber(redis.call('GET', KEYS[1])) or 0
local current = redis.call('INCRBY', KEYS[1], tonumber(ARGV[1]))
if current >= tonumber(ARGV[2]) and prev < tonumber(ARGV[2]) then
  return 1  -- 최초 경계 통과
end
return 0


반면, 네 번째 대안인 Lua 스크립트는 Redis 내부에서 로직을 직접 실행하는 방식을 취합니다. EVAL 명령으로 Lua 스크립트를 Redis 서버에 보내면 서버 내부에서 직접 실행되며, 스크립트가 실행되는 동안 다른 어떤 클라이언트의 명령도 처리하지 않습니다. 스크립트 안의 GETINCRBY 사이에 다른 명령이 끼어들 여지가 없습니다. Redis의 명령 실행이 단일 스레드에서 직렬화되기 때문에 가능한 보장입니다.


Case 2도 같은 원리로 GET → math.max 계산 → SET 세 연산을 한 번의 스크립트 안에서 원자적으로 처리할 수 있습니다.


아래 표는 네 가지 대안을 비교한 것입니다.

물론, Lua가 모든 상황의 정답은 아닙니다. INCRBY만 필요한 단순 카운터라면 Lua를 굳이 쓸 이유가 없습니다. 하지만 ‘읽기 → 판단 → 쓰기’가 하나의 단위여야 할 때, Lua는 복잡성을 낮추면서도 안정성을 확보할 수 있는 가장 합리적인 선택지였습니다.



4️⃣ 구현: Lua 스크립트 코드 리뷰

Case 1. 문제 해결 경계 통과 감지

이제 실제로 에브리유니즈 서비스에 적용한 스크립트를 살펴보겠습니다. 스크립트가 해야 할 일은 세 가지입니다.


✔️ 카운터를 원자적으로 증가시킨다.

✔️ 임계값을 처음 넘는 순간을 감지하고, 이미 넘은 경우에는 반응하지 않는다.

✔️ 시간 윈도우의 TTL이 의도한 대로 유지되도록 보정한다


-- KEYS[1]: 카운터 키
-- ARGV[1]: 증가값 / ARGV[2]: TTL(초) / ARGV[3]: 임계값
-- 반환값(level): -1(ERROR), 0(NONE), 1(FIRST_CROSS)

local inc = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local threshold = tonumber(ARGV[3])

-- 1. 방어적 프로그래밍: 잘못된 입력 시 에러 규약 반환
if not inc or not ttl or not threshold then
  return {-1, -1}
end
if inc <= 0 or ttl <= 0 or threshold <= 0 then
  return {-1, -1}
end

-- 2. 원자적 증가 + TTL 보정
local prev = tonumber(redis.call('GET', KEYS[1])) or 0
local current = redis.call('INCRBY', KEYS[1], inc)

local key_ttl = redis.call('TTL', KEYS[1])
if key_ttl == -1 then
  redis.call('EXPIRE', KEYS[1], ttl)
end

-- 3. 경계 통과 판정: 최초인지 아닌지 구분해서 반환
if current >= threshold and prev < threshold then
  return {current, 1}
end

return {current, 0}


짧은 스크립트지만 곳곳에 치열한 고민과 의도가 담겨 있습니다. 핵심 로직을 4단계로 쪼개어 살펴보겠습니다.


① 입력 검증 | 방어적 프로그래밍

local inc = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local threshold = tonumber(ARGV[3])
if not inc or not ttl or not threshold then
  return {-1, -1}
end
if inc <= 0 or ttl <= 0 or threshold <= 0 then
  return {-1, -1}
end


Lua 스크립트는 Redis 내부에서 실행되기 때문에 런타임 에러가 발생하면 디버깅이 매우 까다롭습니다. 만약 tonumber()가 실패해 nil을 반환했는데 이를 그대로 연산에 넣으면, Redis가 ERR 응답을 내뿜으며 전체 스크립트가 중단됩니다.


그래서 스크립트 진입 직후에 모든 인자를 검증하고, 잘못된 입력에는 {-1, -1}이라는 애플리케이션과 약속된 에러 규약으로 응답합니다.


// 애플리케이션에서의 에러 처리
const [, level] = result;  // Lua 테이블 → JS 배열로 반환되므로 배열 구조분해 사용
if (level < 0) {
  console.error("[Monitoring] Lua incrAndCheck 에러 반환:", JSON.stringify(result));
  return;
}


이렇게 에러 규약을 미리 정해두면, Redis의 EVAL 런타임 에러(Rejected Promise로 전파)와 애플리케이션 레벨 에러를 명확히 구분할 수 있습니다. 예기치 못한 크래시를 막고 호출자가 안전하게 예외 처리를 할 수 있게 됩니다.


② 원자적 증가 | GET → INCRBY

local prev = tonumber(redis.call('GET', KEYS[1])) or 0
local current = redis.call('INCRBY', KEYS[1], inc)


앞에서 문제가 됐던 바로 그 패턴입니다. 차이는 단 하나, 이 두 줄이 Lua 블록 안에서 실행된다는 점입니다. GETINCRBY 사이에 다른 명령이 끼어들 수 없으므로, prev는 정확히 이 요청의 증가 직전 값임이 보장됩니다.


시간 
요청 A: [Lua: GET→99, INCRBY→100]  (99 < 100, 100 >= 100  최초! ✓)
요청 B:                             [Lua: GET→100, INCRBY→101]  (100 >= 100  이미 넘음, 무시)


요청 B의 GET은 요청 A의 Lua가 완전히 끝난 뒤에 실행됩니다. prev=100을 보고 "이미 넘었다"고 정확하게 판단하고, 알림은 나가지 않습니다.


③ TTL 보정 | 윈도우 연장 방지

local key_ttl = redis.call('TTL', KEYS[1])
if key_ttl == -1 then
  redis.call('EXPIRE', KEYS[1], ttl)
end


INCRBY 후에 무조건 EXPIRE를 걸면 안 될까요?


네, 안됩니다. INCRBY는 기존 키의 TTL을 건드리지 않지만, 이미 TTL이 흐르고 있는 키에 EXPIRE를 다시 걸면 남은 시간이 리셋되어 버립니다. 예를 들어 1분 윈도우에서 50초가 지났을 때 새 주문이 들어와 TTL이 다시 1분으로 리셋된다면, 감시 윈도우가 끝없이 늘어나게 됩니다.


그래서 TTL 명령으로 현재 상태를 먼저 확인합니다.


✔️ 반환값이 양수: 이미 TTL이 흐르고 있으므로 건드리지 않음

✔️ 반환값이 -1: TTL이 없는 상태이므로 EXPIRE 설정


INCRBY로 카운터 키가 처음 생성될 때는 항상 TTL 없이 생성되므로 값이 -1이 됩니다. 즉, key_ttl == -1 체크 하나만으로 ‘새로 생성된 키’와 ‘어떤 이유로 TTL이 유실된 키’를 모두 안전하게 커버할 수 있습니다.


④ 경계 통과 판정 | 최초 통과만 알린다

if current >= threshold and prev < threshold then
  return {current, 1}
end

return {current, 0}


level의 의미는 다음과 같습니다.

핵심은 level = 1의 반환 조건입니다. prev < threshold 이면서 current >= threshold인 경우, 즉 오직 이번 요청으로 처음 경계를 넘었을 때만 1을 반환합니다. 이미 임계값을 넘은 상태에서 추가 요청이 들어오면 prev 역시 임계값 이상이므로 조건문을 타지 않고 0을 반환합니다. 이를 통해 알림은 정확히 딱 한 번만 발송됩니다.


Case 2. 문제 해결 매출 차감

이번에는 앞서 본 Case 2의 두 함정(Lost Update, TTL 제거)을 Lua 스크립트 하나로 안전하게 피할 수 있는 방법을 살펴보겠습니다.


-- sales_decr.lua
-- KEYS[1]: 매출 누적 키
-- ARGV[1]: 차감값

local amount = tonumber(ARGV[1])
if not amount or amount <= 0 then return -1 end

local current = tonumber(redis.call('GET', KEYS[1])) or 0
if current <= 0 then return 0 end

local newVal = math.max(0, current - amount)
redis.call('SET', KEYS[1], newVal, 'KEEPTTL')
return newVal


이 짧은 코드 역시 몇 가지 중요한 디테일을 포함하고 있습니다.


① 방어적 프로그래밍 및 Early Return

입력값 검증과 더불어 current <= 0일 때의 조기 종료(Early return)를 적용했습니다. Case 1과 마찬가지로 잘못된 amount(nil, 음수, 0)는 -1을 반환해 호출자에게 에러를 알립니다. 또한, 누적값이 이미 0이거나 키가 없는 상태에서는 굳이 SET을 실행하지 않아 불필요한 쓰기 작업을 피합니다.


② 음수 방지 (math.max)

math.max(0, current - amount)를 통해 차감 결과가 음수면 값을 0으로 고정합니다. 환불 금액이 현재 누적분보다 큰 이상 상황에서도 값이 음수로 떨어지지 않습니다.


③ TTL 증발 방지 (KEEPTTL)

Redis 6.0부터 지원되는 KEEPTTL 옵션을 사용하여, SET 명령어가 기존 키의 TTL을 건드리지 않도록 강제합니다. 이 한 줄이 앞서 언급한 '함정 ①(TTL 증발)'을 막아냅니다.


✔️ Case 1: TTL == -1 분기로 "EXPIRE 재호출 시 발생하는 TTL 리셋" 방지

✔️ Case 2: KEEPTTL 옵션으로 "SET 호출 시 발생하는 TTL 증발" 방지


적용한 방법의 방향은 반대지만, "시간 윈도우가 의도한 시점에 정확히 만료되도록 지킨다"는 목적은 같습니다.


④ 원자적 실행 (Lost Update 방지)

GET → 판단 → SET 세 연산이 Lua 블록 안에서 하나의 단위로 실행됩니다. 동시 환불이 여러 건 들어와도 Lost Update 없이 순차적으로 안전하게 처리됩니다.


다만 차감 총합이 현재 누적값을 초과하는 비정상 상황에서는 초과분이 0으로 clamp되어 버려지므로, 별도의 모니터링·경고 로그로 추적하는 것을 권장합니다.


애플리케이션 연동 Node.js에서의 호출

작성한 Lua 스크립트를 이제 애플리케이션에서 호출할 차례입니다. Node.js 환경에서 많이 사용하는 ioredis 라이브러리는 defineCommand API를 통해 Lua 스크립트를 커스텀 Redis 명령처럼 아주 쉽게 등록할 수 있게 해줍니다.


import type { Result } from 'ioredis';

// 1. TypeScript 타입 확장: 커스텀 명령어 타입 정의
declare module 'ioredis' {
  interface RedisCommander<Context> {
    incrAndCheck(key: string, inc: number, ttl: number, alertTh: number): Result<[number, number], Context>;
  }
}

// 2. Lua 스크립트를 커스텀 Redis 명령으로 등록 (여러 개 등록 가능)
this.redis.defineCommand('incrAndCheck', {
  numberOfKeys: 1,           // KEYS 배열로 들어갈 인자의 개수
  lua: incrCheckScript       // .lua 파일에서 읽어온 스크립트 문자열
});

// 3. 이후 비즈니스 로직에서 일반 Redis 명령처럼 호출
const result = await this.redis.incrAndCheck(
  key,       // KEYS[1]
  inc,       // ARGV[1]
  ttl,       // ARGV[2]
  alertTh    // ARGV[3]
);


이처럼 한 번 세팅해 두면, 네트워크 왕복 1회만으로 복잡한 동시성 제어 로직을 안전하고 깔끔하게 실행할 수 있습니다.


Lua를 써야 할 때 VS 쓰지 말아야 할 때

Redis Lua 스크립트는 동시성 문제를 해결하는 강력한 무기지만, 모든 곳에 쓸 수 있는 도구는 아닙니다. 실무에 적용하기 전, 이 도구가 정말 필요한 상황인지 판단하는 기준이 필요합니다.


⭕ Lua 스크립트가 필요한 순간


❌ Lua 스크립트가 불필요한 순간


⚠️ 주의할 점

실제로 서비스에 이를 적용하며 겪은 시행착오를 바탕으로, 실전에서 주의해야 할 4가지를 정리했습니다.


1️⃣ 스크립트 실행 시간 통제

Lua 스크립트가 실행되는 동안 해당 Redis 노드는 다른 모든 클라이언트의 명령을 처리하지 못합니다. 만약 스크립트에 O(N) 연산이나 느린 명령이 포함되면 노드 전체에 장애를 유발할 수 있습니다. 이 글에서 소개한 스크립트들을 의도적으로 짧게 유지한 이유가 바로 이 때문입니다.


2️⃣ 명확한 에러 규약 설정

Lua 내부에서 redis.call()이 실패하면 스크립트 전체가 에러로 중단됩니다. 따라서 스크립트 초반에 철저한 입력값을 검증하고, 애플리케이션과 사전에 약속된 에러 포맷(예:{-1, -1})을 반환하도록 만들어야 합니다. 그래야 장애 상황에서도 시스템이 뻗지 않고 예측 가능하게 동작합니다.


3️⃣ Redis Cluster 환경의 해시 슬롯 제약

Redis Cluster 환경에서 하나의 Lua 스크립트가 참조하는 모든 KEYS는 반드시 같은 해시 슬롯(Hash Slot)에 속해야 합니다. 만약 하나의 스크립트 안에서 여러 키를 다뤄야 한다면, {user:123}:counter와 같이 해시 태그({})를 사용해 슬롯을 강제로 고정해야 에러를 막을 수 있습니다.


4️⃣ 윈도우 만료 경계의 알림 재발송

‘최초 1회 알림’은 하나의 TTL 윈도우 안에서만 성립합니다. TTL 만료로 카운터가 리셋된 직후, 감시 대상이 또다시 임계값을 넘으면 알림이 한 번 더 발송됩니다. 이는 시간 윈도우 기반 감지의 자연스러운 동작입니다. 만약 짧은 시간 내 중복 알림을 막고 싶다면 발송단에서 쿨다운 키(:cooldown:{key}를 SET NX EX로 점유)를 두어 제어하는 것을 권장합니다.



이 글에서 다룬 Lua 스크립트는 고작 30줄 이내에 불과합니다. 하지만 이 짧은 코드가 보장하는 “읽기와 쓰기 사이에 아무도 끼어들 수 없다”는 성질이, 분산 환경의 중복 알림, Lost Update, TTL 증발 같은 골치 아픈 문제들을 효과적으로 방어할 수 있었습니다.


Redis Lua 스크립트의 본질은 복잡한 비즈니스 로직을 Redis 안으로 끌고 들어가는 것이 아닙니다. "이 연산들은 반드시 하나의 단위로 실행되어야 한다"는 불변식을 코드 레벨에서 강제하는 것입니다.


여러분의 서비스에서도 그 불변식이 깨져 버그가 발생하는 곳이 있다면, 딱 그만큼만 Lua 스크립트를 도입해 보시기를 추천합니다.


-


Written by 장태희 | 비누커머스 소프트웨어 엔지니어

꾸준히 문제를 마주하며, 현상이 아닌 원인을 해결합니다.