Redis는 프로세스 간 통신과 데이터 저장에 널리 사용되는 인기 있는 인메모리 데이터 그리드입니다. Redis에서 Lua 스크립트를 실행할 수 있다는 것은 들어보셨겠지만, 정작 '왜' 필요한지 확실하지 않으셨다면 이 글이 도움이 될 것입니다.
사전 준비 사항
이 가이드를 따라 하려면 시스템에 Redis가 설치되어 있어야 합니다. 글을 읽는 동안 Redis 명령어 레퍼런스를 함께 참고하면 더욱 효과적입니다.
왜 Lua 스크립트가 필요한가?
한마디로 답하자면, 성능 향상 때문입니다. Redis에서 수행하는 대부분의 작업은 여러 단계로 이루어져 있습니다. 이러한 단계들을 애플리케이션 언어로 처리하는 대신, Redis 내부에서 Lua로 처리할 수 있습니다.
- 이를 통해 더 나은 성능을 얻을 수 있습니다.
- 또한 스크립트 내의 모든 단계는 원자적으로(atomic) 실행됩니다. 즉, 스크립트가 실행되는 동안에는 다른 어떤 Redis 명령어도 끼어들 수 없습니다.
예를 들어, 저는 Redis에 저장된 JSON 문자열을 수정할 때 Lua 스크립트를 활용합니다. 이 내용은 글 후반부에서 자세히 다루겠습니다.
하지만 Lua를 전혀 모른다면?
걱정하지 마세요. Lua는 생각보다 어렵지 않습니다. C 계열 언어(C, C++, Java, JavaScript 등)를 다뤄본 경험이 있다면 충분히 따라갈 수 있습니다. 게다가 이 글에는 바로 실행해 볼 수 있는 작동하는 예제들이 포함되어 있습니다.
예제로 시작하기
먼저 redis-cli를 통해 스크립트를 실행해 보겠습니다. 다음 명령어로 CLI를 시작합니다.
redis-cli그다음 아래 명령어를 실행합니다.
eval "redis.call('set', KEYS[1], ARGV[1])" 1 key:name valueEVAL 명령어는 뒤따르는 스크립트를 실행하라고 Redis에 지시하는 역할을 합니다. "redis.call('set', KEYS[1], ARGV[1])" 문자열이 바로 우리가 실행할 스크립트이며, 기능적으로 Redis의 set 명령어와 동일합니다. 스크립트 텍스트 뒤에는 세 개의 파라미터가 순서대로 옵니다.
- 제공되는 키(key)의 개수
- 키 이름
- 첫 번째 인자(argument)
스크립트의 인자는 KEYS와 ARGV, 두 그룹으로 나뉩니다.
스크립트가 몇 개의 키를 필요로 하는지는 스크립트 바로 뒤에 오는 숫자로 지정합니다. 위 예제에서는 1입니다. 이 숫자 바로 뒤에 해당 키들을 차례대로 나열해야 하며, 스크립트 내에서는 KEYS 테이블로 접근할 수 있습니다. 이 예제에서 KEYS 테이블은 인덱스 1에 key:name이라는 값을 하나 담고 있습니다.
참고: Lua의 인덱스는 0이 아니라 1부터 시작합니다.
키 뒤에는 원하는 만큼의 인자를 추가할 수 있으며, 이들은 Lua에서 ARGV 테이블로 제공됩니다. 이 예제에서는 문자열 value라는 ARGV 인자 하나를 전달했습니다. 짐작하셨듯이, 위 명령어는 key:name 키에 value 값을 설정(set)합니다.
스크립트가 사용하는 키는 KEYS로 전달하고, 그 외의 모든 인자는 ARGV로 전달하는 것이 좋은 관행으로 여겨집니다. 따라서 KEYS 개수를 0으로 지정하고 모든 키를 ARGV 테이블에 담아 보내는 식의 사용은 피해야 합니다.
이제 스크립트가 성공적으로 실행되었는지 확인해 보겠습니다. Redis에서 해당 키를 조회하는 또 다른 스크립트를 실행합니다.
eval "return redis.call('get', KEYS[1])" 1 key:name출력 결과로 "value"가 표시된다면, 앞선 스크립트가 key:name 키를 성공적으로 설정했다는 의미입니다.
스크립트 상세 해설
우리의 첫 번째 스크립트는 단 하나의 문장, 즉 redis.call 함수 호출로 이루어져 있습니다.
redis.call('set', KEYS[1], ARGV[1])redis.call을 사용하면 모든 Redis 명령어를 실행할 수 있습니다. 첫 번째 인자는 명령어 이름이며, 그 뒤에 해당 명령어의 파라미터들이 옵니다. set 명령어의 경우 key와 value가 파라미터입니다. 공식 문서에 따르면:
Redis는 모든 명령어를 동일한 Lua 인터프리터로 실행합니다.
두 번째 스크립트는 단순히 명령어를 실행하는 것을 넘어 값을 반환(return)합니다.
eval "return redis.call('get', KEYS[1])" 1 key:name스크립트가 반환하는 모든 값은 호출한 프로세스로 전달됩니다. 이 경우 호출 주체는 redis-cli이므로, 결과가 터미널 창에 출력되는 것입니다.
좀 더 복잡한 예제
저는 한때 Lua 스크립트를 사용해 해시맵(hash map)의 요소들을 특정 순서대로 반환해야 하는 상황이 있었습니다. 그 순서 자체는 정렬된 셋(sorted set)에 저장된 해시 키들로 지정되었습니다.
먼저 redis-cli에서 다음 명령어들을 실행해 테스트 데이터를 준비합니다.
hmset hkeys key:1 value:1 key:2 value:2 key:3 value:3 key:4 value:4 key:5 value:5 key:6 value:6zadd order 1 key:3 2 key:1 3 key:2이 명령어들은 hkeys 키에 해시맵을 생성하고, order 키에 hkeys의 일부 키들을 특정 순서로 담은 정렬된 셋을 생성합니다.
자세한 내용은 hmset과 zadd 명령어 레퍼런스를 참고하세요.
이제 다음 스크립트를 실행해 보겠습니다.
eval "local order = redis.call('zrange', KEYS[1], 0, -1); return redis.call('hmget',KEYS[2],unpack(order));" 2 order hkeys다음과 같은 출력이 표시될 것입니다.
"value:3""value:1""value:2"즉, 원하는 키들의 값을 올바른 순서대로 가져오는 데 성공한 것입니다.
매번 전체 스크립트를 지정해야 하나요?
아니요! Redis는 SCRIPT LOAD 명령어로 스크립트를 미리 메모리에 적재할 수 있게 해줍니다.
script load "return redis.call('get', KEYS[1])"다음과 비슷한 출력이 표시됩니다.
"4e6d8fc8bb01276962cce5371fa795a7763657ae"이것은 스크립트의 고유 해시(SHA1)값입니다. 이후 EVALSHA 명령어에 이 해시를 전달하여 스크립트를 실행할 수 있습니다.
evalsha 4e6d8fc8bb01276962cce5371fa795a7763657ae 1 key:name참고: 반드시 SCRIPT LOAD 명령어가 반환한 실제 SHA1 해시를 사용해야 합니다. 위 해시는 예시일 뿐입니다.
JSON 변경 이야기
종종 사람들은 JSON 객체를 Redis에 저장합니다. 그것이 좋은 설계인지 아닌지는 별개의 문제지만, 실무에서는 실제로 매우 흔한 일입니다.
이 JSON 객체의 특정 키를 변경하려면, Redis에서 객체를 가져오고(get), 파싱하고, 키를 수정한 뒤, 다시 직렬화하여 Redis에 저장(set)해야 합니다. 이 접근 방식에는 몇 가지 문제가 있습니다.
- 동시성(Concurrency): get과 set 사이에 다른 프로세스가 해당 JSON을 변경할 수 있으며, 이 경우 그 변경 사항은 유실됩니다.
- 성능: 이러한 변경이 자주 발생하고 객체 크기가 크다면 애플리케이션의 병목 지점이 될 수 있습니다. 이 로직을 Lua로 구현하면 성능을 개선할 수 있습니다.
먼저 obj 키로 테스트용 JSON 문자열을 Redis에 추가합니다.
set obj '{"a":"foo","b":"bar"}'이제 스크립트를 실행합니다.
EVAL 'local obj = redis.call("get",KEYS[1]); local obj2 = string.gsub(obj,"(" .. ARGV[1] .. "\":)([^}]+)", "%1" .. ARGV[2]); return redis.call("set",KEYS[1],obj2);' 1 obj b bar2실행 후 obj 키에는 다음과 같은 객체가 저장되어 있을 것입니다.
{"a":"foo","b":"bar2"}물론 이 스크립트를 SCRIPT LOAD로 미리 적재한 뒤,
SCRIPT LOAD 'local obj = redis.call("get",KEYS[1]); local obj2 = string.gsub(obj,"(" .. ARGV[1] .. "\":)([^}]+)", "%1" .. ARGV[2]); return redis.call("set",KEYS[1],obj2);'다음과 같이 EVALSHA로 실행할 수도 있습니다.
EVALSHA <your_script_sha> 1 obj b bar2참고 사항:
..는 Lua의 문자열 연결(concatenation) 연산자입니다.- 키를 찾아 값을 교체하기 위해 정규표현식(RegEx) 패턴을 사용합니다. 정규표현식이 익숙하지 않다면 별도의 가이드를 먼저 학습하는 것을 추천합니다.
- Lua의 정규표현식 방언이 다른 대부분의 방언과 다른 점 중 하나는, 백레퍼런스(backreference) 표시와 정규표현식 특수문자 이스케이프에 모두
%를 사용한다는 점입니다. - 반면
"는%가 아닌\로 이스케이프하는데, 이는 정규표현식 특수문자가 아니라 Lua 문자열 구분자를 이스케이프하는 것이기 때문입니다.
항상 Lua 스크립트를 사용해야 할까?
아니요. 더 나은 성능으로 이어진다는 것을 입증할 수 있는 경우에만 사용할 것을 권장합니다. 항상 먼저 벤치마크를 실행하세요.
단순히 원자성(atomicity)만 필요하다면, Redis 트랜잭션(MULTI/EXEC)을 검토하는 편이 좋습니다.
또한 스크립트가 너무 길어서는 안 됩니다. 스크립트가 실행되는 동안 다른 모든 요청은 그 종료를 기다려야 한다는 점을 기억하세요. 스크립트 실행 시간이 길어지면 성능을 개선하는 대신 오히려 병목을 유발할 수 있습니다. 스크립트는 타임아웃(기본값 5초)에 도달하면 강제로 중단됩니다.
마무리
Lua에 대해 더 알고 싶다면 lua.org를 참고하세요.
또한 GitHub에서 제 node.js 라이브러리(src/lua 폴더 참조)를 확인하면 Lua 스크립트의 다양한 예제를 볼 수 있습니다. 이 라이브러리를 node.js에서 사용하면 직접 Lua 스크립트를 작성하지 않고도 JSON 객체를 손쉽게 수정할 수 있습니다.
글을 읽어주셔서 감사합니다. 질문과 댓글은 언제나 환영합니다.
freeCodeCamp의 오픈소스 커리큘럼은 40,000명 이상의 사람들이 개발자로 취업하는 데 도움을 주었습니다. 무료로 코딩을 배워보세요.