Lua Script
Lua Script는 여러 Redis 명령을 서버 측에서 하나의 단위로 실행하기 위한 내장 스크립팅 기능으로, 명령 묶음을 원자적으로 처리해야 할 때 사용한다.
Lua Script가 필요한 이유
섹션 제목: “Lua Script가 필요한 이유”Redis의 단일 명령은 싱글 스레드 실행 덕분에 자동으로 원자성이 보장되지만, 여러 명령을 묶어 처리할 때는 추가 보장이 필요하다.
- 명령 사이에 다른 클라이언트의 명령이 끼어들 수 있는 race condition 존재 (예:
GET→ 비교 →SET) - 클라이언트와 서버 사이의 네트워크 왕복(RTT)이 명령마다 누적되어 지연 발생
MULTI/EXEC트랜잭션은 명령을 큐잉했다 한 번에 실행하지만, 중간 결과를 보고 분기하는 조건부 로직 표현 불가
Lua Script는 위 한계를 모두 해결한다.
- 스크립트 전체가 하나의 명령처럼 단일 스레드에서 실행되어 도중에 다른 명령이 끼어들 수 없음
- 여러 명령이 한 번의 RTT로 묶여 네트워크 지연 감소
- Lua 문법으로 조건문·반복문을 사용해 중간 결과 기반의 분기 로직 작성 가능
동작 원리
섹션 제목: “동작 원리”Redis는 내장 Lua 인터프리터(Lua 5.1)를 통해 스크립트를 실행하며, 실행 동안 이벤트 루프는 해당 스크립트에 점유된다.
실행 흐름
섹션 제목: “실행 흐름”EVAL 명령으로 스크립트를 전달하면 다음 순서로 처리된다.
- 클라이언트가
EVAL <script> <numkeys> <key1> ... <arg1> ...형태로 스크립트와 인자 전송 - Redis 서버가 스크립트를 Lua 인터프리터에 로드하고 컴파일
- 메인 스레드가 스크립트 본문을 순차 실행하며, 내부의
redis.call()호출은 일반 명령과 동일한 경로로 처리 - 스크립트가 끝나면 마지막 반환값을 RESP 프로토콜에 맞춰 클라이언트에 응답
스크립트가 실행되는 동안 메인 스레드는 다른 클라이언트 명령을 처리하지 않으므로, 스크립트 전체가 사실상 하나의 거대한 원자 명령처럼 동작한다.
Script Cache와 EVALSHA
섹션 제목: “Script Cache와 EVALSHA”같은 스크립트를 매번 본문째 전송하면 네트워크와 컴파일 비용이 반복되므로, Redis는 스크립트를 SHA1 해시로 캐싱한다.
SCRIPT LOAD <script>: 스크립트를 서버에 등록하고 SHA1 해시 반환EVALSHA <sha1> <numkeys> ...: 해시만 보내 캐시된 스크립트 실행- 캐시 미스 시
NOSCRIPT에러 반환 → 클라이언트는EVAL로 재전송하며 자동 재캐싱
대부분의 클라이언트 라이브러리(Lettuce, Jedis 등)는 EVALSHA 우선 시도 후 NOSCRIPT 발생 시 자동 폴백 로직을 내장하고 있다.
MULTI/EXEC 트랜잭션과의 비교
섹션 제목: “MULTI/EXEC 트랜잭션과의 비교”Redis는 Lua Script 외에도 MULTI/EXEC 기반 트랜잭션을 제공하며, 두 방식은 원자성을 달성하는 방법과 다룰 수 있는 로직 범위에서 차이가 있다.
동작 방식의 차이
섹션 제목: “동작 방식의 차이”MULTI/EXEC는 명령을 큐잉해 두었다가 EXEC 시점에 일괄 실행하는 구조다.
MULTI로 트랜잭션 시작 → 이후 명령은 즉시 실행되지 않고 서버 큐에 적재EXEC를 받으면 메인 스레드가 큐의 명령을 순서대로 한 번에 실행- 실행 도중 다른 클라이언트 명령이 끼어들지 않음 (Lua Script와 동일한 원자성)
핵심 차이는 큐잉 시점과 실행 시점이 분리되어 있다는 점이다.
- 큐잉 단계에서는 이전 명령의 응답이 아직 없어, 결과를 보고 다음 명령을 결정하는 조건부 로직 작성 불가
- Lua Script는 인터프리터 안에서 즉시 결과를 받아 분기·반복문으로 후속 동작 결정 가능
항목별 비교
섹션 제목: “항목별 비교”| 항목 | MULTI/EXEC | Lua Script |
|---|---|---|
| 원자성 | 큐 일괄 실행으로 보장 | 단일 명령처럼 실행 |
| 조건부 분기 | 불가 (큐잉 단계라 결과 미확인) | 가능 (if, for 등 Lua 제어문) |
| 네트워크 RTT | 명령마다 1회 + EXEC 1회 | 스크립트 전체 1회 |
| 에러 처리 | 큐잉 에러 시 EXEC 거부, 실행 중 에러는 나머지 명령 계속 | 도중 에러 시 즉시 중단, 이미 수행된 쓰기는 그대로 유지 |
| 낙관적 락 | WATCH로 키 변경 감시 후 EXEC 충돌 검출 | 불필요 (스크립트 자체가 원자적이라 race 없음) |
| 적합한 상황 | 단순 명령 묶음 + 충돌 감지 | 중간 결과 기반 로직, 복잡한 원자 연산 |
WATCH의 역할
섹션 제목: “WATCH의 역할”MULTI/EXEC가 분기 로직을 표현하지 못하는 약점은 WATCH로 보완한다.
WATCH key후 트랜잭션 시작 → 다른 클라이언트가 해당 키를 변경하면EXEC가nil을 반환하며 트랜잭션 폐기- 클라이언트가 응답을 보고 재시도하는 낙관적 락(Optimistic Locking) 패턴
- 충돌 빈도가 낮을 때만 효율적이며, 잦은 충돌에서는 재시도 비용이 누적
Lua Script는 스크립트 실행 자체가 단일 단위라 다른 클라이언트가 끼어들 수 없으므로, WATCH 없이도 동일한 원자성을 더 짧은 RTT로 달성한다.
선택 기준
섹션 제목: “선택 기준”- 단순히 여러 쓰기를 묶고 중간 결과 기반의 분기가 필요 없는 경우
MULTI/EXEC로 충분 - “값을 확인 후 결정”이 필요한 모든 시나리오(CAS, 조건부 차감, 분산 락 해제 등)는 Lua Script가 자연스러움
- 클러스터에서 여러 키를 다루는 경우 두 방식 모두 동일 슬롯 제약 → hash tag로 묶어야 함