Redis의 트랜잭션과 원자적 처리 방식
Redis는 기본적으로 단일 스레드 기반으로 동작하며, 이를 활용한 단순한 트랜잭션 기능과 고급 제어를 위한 Lua 스크립트 기능을 제공합니다. 이 글에서는 Redis의 트랜잭션 개념과 그 한계, 그리고 Lua 스크립트를 통한 원자적 처리 방식에 대해 설명합니다.
1. Redis 트랜잭션 (MULTI / EXEC)
Redis의 트랜잭션은 RDBMS의 ACID 트랜잭션과는 다르게, 명령어를 일괄적으로 순차 실행하는 방식입니다.
사용 예시
MULTI
SET key1 "value1"
SET key2 "value2"
EXEC
- MULTI로 트랜잭션을 시작하고,
- 여러 명령을 큐에 쌓은 뒤,
- EXEC 명령을 통해 한 번에 실행합니다.
특징
- 트랜잭션 내부의 명령어는 순차적으로 실행되며, 실행 중간에 다른 클라이언트의 명령이 끼어들 수 없습니다.
- 하지만 중간에 명령어가 실패해도 rollback은 되지 않습니다.
- 모든 명령어는 EXEC 시점에 실행되므로, 실행 전에는 단순히 큐에 쌓여있는 상태입니다.
에러 처리 방식 및 에러 유형
Redis 트랜잭션은 명령어 실행 중 에러가 발생해도 롤백되지 않으며, 다음과 같은 방식으로 에러를 처리합니다.
1. 큐잉(Queuing) 단계 에러
- MULTI 이후 명령어를 큐에 쌓을 때 문법 오류 등이 발생하면, 해당 명령어는 큐에 등록되지 않고 즉시 에러를 반환합니다.
- 이 경우 전체 트랜잭션이 무효화되며, EXEC 시 모든 명령어가 실행되지 않습니다.
MULTI
SET key1 "value1"
BADCOMMAND # 존재하지 않는 명령어 → 큐잉 실패
SET key2 "value2"
EXEC # → null reply (트랜잭션 전체 무효화)
2. 실행(EXEC) 단계 에러
- EXEC을 호출하면 큐에 있는 명령어들을 순차적으로 실행합니다.
- 이 중 하나라도 런타임 에러가 발생해도 나머지 명령어는 그대로 실행됩니다.
- Redis는 각 명령어 결과로 에러인지 아닌지만 클라이언트에 반환합니다.
SET someKey "not-a-number"
MULTI
INCR someKey # 런타임 에러 (not an integer)
SET anotherKey "ok"
EXEC
→ 결과
1) (error) ERR value is not an integer or out of range
2) OK
주요 실행 단계 에러 예시
명령어 에러 상황
INCR / DECR | 값이 숫자가 아님 |
LPUSH, RPUSH | 키가 리스트가 아닌 다른 타입 |
SADD, HSET | 키가 집합/해시가 아닌 다른 타입 |
GET | 키가 존재하지 않지만 에러는 발생하지 않음 |
사용자 정의 명령어 | 스크립트 또는 모듈이 내부 에러를 던졌을 때 |
⚠️ 이와 같은 실행 단계 에러는 트랜잭션을 멈추지 않으며, 개발자가 직접 결과를 검사하고 후속 처리해야 합니다.
롤백 미지원
Redis 트랜잭션은 RDBMS와 달리 중간에 오류가 발생해도 이전에 실행된 명령어를 되돌리는 기능(rollback)을 제공하지 않습니다.
- EXEC 이후에 실행된 명령어 중 일부가 실패하더라도, 성공한 명령어는 그대로 반영됩니다.
- 전체 트랜잭션을 무효화할 수 있는 유일한 방법은 큐잉 단계에서 에러가 발생하거나 WATCH 충돌로 인해 EXEC 자체가 실행되지 않는 경우뿐입니다.
- 이러한 특성으로 인해, Redis 트랜잭션은 "원자적이지 않은 일괄 처리"**에 가깝습니다.
💡 복잡한 원자성 보장이나 예외 처리가 필요한 경우, Lua 스크립트를 사용하는 것이 바람직합니다.
2. 트랜잭션과 WATCH를 이용한 낙관적 락
WATCH 명령어를 사용하면 트랜잭션 실행 전 지정한 키의 변경을 감시할 수 있습니다. 해당 키가 다른 클라이언트에 의해 변경되면, EXEC은 실패하고 트랜잭션은 실행되지 않습니다.
WATCH balance
MULTI
DECR balance
EXEC
- 트랜잭션 실행 전 balance 키를 감시하고,
- 누군가 이 키를 변경하면 EXEC은 실패합니다.
이 방식은 낙관적 락(Optimistic Locking)을 구현할 때 사용됩니다.
3. Redis 트랜잭션의 한계
- Redis는 트랜잭션 내 명령어 수준에서 오류가 발생해도 나머지 명령어를 계속 실행합니다.
- 롤백 기능이 없기 때문에, 트랜잭션 전체의 일관성을 보장하지 못합니다.
- 복잡한 조건 분기나 예외 처리는 불가능합니다.
이러한 한계를 극복하기 위해 Redis는 Lua 스크립트를 통한 원자적 실행을 지원합니다.
4. Lua 스크립트를 통한 원자적 실행
Redis는 EVAL 명령어를 통해 Lua 스크립트를 실행할 수 있으며, 해당 스크립트는 Redis 서버 내에서 원자적으로 실행됩니다. 이를 통해 복잡한 조건 분기, 키 존재 여부 확인, 롤백 제어 등이 가능합니다.
예시: 키가 없을 때만 값을 설정
-- Lua 스크립트
if redis.call("EXISTS", KEYS[1]) == 1 then
return redis.call("GET", KEYS[1])
else
redis.call("SET", KEYS[1], ARGV[1])
return ARGV[1]
end
EVAL "<스크립트>" 1 someKey someValue
Lua의 장점
- 완전한 원자성 보장
- 조건문, 반복문 등 로직 제어 가능
- 여러 명령어를 하나의 트랜잭션처럼 묶어서 실행 가능
- 롤백과 유사한 제어 로직 작성 가능
- 예외 처리 코드에 원 상태로 복구하게 도와주는 명령어 삽입함으로써 구현
5. 비교 요약
기능 | MULTI/EXEC 트랜잭션 | Lua 스크립트 |
원자성 보장 | X | O |
롤백 지원 | X | 제어 가능 |
조건 분기 | X | O |
복잡한 로직 처리 | X | O |
실무 사용 추천 | 간단한 처리 | 복잡한 트랜잭션 대체 |
'DB > Redis' 카테고리의 다른 글
[Redis][실전레디스] 레디스 운용관리 - 스냅샷 심화 (0) | 2025.03.29 |
---|---|
[Redis][실전레디스] 레디스 운용관리 - 스냅샷 & AOF (0) | 2025.03.29 |