본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

강의 요약

 

오늘은 Redis 트랜잭션과 관련된 내용에 대해 학습했다. 어제까지 다룬 데이터 타입들이 "무엇을 저장할 것인가"에 대한 내용이었다면, 오늘은 "어떻게 안전하게 연산할 것인가"에 초점을 맞춘 내용이다.

 

Redis 트랜잭션 기본 구조

Redis 트랜잭션은 MULTI로 시작해서 명령들을 큐에 쌓고, EXEC로 일괄 실행하는 방식이다. 명령들이 순차적으로 실행되며 중간에 다른 클라이언트의 요청이 끼어들지 않는다. 단, 실행 단계에서 일부 명령이 실패해도 나머지는 계속 실행된다는 점이 특징이다. 즉 롤백을 제공하지 않는다.

 

 

WATCH: 낙관적 락

WATCH는 트랜잭션 실행 전 특정 키를 감시하여, 해당 키가 EXEC 전에 변경되면 트랜잭션이 실패하고 null을 반환한다. 선착순 처리나 잔고 차감처럼 동시성 충돌이 우려되는 상황에서 활용할 수 있다.

 

Lua 스크립트

Lua는 Redis 서버 내부에서 실행되는 스크립트 언어로, EVAL 명령으로 실행한다. 스크립트 전체가 원자적으로 실행되며, 조건 분기와 루프가 가능하다. 다만 긴 실행 시간은 Redis 전체를 블로킹하므로 주의가 필요하다.

 

Redis 트랜잭션의 두 가지 보장

Redis 공식 문서에서는 트랜잭션이 보장하는 것을 명확히 두 가지로 정의한다.

 

1. 격리된 실행 (Isolation)

트랜잭션 내의 모든 명령은 직렬화되어 순차적으로 실행된다. 다른 클라이언트의 요청이 실행 중간에 끼어드는 일은 없다.

 

2. 전부 아니면 전무 (All or Nothing) - 단, EXEC 호출 기준

상황 결과
EXEC 호출 전 연결 끊김 아무 작업도 수행되지 않음
EXEC 호출됨 모든 작업 수행

 

 

AOF 사용 시 주의점

  • AOF(Append-Only File) 모드에서 Redis는 트랜잭션을 디스크에 기록할 때 단일 write syscall을 사용한다.
  • 하지만 서버 크래시나 강제 종료 시 일부 작업만 기록될 수 있다.
  • Redis는 재시작 시 이를 감지하고 에러와 함께 종료되며, redis-check-aof 도구로 부분 트랜잭션을 제거하여 복구할 수 있다.

 

Redis 트랜잭션과 RDBMS 트랜잭션 차이

항목 Redis RDMS
명령 격리 O (실행 중 다른 요청 차단) O
실행 후 롤백 X O
일부 명령 실패 시 나머지 계속 실행 전체 롤백
락 방식 낙관적 (WATCH) 비관적/낙관적 선택 가능

 

에러 발생 시점에 따른 동작 차이

에러 시점 예시 동작
EXEC 전 (큐잉 단계) 문법 오류, 잘못된 인자 개수 트랜잭션 전체 실행 거부
EXEC 후 (실행 단계) 타입 불일치 (String에 LPOP 등) 해당 명령만 실패, 나머지 실행
  • 큐잉 단계에서 문법 오류가 감지되면 Redis는 EXEC 시점에 트랜잭션 전체를 거부한다.
  • 이는 "롤백"이 아니라 애초에 실행 자체가 되지 않는 것이다. 반면 실행 단계에서 발생하는 타입 오류 같은 런타임 에러는 해당 명령만 실패하고 나머지는 정상 실행된다.

버전 참고: Redis 2.6.5 이전에는 큐잉 중 에러가 발생해도 클라이언트가 직접 감지해야 했고, EXEC를 호출하면 성공한 명령들만 실행되었다.

 

 

롤백을 지원하지 않는 이유

 

Redis 공식 문서에서는 롤백 미지원 이유를 명확히 밝힌다. 롤백 메커니즘을 구현하면 Redis의 단순성과 성능에 상당한 영향을 미치기 때문이다. 또한 실행 단계 에러는 대부분 프로그래밍 실수(타입 불일치 등)이므로, 프로덕션 환경에서는 발생하지 않아야 한다는 관점이다.

 

WATCH 상세 동작

WATCH는 CAS(Check-And-Set) 패턴을 제공하여 낙관적 락을 구현한다.

 

변경 감지 범위

  • WATCH는 클라이언트의 쓰기 명령뿐 아니라 Redis 자체의 변경도 감지한다.
감지 대상 설명
클라이언트 쓰기 SET, INCR 등 다른 클라이언트의 명령
키 만료 (Expiration) TTL 만료로 인한 키 삭제
키 퇴거 (Eviction) maxmemory 정책에 의한 키 삭제

버전 참고: Redis 6.0.9 이전에는 키 만료가 트랜잭션을 중단시키지 않았다.

 

주의사항

트랜잭션 내부의 명령은 WATCH 조건을 트리거하지 않는다. EXEC가 호출되기 전까지 명령들은 큐에만 저장되어 있기 때문이다.

 

UNWATCH 활용

WATCH로 키를 감시했지만 조건 확인 후 트랜잭션을 진행하지 않기로 결정한 경우, UNWATCH를 호출하여 감시를 해제할 수 있다.

WATCH mykey
val = GET mykey
// 조건 확인 후 진행하지 않기로 결정
UNWATCH
// 연결을 새 트랜잭션에 자유롭게 사용 가능

 

WATCH 실전 예제: ZPOP 구현

Redis 5.0 이전에는 ZPOPMIN/ZPOPMAX가 없었다. WATCH로 원자적 ZPOP을 구현하는 패턴이다.

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC
// EXEC 실패(null 반환) 시 처음부터 재시도

 

 

Lua 스크립트

트랜잭션만으로 해결하기 어려운 경우가 있다. "현재 값을 읽고, 조건을 판단한 후, 조건에 맞으면 수정"하는 로직은 MULTI/EXEC만으로 구현하기 까다롭다. 큐에 쌓인 명령들은 조건 분기가 불가능하기 때문이다.

 

트랜잭션 vs Lua 스크립트

항목 MULTI/EXEC  Lua 스크립트
원자성 O O
조건 분기 X O
루프 X O
중간 결과 활용 X O
네트워크 왕복 명령 수만큼 1회
  • Redis 공식 문서에서도 트랜잭션으로 할 수 있는 모든 것을 스크립트로도 할 수 있으며, 일반적으로 스크립트가 더 단순하고 빠르다고 설명한다.

 

Lua 스크립트 기본 구조

EVAL "스크립트" 키개수 키... 인자...

 

함수/변수 설명
redis.call() Redis 명령 실행, 오류 시 스크립트 중단
redis.pcall() 오류 발생해도 nil 반환, 예외 방지 가능
KEYS 외부에서 전달받은 키 배열
ARGV 외부에서 전달받은 인자 배열

 

주의사항

  • 스크립트는 강력하지만 리소스 소비와 유지보수 측면에서 고려할 점이 많다.
  • 성능 한계
    • 긴 실행 시간 → Redis 전체 블로킹 (단일 스레드)
    • lua-time-limit 기본 5초
  • 디버깅
    • 에러 메시지 단순, 로그 출력 불가
    • 멀티라인 스크립트는 외부 파일 분리 권장
  • 보안
    • 외부 입력값 injection 주의
    • MONITOR같은 일부 명령은 Lau 내부에서 지원 하지 않는다.

 

참고 출처

 

 

 

시작 시간
종료 시간
학습 인증
수강 인증

 

 

https://fastcampus.info/4oKQD6b

+ Recent posts