[TIL] Redis 특강 복습: 캐시, 분산 락, 비동기 처리까지 이해하기
Redis 인메모리 아키텍처, 캐싱 전략, 분산 락 동시성 제어, Kafka 비동기 이벤트 처리 구조 및 핵심 개념
For the English version of this post, see here.
오늘 복습한 내용
Redis와 Memcached의 차이
캐싱의 의미
Batch와 Worker의 역할
커넥션 풀 고갈 문제
Redis 분산 락과 Redisson
Kafka를 이용한 비동기 처리
선착순 쿠폰 발급 예시에서 Redis와 Kafka 비교
Redis Pub/Sub의 유실 가능성과 Redis List 활용
Redis Master/Slave 구조와 Redlock 알고리즘의 한계
Redis와 Memcached
Redis와 Memcached는 둘 다 메모리 기반의 저장소이며, 자주 사용하는 데이터를 빠르게 조회하기 위해 캐시로 많이 사용된다.
Memcached는 단순한 key-value 캐시에 특화되어 있다. 구조가 단순하고 빠르지만, 문자열 중심의 저장 방식이며 영속성이나 복잡한 자료구조 지원은 거의 없다.
반면 Redis는 key-value 저장소이면서도 다양한 자료구조를 지원한다. String, List, Set, Hash, Sorted Set 같은 자료구조를 사용할 수 있고, 이를 활용해 캐시뿐만 아니라 분산 락, 랭킹, 큐, 세션 저장소 등으로도 사용할 수 있다.
→ Memcached는 단순 캐시에 적합하고, Redis는 캐시를 포함해 더 다양한 기능이 필요한 상황에 적합하다.
캐싱이란?
캐싱이란 한 번 조회한 데이터를 임시 저장소에 저장해두고, 다음에 같은 데이터가 필요할 때 빠르게 꺼내 쓰는 것을 말한다.
- 예시: 상품 상세 정보 조회
상품 상세 정보를 조회할 때 매번 DB에 접근하면 DB 부하가 커질 수 있다. 이때 상품 정보를 Redis나 Memcached에 저장해두면, 다음 요청부터는 DB가 아니라 캐시에서 데이터를 바로 가져올 수 있다.
캐싱을 사용하면 응답 속도가 빨라지고 DB 부하를 줄일 수 있다. 다만 캐시는 원본 데이터 저장소가 아니므로, 사라져도 다시 만들 수 있는 데이터를 저장하는 데 적합하다.
Batch와 Worker
Batch는 작업을 실시간으로 하나씩 처리하는 것이 아니라, 일정 시간이나 조건에 따라 모아서 한 번에 처리하는 방식이다.
예를 들어 매일 밤 12시에 주문 정산을 하거나, 매일 오전에 통계 데이터를 생성하는 작업은 배치 처리에 해당한다.
Worker는 큐나 메시지 브로커에 쌓인 작업을 뒤에서 꺼내 실제로 처리하는 백그라운드 작업자이다.
예시: 선착순 쿠폰 발급
API 서버가 사용자의 요청을 바로 DB에 저장하지 않고 Redis List나 Kafka에 쌓아둔다. 이후 Worker가 그 요청을 하나씩 꺼내 실제 쿠폰 발급 처리를 수행한다.1 2 3 4
API 서버 = 요청 접수 담당 Redis List / Kafka = 작업 대기열 Worker = 실제 처리 담당 DB = 최종 저장소
→ 이렇게 역할을 분리하면 API 서버는 빠르게 요청을 받고, 실제 무거운 작업은 Worker가 안정적으로 처리할 수 있다.
커넥션 풀 고갈
커넥션 풀 고갈은 애플리케이션이 DB와 연결하기 위해 미리 만들어둔 커넥션들이 모두 사용 중인 상태를 말한다.
요청은 DB 작업을 수행하기 위해 커넥션 풀에서 커넥션을 빌리고, 작업이 끝나면 다시 반납한다. 하지만 쿼리가 오래 걸리거나, 트랜잭션이 길게 유지되거나, 락 대기가 발생하면 커넥션 반납이 늦어진다.
그 결과 새로운 요청은 사용할 커넥션을 얻지 못해 대기하거나 timeout이 발생할 수 있다.
- 예시: 선착순 쿠폰 발급
짧은 시간에 많은 요청이 몰리는 경우, 모든 요청이 DB로 바로 접근하면 커넥션 풀 고갈 문제가 발생할 수 있다. 그래서 Redis나 Kafka를 사용해 DB 앞에서 요청을 조절하는 구조를 사용할 수 있다.
Redis 분산 락과 Redisson
분산 락은 여러 서버가 동시에 같은 자원에 접근하지 못하도록 제어하는 방식이다.
서버가 한 대라면 Java의 synchronized 같은 방식으로 동시 접근을 막을 수 있지만, 서버가 여러 대라면 각 서버의 메모리는 서로 공유되지 않는다. 따라서 여러 서버가 공통으로 접근할 수 있는 Redis를 이용해 락을 관리할 수 있다.
Java/Spring 환경에서는 Redis 분산 락을 직접 구현하기보다 Redisson을 많이 사용한다. Redisson은 Redis 기반의 Java 클라이언트 라이브러리로, RLock을 통해 분산 락을 객체처럼 사용할 수 있게 해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RLock lock = redissonClient.getLock("stock:" + productId);
boolean available = false;
try {
available = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!available) {
throw new RuntimeException("잠시 후 다시 시도해주세요.");
}
decreaseStock(productId);
} finally {
if (available && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
Redisson을 사용하면 락 획득, 락 해제, 만료 시간 설정 등을 직접 구현하는 부담을 줄일 수 있다. 또한 lock watchdog 기능을 통해 락을 잡은 스레드가 살아있는 동안 락 만료 시간을 자동으로 연장할 수 있다.
동기 방식과 비동기 방식
동기 방식은 요청을 보낸 뒤 결과가 올 때까지 기다리는 방식이다.
예시
주문 생성 시 order-service가 delivery-service를 직접 호출한다면, order-service는 delivery-service의 응답을 기다려야 한다.이 경우 delivery-service에 장애가 발생하면 주문 생성도 함께 실패할 수 있다. 즉, 서비스 간 강결합 문제가 생긴다.
반면 비동기 방식은 요청을 보낸 뒤 상대 서비스의 처리를 기다리지 않고, 이벤트나 메시지를 남긴 뒤 자신의 작업을 먼저 끝내는 방식이다.
예시
Order Service는 Kafka에 주문 생성 이벤트를 발행하고, Delivery Service는 해당 이벤트를 구독해 배송을 생성한다. 이렇게 하면 배송 서비스 장애가 주문 생성 흐름에 직접 영향을 주지 않도록 만들 수 있다.1 2
동기 = 지금 바로 결과가 필요할 때 비동기 = 나중에 처리돼도 괜찮을 때
다만 비동기 방식이 항상 더 좋은 것은 아니다. 로그인, 권한 확인, 결제 승인처럼 사용자가 즉시 성공/실패 결과를 알아야 하는 기능은 동기 방식이 적합하다. 반면 알림 발송, 통계 집계, 배송 생성처럼 약간의 지연을 허용할 수 있는 기능은 비동기 방식이 적합하다.
Kafka란?
Kafka는 대용량 이벤트 스트리밍 플랫폼이다. Producer가 Topic에 메시지를 발행하면 Consumer가 이를 구독해 처리하는 구조이다.
- Producer → Kafka Topic → Consumer
Kafka는 메시지를 디스크 기반 로그로 저장하고, Consumer는 offset을 기준으로 어디까지 읽었는지 관리한다. 따라서 Consumer가 잠시 장애가 나더라도 복구 후 다시 메시지를 읽어 처리할 수 있다.
MSA 환경에서는 Kafka를 사용해 서비스 간 직접 의존성을 줄이고, 장애 전파를 완화하며, 비동기 이벤트 기반 처리를 구현할 수 있다.
선착순 쿠폰 발급 예시
선착순 쿠폰 발급을 동기 방식으로 처리하면 사용자의 요청이 들어올 때마다 DB에서 쿠폰 수량을 조회하고, 발급 내역을 저장하고, 쿠폰 수량을 차감해야 한다.
하지만 수십만 명이 동시에 요청하면 DB에 부하가 몰리고, 락 대기와 커넥션 풀 고갈 문제가 발생할 수 있다. 또한 동시에 여러 요청이 같은 쿠폰 수량을 보고 발급을 시도하면 초과 발급 문제가 생길 수 있다.
이를 개선하기 위해 비동기 방식으로 전환할 수 있다.
1
2
3
4
5
사용자 요청
→ Redis 또는 Kafka에 발급 요청 저장
→ 사용자에게 접수 완료 응답
→ Worker가 요청을 순서대로 처리
→ DB에 최종 발급 내역 저장
Redis는 빠른 원자 연산을 통해 선착순 수량 제한에 적합하다. 예를 들어 Redis의 DECR 명령을 사용하면 쿠폰 수량을 빠르게 감소시켜 100명까지만 통과시키는 구조를 만들 수 있다.
Kafka는 대량의 발급 요청 이벤트를 안정적으로 저장하고, Consumer가 순서대로 처리하며, 장애 발생 시 재처리할 수 있다는 장점이 있다.
→ 따라서 선착순 쿠폰 발급에서는 Redis와 Kafka를 함께 사용할 수도 있다.
1
2
3
Redis = 빠른 선착순 컷
Kafka = 발급 요청 이벤트를 안정적으로 저장하고 비동기 처리
DB = 최종 발급 결과 저장
Redis Pub/Sub과 Redis List
Redis Pub/Sub은 메시지를 발행하면 구독 중인 Subscriber에게 전달하는 구조이다. 하지만 메시지를 저장하지 않기 때문에 Subscriber가 그 순간 꺼져 있거나 장애가 나 있으면 메시지를 받을 수 없다.
따라서 중요한 쿠폰 발급 요청 데이터를 Redis Pub/Sub으로만 전달하면 유실 위험이 있다.
1
2
3
4
5
쿠폰 발급 요청
→ Pub/Sub으로 발행
→ Worker 장애
→ 메시지 유실
→ 요청 데이터 유실
하지만 Pub/Sub을 알림용 트리거로만 사용하고, 실제 요청 데이터는 Redis List에 저장하면 이야기가 달라진다.
1
2
3
4
쿠폰 발급 요청
→ Redis List에 RPUSH로 요청 데이터 저장
→ Pub/Sub으로 Worker에게 알림
→ Worker가 Redis List에서 LPOP으로 데이터 꺼내 처리
이 구조에서는 중요한 데이터가 Pub/Sub에 있는 것이 아니라 Redis List에 저장되어 있다. 따라서 Pub/Sub 알림이 유실되어도 요청 데이터 자체는 List에 남아 있다.
1
2
3
Redis List = 대기표 보관함
Redis Pub/Sub = 알림벨
Worker = 대기표를 보고 처리하는 작업자
즉, Redis Pub/Sub은 중요한 데이터를 안전하게 전달하는 용도가 아니라, 처리할 데이터가 생겼음을 알려주는 가벼운 트리거로 사용하는 것이 적절하다.
Redis Master/Slave 구조와 Redlock
Redis에서 Master/Slave 구조는 하나의 Master가 쓰기 작업을 담당하고, Slave가 Master의 데이터를 복제해 가지는 구조이다. 요즘은 Master/Slave 대신 Primary/Replica라는 표현을 더 많이 사용한다.
1
2
Master = 쓰기 작업을 담당하는 메인 서버
Slave = Master 데이터를 복제하는 보조 서버
문제는 Redis를 분산 락으로 사용할 때 Master가 갑자기 죽는 경우이다.
1
2
3
4
5
6
1. Client A가 Master에 락을 획득
2. 락 정보가 Slave로 복제되기 전에 Master 장애 발생
3. Slave가 새 Master로 승격
4. 새 Master에는 A의 락 정보가 없음
5. Client B도 락을 획득
→ 결과적으로 Client A와 Client B가 동시에 같은 락을 잡았다고 생각하는 상황이 생길 수 있다.
이를 보완하기 위해 Redis 창시자는 Redlock 알고리즘을 제안했다. Redlock은 하나의 Redis 노드에만 락을 거는 것이 아니라, 여러 독립 Redis 노드에 락을 요청하고 과반수 이상에서 락 획득에 성공했을 때만 락을 인정하는 방식이다.
예를 들어 Redis 노드 5개 중 3개 이상에서 락 획득에 성공해야 락을 얻은 것으로 판단한다.
하지만 Redlock도 완벽한 해결책은 아니다. 분산 시스템에서는 네트워크 지연, 서버 시간 차이, 장애 복구 과정에서 상태 불일치가 발생할 수 있다. 그래서 금융 거래나 계좌 이체처럼 절대 꼬이면 안 되는 영역에서는 Redis 분산 락보다 DB 트랜잭션, Zookeeper, etcd 같은 더 강한 합의 방식을 고려해야 한다.
정리
Redis는 단순히 캐시로만 사용하는 도구가 아니라, 다양한 자료구조와 원자 연산을 활용해 분산 락, 선착순 이벤트 처리, 큐와 비슷한 구조까지 구현할 수 있는 인메모리 저장소이다.
하지만 Redis가 모든 문제를 해결해주는 것은 아니다. Redis Pub/Sub은 메시지를 저장하지 않기 때문에 중요한 데이터 전달용으로 사용하면 유실 위험이 있고, Redis 기반 분산 락 역시 Master 장애나 복제 지연 상황에서 한계가 존재한다.
Kafka는 Redis보다 무겁지만, 메시지를 안정적으로 저장하고 재처리할 수 있어 이벤트 기반 비동기 처리에 적합하다. 반면 Redis는 빠른 원자 연산과 가벼운 구조가 필요한 상황에 적합하다.
결국 중요한 것은 기술 자체가 아니라 상황에 맞게 선택하는 것이다.
이번 복습을 통해 Redis를 단순 캐시 저장소가 아니라, 성능 개선과 동시성 제어를 위한 도구로 이해할 수 있었다. 동시에 Redis의 한계도 함께 알아야 안전한 설계를 할 수 있다는 점을 배웠다.
댓글
궁금한 점, 피드백, 오류 제보를 남겨 주세요.