본문 바로가기
프로그래밍 언어 실전 가이드

루아 입문 시리즈 #11: Redis와 루아 스크립팅

by devcomet 2025. 6. 28.
728x90
반응형

Redis Lua 스크립팅 완벽 가이드 - 원자적 연산과 고성능 캐시 전략
Redis Lua 스크립팅 완벽 가이드 - 원자적 연산과 고성능 캐시 전략

 

Redis Lua 스크립트를 활용한 원자적 연산과 고성능 캐시 전략으로 데이터베이스 부하를 줄이고 애플리케이션 성능을 극대화하는 실전 가이드입니다.


Redis Lua 스크립팅이란?

Redis는 인메모리 데이터 구조 저장소로 널리 알려져 있지만, Lua 스크립팅 기능을 제공하여 복잡한 원자적 연산을 수행할 수 있습니다.

Redis Lua 스크립트는 서버 측에서 실행되는 스크립트로, 여러 Redis 명령어를 하나의 원자적 단위로 묶어 실행할 수 있게 해줍니다.

이를 통해 네트워크 왕복 시간을 줄이고, 데이터 일관성을 보장하며, 복잡한 비즈니스 로직을 Redis 내부에서 처리할 수 있습니다.

 

Redis Lua 스크립팅 아키텍처 구조도 - 클라이언트와 서버 간 원자적 연산 처리 과정
Redis Lua 스크립팅 아키텍처 다이어그램


Redis Lua 스크립팅의 핵심 장점

원자적 연산 보장

Redis Lua 스크립트의 가장 중요한 특징 중 하나는 스크립트 전체가 원자적으로 실행된다는 점입니다.

스크립트가 실행되는 동안 다른 클라이언트의 명령어는 대기하게 되므로, 데이터 경합 상태(race condition)를 완전히 방지할 수 있습니다.

-- 원자적 카운터 증가 및 만료 시간 설정
local current = redis.call('GET', KEYS[1])
if current == false then
    redis.call('SET', KEYS[1], 1)
    redis.call('EXPIRE', KEYS[1], ARGV[1])
    return 1
else
    return redis.call('INCR', KEYS[1])
end

네트워크 오버헤드 감소

여러 Redis 명령어를 개별적으로 전송하는 대신 하나의 스크립트로 묶어서 실행하면 네트워크 왕복 시간을 크게 줄일 수 있습니다.

특히 마이크로서비스 아키텍처에서 Redis가 원격 서버에 위치한 경우 이러한 성능 향상은 더욱 중요해집니다.


Redis Lua 스크립트 기본 문법

EVAL 명령어 사용법

Redis에서 Lua 스크립트를 실행하는 기본 명령어는 EVAL입니다.

EVAL script numkeys key [key ...] arg [arg ...]
  • script: 실행할 Lua 스크립트
  • numkeys: 키의 개수
  • key: Redis 키들
  • arg: 스크립트에 전달할 인수들

KEYS와 ARGV 배열

Lua 스크립트 내에서는 KEYS 배열과 ARGV 배열을 통해 전달된 매개변수에 접근할 수 있습니다.

-- KEYS[1]: 첫 번째 키
-- ARGV[1]: 첫 번째 인수
local value = redis.call('GET', KEYS[1])
if value == false then
    redis.call('SET', KEYS[1], ARGV[1])
    return "새로운 값 설정"
else
    return "기존 값: " .. value
end

 

Redis EVAL 명령어 구조도 - KEYS와 ARGV 배열을 통한 매개변수 전달 방식
EVAL 명령어 구조와 매개변수 전달 방식 다이어그램


실전 캐시 전략 구현

분산 락(Distributed Lock) 구현

Redis Lua 스크립트를 활용하면 안전한 분산 락을 구현할 수 있습니다.

-- 분산 락 획득 스크립트
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

이 스크립트는 락을 획득한 클라이언트만이 락을 해제할 수 있도록 보장합니다.

Rate Limiting 구현

API 호출 제한을 구현하는 것도 Redis Lua 스크립트의 대표적인 활용 사례입니다.

-- 슬라이딩 윈도우 기반 Rate Limiting
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])

-- 만료된 항목 제거
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)

-- 현재 요청 수 확인
local current_requests = redis.call('ZCARD', key)

if current_requests < limit then
    -- 새 요청 추가
    redis.call('ZADD', key, current_time, current_time)
    redis.call('EXPIRE', key, math.ceil(window / 1000))
    return {1, limit - current_requests - 1}
else
    return {0, 0}
end

캐시 워밍(Cache Warming) 전략

대규모 서비스에서는 캐시 무효화 시 대량의 데이터베이스 요청이 발생할 수 있습니다.

Lua 스크립트를 사용하여 지능적인 캐시 워밍 전략을 구현할 수 있습니다.

-- 점진적 캐시 무효화 스크립트
local pattern = KEYS[1]
local batch_size = tonumber(ARGV[1])
local cursor = ARGV[2] or "0"

local result = redis.call('SCAN', cursor, 'MATCH', pattern, 'COUNT', batch_size)
local next_cursor = result[1]
local keys = result[2]

if #keys > 0 then
    redis.call('DEL', unpack(keys))
end

return {next_cursor, #keys}

 

전통적 캐시 전략 vs Redis Lua 스크립트 캐시 전략 성능 비교 차트
캐시 전략 비교 차트 (Traditional vs Lua Script)


성능 최적화 기법

스크립트 캐싱과 EVALSHA

자주 사용하는 스크립트는 SCRIPT LOAD 명령어로 미리 로드하고 EVALSHA로 실행하면 네트워크 대역폭을 절약할 수 있습니다.

# 스크립트 로드
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 반환된 SHA1 해시로 실행
EVALSHA 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 1 mykey

메모리 효율적인 스크립트 작성

Lua 스크립트에서는 불필요한 변수 생성을 피하고, 가능한 한 Redis 명령어를 직접 사용하는 것이 좋습니다.

-- 비효율적인 방법
local values = {}
for i = 1, #KEYS do
    values[i] = redis.call('GET', KEYS[i])
end
return values

-- 효율적인 방법
return redis.call('MGET', unpack(KEYS))

Redis Lua vs 다른 솔루션 비교

특징 Redis Lua Redis Modules 애플리케이션 로직
원자성 보장
네트워크 오버헤드 낮음 낮음 높음
개발 복잡도 중간 높음 낮음
성능 우수 매우 우수 보통
유연성 높음 매우 높음 매우 높음
배포 복잡도 낮음 높음 낮음

실무에서의 주의사항

스크립트 실행 시간 제한

Redis Lua 스크립트는 단일 스레드에서 실행되므로 오래 실행되는 스크립트는 전체 Redis 서버를 블록할 수 있습니다.

lua-time-limit 설정을 통해 실행 시간을 제한하고, 복잡한 연산은 여러 단계로 나누어 처리하는 것이 좋습니다.

키 명명 규칙과 네임스페이스

대규모 애플리케이션에서는 일관된 키 명명 규칙을 사용하여 충돌을 방지해야 합니다.

-- 좋은 예: 네임스페이스와 버전 포함
local key = "myapp:v1:user:" .. ARGV[1] .. ":session"

에러 처리와 로깅

Lua 스크립트 내에서 발생하는 에러는 클라이언트로 전파되므로 적절한 에러 처리가 중요합니다.

local ok, result = pcall(redis.call, 'GET', KEYS[1])
if not ok then
    return {err = "키 조회 실패: " .. result}
end
return result

고급 활용 사례

실시간 순위 시스템

Redis의 Sorted Set과 Lua 스크립트를 결합하면 효율적인 실시간 순위 시스템을 구현할 수 있습니다.

-- 점수 업데이트 및 순위 조회 스크립트
local leaderboard = KEYS[1]
local user_id = ARGV[1]
local score = tonumber(ARGV[2])

-- 점수 업데이트
redis.call('ZADD', leaderboard, score, user_id)

-- 사용자 순위 조회
local rank = redis.call('ZREVRANK', leaderboard, user_id)

-- 상위 10명 조회
local top_users = redis.call('ZREVRANGE', leaderboard, 0, 9, 'WITHSCORES')

return {
    user_rank = rank and (rank + 1) or nil,
    top_users = top_users
}

복합 캐시 무효화

여러 캐시 키가 서로 연관되어 있을 때, 하나의 변경사항이 여러 캐시에 영향을 줄 수 있습니다.

-- 연관 캐시 일괄 무효화 스크립트
local user_id = ARGV[1]
local patterns = {
    "user:" .. user_id .. ":*",
    "feed:" .. user_id,
    "recommendations:" .. user_id
}

local deleted_count = 0
for _, pattern in ipairs(patterns) do
    local keys = redis.call('KEYS', pattern)
    if #keys > 0 then
        deleted_count = deleted_count + redis.call('DEL', unpack(keys))
    end
end

return deleted_count

모니터링과 디버깅

스크립트 성능 모니터링

Redis의 SLOWLOG 명령어를 통해 느린 스크립트를 식별할 수 있습니다.

또한 redis-cli --latency 명령어로 전반적인 성능을 모니터링할 수 있습니다.

디버깅 기법

복잡한 Lua 스크립트를 디버깅할 때는 다음과 같은 기법을 활용할 수 있습니다:

-- 디버그 정보 포함 스크립트
local debug_info = {}

local step1_result = redis.call('GET', KEYS[1])
table.insert(debug_info, "Step 1: " .. tostring(step1_result))

-- 최종 결과와 함께 디버그 정보 반환
return {
    result = final_result,
    debug = debug_info
}

마이그레이션과 버전 관리

스크립트 버전 관리

프로덕션 환경에서는 스크립트 변경 시 점진적 배포가 중요합니다.

-- 버전 호환성 체크 포함 스크립트
local script_version = tonumber(ARGV[1]) or 1

if script_version >= 2 then
    -- 새로운 로직
    return enhanced_logic()
else
    -- 기존 로직 유지
    return legacy_logic()
end

무중단 업데이트 전략

스크립트 업데이트 시 서비스 중단을 방지하기 위해 블루-그린 배포 방식을 적용할 수 있습니다.

새로운 스크립트를 다른 SHA1 해시로 등록하고, 점진적으로 트래픽을 전환하는 방식입니다.


결론

Redis Lua 스크립팅은 현대적인 애플리케이션에서 고성능 캐시 전략을 구현하는 핵심 기술입니다.

원자적 연산 보장, 네트워크 오버헤드 감소, 복잡한 비즈니스 로직의 서버 측 실행 등 다양한 장점을 제공합니다.

특히 마이크로서비스 아키텍처에서 데이터 일관성과 성능을 동시에 확보해야 하는 상황에서 Redis Lua 스크립트는 필수적인 도구가 되었습니다.

앞서 살펴본 실전 예제들을 통해 여러분만의 고성능 캐시 전략을 구현해보시기 바랍니다.

다음 시리즈에서는 Lua와 다른 기술 스택의 통합에 대해 더 자세히 알아보겠습니다.


참고 자료

728x90
반응형