레디스가 클라이언트 처리/관리하는 방법, 성능 향상 위한 파이프라이닝, 클라이언트 사이드 캐싱에 대해 알려주겠다
클라이언트 핸들링
TCP 포트, 유닉스 소켓을 통해 연결
- 레디스는 클라 연결 수락하는데 TCP 포트, 유닉스 소켓을 사용할 수 있다.
- 일반적으로 TCP 포트로 받음
port 6379
- 설정파일에서 unixsocket, unixsockerperm 설정하면 원하는 경로에 소켓 파일 생성 가능
unixsocket /tmp/redis.socket
unixsocketperm 777
※동일한 서버 안에 앺과 레디스가 같이 있는 구조면 성능 향상
※.socket 파일은 데이터 저장 용도가 아니라 데이터 주고받는 용도임
이후 클라가 해당 소켓 파일 경로로 레디스 서버 연결 가능 (redis-cli -s /tmp/redis.sock)
🤖 .socket 파일은 어떤 역할을 하나요?
유닉스 소켓 파일은 일종의 "전화선 연결 단자"라고 생각하면 쉽습니다.
- 레디스 서버가 실행되면서 "나는 이 파일 경로(
/tmp/redis.sock)를 통해 대화할 준비가 됐다"라고 OS에 선언합니다.- **애플리케이션(클라이언트)**이 이 파일에 접근하면, OS 커널이 "아, 너 레디스랑 대화하고 싶구나?" 하고 둘 사이의 메모리 통로를 직접 연결해 줍니다.
- 데이터 흐름: 데이터는 파일을 거쳐서 저장되는 게 아니라, 커널 내부의 버퍼를 통해 메모리에서 메모리로 직접 이동합니다.
요약하자면:
redis.sock파일 자체에는 아무런 내용이 저장되지 않습니다.- 이 파일은 애플리케이션과 레디스 서버가 **서로를 찾아내고 대화를 시작하기 위한 '접점'**일 뿐입니다.
- 연결이 성립되면 데이터는 파일 시스템을 거치지 않고 OS 커널을 통해 빛의 속도로 오고 갑니다.
멀티플렉싱
- 하나의 통신 채널로 여러 데이터 스트림 전송 가능
- 하나의 스레드에서 여러 소켓 감시, 소켓 이벤트 발생하는지 지속적으로 확인 가능하기 때문에 다중 클라이언트 지원 가능하고 많은 클라 요청 동시에 처리하는데 블로킹 문제를 피할 수 있다.
- 논블롴킹 I/O를 활용해서 I/O 작업 끝날 때 까지 기다리지 않고 다른 작업 처리한다.
🤖 멀티플렉싱 - single thread / non-blocking
연결된 소켓 감시하면서 연결 요청할 때만 처리해줌
소켓을 소켓 파일이 아니라 TCP 포트는 소켓 파일이든 통신의 끝점을 의미 (커널이 발급한 통신 번호표 FIle descriptor 를(tcp든 소켓 파일이든) 레디스는 감시)
**
**1. 이해를 돕는 비유: "베테랑 웨이터"
멀티플렉싱이 없는 방식과 있는 방식을 식당에 비유해 보겠습니다.
- 멀티플렉싱이 없는 경우 (Multi-thread/Blocking): 손님 한 명당 담당 웨이터가 한 명씩 붙습니다. 손님이 메뉴판을 보는 동안 웨이터는 그 앞에서 아무것도 안 하고 기다려야 합니다(Blocking). 손님이 1,000명이면 웨이터도 1,000명이 필요하고, 식당은 인건비(메모리, CPU 컨텍스트 스위칭 비용)로 망하게 됩니다.
- 멀티플렉싱이 있는 경우 (Single-thread/Non-blocking): 베테랑 웨이터 한 명이 구역 전체를 담당합니다. 웨이터는 가만히 서 있지 않고 계속 테이블 사이를 돕니다. "주문하실 분?" 하고 물어보며 손을 든 손님(이벤트 발생)에게만 가서 주문을 받습니다. 메뉴판을 보는 손님은 그냥 지나칩니다. 혼자서 수십 개의 테이블을 빛의 속도로 처리할 수 있습니다.
2. 기술적인 핵심 원리
레디스는 Linux의
epoll(또는 BSD의kqueue, Windows의select)이라는 시스템 콜을 사용합니다.
- 감시 (Monitoring): 레디스는 커널에게 "내가 관리하는 수천 개의 소켓(클라이언트 연결) 중에 데이터가 들어온 놈이 있는지 감시해 줘"라고 부탁합니다.
- 알림 (Event Notification): 커널은 소켓들을 지켜보다가, 데이터가 도착한 소켓들의 목록만 레디스에게 딱 전달합니다.
- 처리 (Execution): 레디스는 전체를 다 뒤질 필요 없이, 알림이 온 소켓들만 순서대로 처리합니다.
이 과정이 워낙 빨라서 사용자 입장에서는 "동시에" 처리되는 것처럼 느껴지는 것입니다.
❓소켓 감시한다했는데 소켓 파일 감시하는거야? 그럼 TCP 포트로 연결된 클라는 어케 감시해?
아주 날카로운 질문입니다! "소켓(Socket)"이라는 용어가 유닉스 소켓 파일과 헷갈릴 수 있는데, 네트워크 세계에서 소켓은 '파일' 그 자체라기보다 '통신의 끝점(Endpoint)'을 추상화한 개념이라고 이해하시면 명확해집니다.
레디스가 TCP 포트와 유닉스 소켓을 동시에 감시하는 원리는 다음과 같습니다.
1. "모든 소켓은 파일 디스크립터(FD)다"
리눅스 같은 유닉스 계열 운영체제에서 가장 중요한 원칙은 **"모든 것을 파일처럼 다룬다"**는 것입니다.
- TCP 연결: 클라이언트가 6379 포트로 접속하면, 커널은 이 연결을 나타내는 **번호표(File Descriptor, FD)**를 레디스에게 줍니다. (예: 10번 FD)
- 유닉스 소켓:
/tmp/redis.sock을 통해 접속해도, 커널은 똑같이 **번호표(FD)**를 줍니다. (예: 11번 FD)레디스 입장에서는 이게 실제 파일인지, 네트워크 연결인지 구분할 필요가 없습니다. 그저 커널이 준 '번호표'들만 모아서 감시하면 됩니다.
2. 멀티플렉싱의 감시 대상: FD 리스트
레디스가
epoll같은 멀티플렉싱 기술을 쓸 때, 커널에게 넘겨주는 리스트는 아래와 같은 형태입니다."커널님, 여기 제가 관리하는 번호표(FD) 목록입니다. 데이터 들어오는 놈 있으면 알려주세요!"
- FD 3: (TCP 리스닝 소켓 - 새 접속 기다림)
- FD 4: (유닉스 소켓 리스닝 - 새 접속 기다림)
- FD 10: (이미 연결된 A라는 외부 TCP 클라이언트)
- FD 11: (이미 연결된 B라는 로컬 유닉스 소켓 클라이언트)
3. TCP 클라이언트는 어떻게 감시하나요?
TCP 포트로 연결된 클라이언트도 결국 **OS 커널 내부에 통신 버퍼(Buffer)**를 가지고 있습니다.
- 데이터 도착: 외부 클라이언트가 패킷을 보내면 랜카드(NIC)를 통해 들어와 커널의 해당 TCP 소켓 버퍼에 쌓입니다.
- 이벤트 발생: 커널은 "오, 10번 FD(TCP 클라이언트)에 데이터 들어왔네?" 하고 체크합니다.
- 알림: 레디스가 "뭐 온 거 있나요?" 하고 물어보면(
epoll_wait), 커널은 "10번이랑 11번에 데이터 왔으니까 읽어가세요"라고 리스트를 던져줍니다.- 읽기: 레디스는 10번(TCP) 소켓에서 데이터를 읽고 명령을 처리합니다.
4. 요약: "파일"이 아니라 "통로의 번호"를 감시
- 유닉스 소켓 파일: 실제로 디스크에 존재하는 파일 경로를 주소로 쓰지만, 통신이 시작되면 OS는 이를 **번호표(FD)**로 관리합니다.
- TCP 포트: IP/Port를 주소로 쓰지만, 연결이 맺어지면 이 또한 OS가 **번호표(FD)**로 관리합니다.
즉, 레디스의 멀티플렉싱은 TCP든 유닉스 소켓이든 상관없이, OS가 부여한 '통신 번호표(File Descriptor)'들을 한꺼번에 감시하기 때문에 다중 클라이언트를 동시에 처리할 수 있는 것입니다.
TCP_NODELAY 옵션
- 레디스는 클라 커넥션 생성 시
TCP_NODELAY옵션 사용 - 소켓은 작은 데이터여도 버퍼링하지 않고 지연 없이 가능한 빨리 패킷 전송 시도함
- 이걸로 연결 지연 최소화함
- 레디스 7버전에서 레디스 ↔ 클라끼리는
TCP_NODELAY해제 불가, 레디스 복제 연결 할 때는TCP_NODELAY해제 가능- 복제 연결은 대용량 데이터라
repl-disable-tcp-nodelay on하면 적은 양의 TCP 패킷 수, 대역폭 사용/ off 하면 대역폭 커짐 - ※ 대역폭: 한 번에 보낼 수 있는 데이터 크기
- 복제 연결은 대용량 데이터라
maxclients
- 클라 소켓에서 새 데이터 도착 시 읽기 가능한 파일 이벤트 생성하고 레디스는 이걸로 전송한 쿼리 수집하고 처리한다.
- 클라 초기화되면 레디스는 maxclients 설정값 보다 큰지 확인하고 크면 새 클라 접속 거부한다.
- 레디스 2.6 이후 maxclients는 기본 10000임. 근데 레디스 시작할 때 32개 파일 디스크립터 끼고 있기 때문에 maxclients + 32 보다 os의 파일 디스크립터 수가 작으면 임의로 os 값에 맞게 maxclients 값이 변경돼 레디스 인스턴스가 시작된다.
1. 파일 디스크립터(FD)란?
리눅스/유닉스 시스템에서 프로세스가 파일, 소켓, 파이프 등 I/O 자원에 접근하기 위해 사용하는 **'정수형 번호표'**입니다.
- 프로세스가 파일을 열거나 네트워크 연결(TCP, 소켓)을 맺을 때마다 OS는
3,4,5... 같은 숫자를 부여합니다.
클라이언트 버퍼 제한
출력 버퍼 제한
- 레디스는 클라에 반환할 응답 데이터를 각 클라의 출력 버퍼에 저장한다.
- 클라가 1000개면 출력 버퍼도 1000개 생성한다.
- 만약 어떤 클라가 데이터 처리 못하고 계속 요청만 보내면 출력 버퍼 크기가 증가해서 메모리 사용량 늘어날 것
- 출력 버퍼는 클라가 응답을 가져가야지만 비워진다.
- 특히 pub/sub에서 구독자가 메시지 제때 처리하지 못하면 출력 버퍼 크기 계속 증가함
- 그래서 출력 버퍼 크기를 제한 둬서 넘어가면 클라 연결 종료한다.
- 하드 제한: 고정된 제한값
- 소프트 제한: 시간을 지정. ex) 10초 동안 지속적으로 출력 버퍼 32MB보다 크면 연결 닫기
- 일반 클라 / pub/sub 클라 / 복제본 위한 출력 버퍼 크기를 각각 다르게 적용
- 일반 클라: 기본값 0. (커맨드 보내고 다음 커맨드 보내기 전 응답 기다리기 때문에 연결 닫는것은 예상하지 못한 동작 방식)
- pub/sub 클라: 기본 하드 제한 32MB, 소프트 제한은 60초 당 8MB
- 복제본 위한 출력 버퍼: 기본 하드 제한 256MB, 소프트 제한은 60초 당 64MB (대용량 데이터라 더 큼)
- 제한 설정 변경하기 (설정 파일 OR CONFIG)
-
CONFIG SET client-output-buffer-limit <class> <hard-limit> <soft-limit> <soft-limit-duration>
-
복제 클라 limit 모두 제거
CONFIG SET client-output-buffer-limit "slave 0 0 0"
`<class>`: normal, slave(replica), pubsub 중 하나
`<soft-limit-duration>` : soft-limit이 적용되는 시간 (초)
### 쿼리 버퍼 제한
- 클라에서 받은 커맨드를 잠시 보관하는 클라 쿼리 버퍼 (내부 버퍼)도 있음
- 클라가 레디스에 커맨드 전송하면 쿼리 버퍼에 저장되고 처리함
- 기본 1GB, `client-query-buffer-limit` 값으로 조정 가능
<br>
## 클라이언트 이빅션 (퇴출)
- 레디스에 클라 연결 수가 증가하면 메모리 사용량도 증가함
- 클라 연결이 메모리 넘 많이 사용하면 OOM이나 데이터 이빅션 유발 가능
- 레디스 7 이하 버전에서는 `maxmemory-policy` 값이 클라 연결, 데이터 저장 메모리 합해서 쓰는 한도였음
- 레디스 7 부터 `maxmemory-clients` 로 분리돼서 연결된 모든 클라 연결의 최대 총 메모리 사용량 정의 가능하다.
- 쿼리 버퍼, 출력 버퍼, 중간 버퍼에서 사용하는 메모리 다 포함됨
- 가장 많은 메모리 사용하는 클라부터 연결 끊는다.
- 설정파일이나 CONFIG로 설정 가능
```java
maxmemory-clients 0 # 기능 사용 안 함 (기본값)
maxmemory-clients 1G # 클라 메모리 1G 넘으면 클라 접속 끊음
maxmemory-clients 5% # 클라 메모가 maxmeory의 5% 넘으면 클라 접속 끊음
- 대규모 트래픽 환경에선 5%, 10% 같이 값 설정 권장함
- 복제 연결로 사용되는 복제본, 마스터 커넥션은 이빅션 기능에 영향 받지 않음 (메모리 많이 써도 안 끊어짐)
maxmemory-clients계산에서 제외됨
- 특정 클라는 이빅션 대상에서 제외하는 것도 가능 (ex. 레디스 서버 주기적으로 모니터링하는 클라)
> CLIENT NO-EVICT on
Timeout과 TCP Keepalive
timeout
- 레디스 서버에 클라 연결되면 커맨드 오랫동안 수행하지 않더라도 연결은 유지도.ㄴ다.
- 타임아웃 설정으로 활동 없는 유휴 연결 해제 가능 (설정파일 or CONFIG)
> CONFIG SET timeout 600 (기본값 0)
pub/sub 클라는 영향 안 받음
tcp-keepalive
- 연결된 클라한테 주기적으로 TCP ACK보내고, 응답 없으면 연결 끊는 설정
- timeout처럼 활동 없으면 끊는게 아니라 응답 안 할 때만 연결 끊음
- 기본값 300초 (5분마다 TCP ACK 보냄)
파이프라이닝
- 요청과 응답 사이의 왕복 시간(RTT, Round Trip Time)이 성능에 큰 영향 끼침
- RTT가 250ms면 초 당 10만개 처리가능한 레디스라도 초 당 4개밖에 처리 못함
- 파이프라이닝을 쓰면 네트워크 통신 소요 시간을 줄일 수 있다.
- 파이프라이닝: 여러 커맨드를 일괄 전송
- 파이프라이닝은 RTT뿐만 아니라 레디스 서버 처리량도 크게 향상 시킴
- 레디스 서버가 클라 응답 위해 소켓 I/O (네트워크 I/O) 수행할 때, os 커널의 read(), write() 시스템 콜 호출하는데 이게 레이턴시가 큼 (레디스가 데이터 찾고 반환하는 것보다)
- 파이프라이닝 사용하면 여러 명령이 단일 read(), write()로 호출되기 때문에 처리량을 줄일 수 있다.
- 키 100만개 접근 로직에서 파이프라이닝 미사용: 4시간 2분, 사용: 3분33초
- 네트워크 대역폭 고려해 배치 사이즈 정하자
- 파이프라이닝의 일괄 커맨드가 원자성을 보장하진 않는다.

- 클라 1 파이프라이닝 커맨드 실행 중 클라 2 파이프라이닝 들어오면 사이좋게 수행
- 파이프라이닝 커맨드 중간에 에러나면 롤백하지 않는다. 에러난 커맨드만 빼고 다른 커맨드는 정상 수행한다.
클라이언트 사이드 캐싱
- 레디스 6버전에 클라 사이드에서 캐싱하는 기능 추가됨
- 레디스 통신 중 가장 많은 시간 차지하는 건 네트워크 I/O (RTT)임
- 네트워크 I/O 를 줄이기 위해 쿼리마다 레디스에 요청하는 대신 클라 측에서 데이터 로컬 캐싱 하는 방법이 있음
- 클라 사이드 캐싱 사용하면 앺이 쿼리 응답을 앺 메모리 내 직접 저장한다. (로컬 캐싱)
- 업데이트 된 데이터 정합성 맞추기 위한 방법 고려 필요 (트래킹이라 부름)

- 기본 모드: 클라가 접근한 키 하나하나를 기억해서, 동일한 키가 수정될 때 무효 메시지를 전송한다. (레디스 서버 메모리 ↑)
- 브로드캐스팅 모드: 특정 prefix 키에 접근한 클라를 기억함. 레디스 서버 메모리 ↓, 클라 쪽에는 자기가 안 갖고 있지만 prefix 일치하는 키여도 무효 메시지 받는 단점
❓ 앺에 로컬 캐싱하는건데 레디스에서 제공하는 클라 사이드 캐싱은 뭐야
"내 데이터가 바뀌었으니 너의 로컬 캐시를 지워라!"라고 알려주는 '서버 주도형 동기화 알림(Invalidation)' 서비스
기존 로컬 캐싱의 가장 큰 문제: 정합성
레디스가 이 기능을 제공하기 전에도 앱 메모리에 데이터를 저장할 수는 있었습니다. 하지만 문제는 **"언제 이 데이터를 업데이트(삭제)할 것인가?"**였습니다.
- 기존 방식: 앱이 데이터를 1분 동안 로컬 캐싱함. 그 사이에 다른 앱이 레디스의 데이터를 수정함. 앱은 1분 동안 **'낡은 데이터(Stale Data)'**를 계속 사용하게 됨.
- 레디스 6 방식: 데이터가 변경되는 즉시 레디스가 앱에게 **"야, 너 그거 가지고 있지? 그거 변했으니까 빨리 지워!"**라고 신호를 보냅니다.
레투스 실전
public class RedisClientSideCachingExample {
public static void main(String[] args) {
// 1. 레디스 클라이언트 및 연결 생성
RedisClient redisClient = RedisClient.create("redis://localhost:6379");
StatefulRedisConnection<String, String> connection = redisClient.connect();
// 2. 로컬 캐시로 사용할 Map 생성 (애플리케이션 메모리)
Map<String, String> localCache = new ConcurrentHashMap<>();
// 3. CacheFrontend 설정
// - 클라이언트 사이드 캐싱 기능을 활성화하고 로컬 Map과 레디스 연결을 바인딩합니다.
CacheFrontend<String, String> frontend = ClientSideCaching.create(
CacheAccessor.forMap(localCache),
connection
);
// 4. 데이터 조회 시뮬레이션
String key = "user:100";
while (true) {
// frontend.get()을 호출하면:
// 1) 로컬 캐시(Map)에 데이터가 있는지 확인 (있으면 즉시 반환, RTT 0)
// 2) 없으면 레디스 서버에서 가져온 뒤 로컬 캐시에 저장하고 CLIENT TRACKING 등록
String value = frontend.get(key);
System.out.println("현재 값: " + value);
try {
Thread.sleep(3000); // 3초마다 확인
} catch (InterruptedException e) {
e.printStackTrace();
}
// 이 상태에서 다른 터미널에서 redis-cli로 'SET user:100 "new_value"'를 입력하면
// 레디스 서버가 이 앱에 무효화 알림을 보내고, Lettuce가 localCache에서 해당 키를 자동으로 삭제합니다.
}
}
}
- localCache Map을 lettuce의 CleintSideCaching과 바인딩한다.
- 데이터 변경되면 레디스가 푸시 보내고 lettuce가 받아서 localCache Map에 키 remove한다.
클라이언트 사이드 캐싱을 지원하는 라이브러리(예: Java의 Lettuce, Go의 Go-redis 등)를 사용하면, 내부적으로 CLIENT TRACKING ON 설정을 호출하고 레디스에서 보내주는 무효화 푸시 메시지를 알아서 해석해서 여러분의 앱 메모리에 있는 Map이나 객체를 삭제해 줍니다.
→
// frontend.get()을 호출하면:
// 1) 로컬 캐시(Map)에 데이터가 있는지 확인 (있으면 즉시 반환, RTT 0)
// 2) 없으면 레디스 서버에서 가져온 뒤 로컬 캐시에 저장하고 CLIENT TRACKING 등록'SearchDeveloper > 개발자를 위한 레디스' 카테고리의 다른 글
| 13 레디스 운영하기 (끝) (0) | 2026.06.07 |
|---|---|
| 11장 보안 (0) | 2026.06.07 |
| 10장 클러스터 (0) | 2026.06.07 |
| 9장 센티널 (0) | 2026.06.07 |
| 8장 복제 (1) | 2026.06.07 |
