Java 아키텍처·Spring 용어 사전
캐시레디스

Redis

메모리에 사는 자료구조 서버. 단일 스레드인데도 빠른 이유가 곧 설계의 핵심이다.

메모리에 데이터를 두는 키-값 저장소. 다만 "빠른 맵" 이라고만 보면 절반만 이해한 것이다. Redis 는 값을 통째로 넣고 빼는 것이 아니라 자료구조 자체를 서버에 두는 물건이다.

구조쓰임
String단순 캐시, 카운터
Hash객체를 필드별로 — 한 필드만 고쳐도 전체를 다시 안 보낸다
List
Set중복 없는 집합, 교집합 연산
Sorted Set점수 순 정렬 — 랭킹·리더보드
Stream추가 전용 로그

객체를 JSON 문자열로 통째 저장하면 필드 하나 고치는 데도 전체를 읽고 고쳐 다시 쓴다. Hash 로 두면 그 필드만 건드린다. 자료구조를 고르는 일이 곧 네트워크 왕복량을 고르는 일이다.

단일 스레드인데 왜 빠른가

명령을 한 번에 하나씩 처리한다. 느릴 것 같지만 셋이 맞물려 빠르다.

  • 디스크 I/O 가 없다 — 전부 메모리에 있다. 디스크 접근은 밀리초, 메모리 접근은 나노초 단위다. 자릿수가 여섯 개 차이 난다
  • 경합 비용이 0 이다 — 스레드가 하나뿐이니 락도, 락을 기다리다 생기는 컨텍스트 스위칭도 없다. 동시성을 포기한 대가로 얻은 것이다
  • 연결은 I/O 멀티플렉싱으로 받는다epoll·kqueue 로 한 스레드가 수만 연결을 논블로킹하게 다룬다. "데이터가 도착한 연결" 만 커널이 알려 주므로, 연결 수가 스레드 수를 강요하지 않는다

즉 병목이 CPU 가 아니라 메모리 대역폭과 네트워크다. 한 스레드로도 초당 수십만 명령이 나온다.

그 대가가 원자성을 준다

한 번에 하나씩 처리하니 개별 명령이 그 자체로 원자적이다. INCR 로 값을 읽고 더하고 쓰는 사이에 다른 명령이 끼어들 수 없다. 애플리케이션이라면 락을 걸어야 할 일을 명령 하나로 끝낸다. 분산 락(SET key val NX PX 3000)·카운터·레이트 리밋을 별도 동기화 없이 세울 수 있는 근거가 이것이다.

뒤집으면 느린 명령 하나가 전부를 막는다. KEYS * 로 전체 키를 훑거나 원소가 수백만인 컬렉션을 통째로 지우면 그 시간 동안 모든 요청이 줄을 선다. 운영에서 KEYS 대신 커서로 조금씩 훑는 SCAN 을 쓰라는 것이 이 때문이다.

왕복을 줄이는 것이 곧 성능이다

명령 하나하나가 나노초에 끝나는데 네트워크 왕복은 수백 마이크로초다. 명령 100개를 따로 보내면 처리 시간이 아니라 왕복 시간이 전부를 차지한다.

  • 파이프라이닝 — 응답을 기다리지 않고 명령을 몰아 보낸 뒤 응답을 한꺼번에 받는다
  • MGET·HMGET — 여러 키를 한 명령으로 가져온다
  • Lua 스크립트 — 여러 명령을 서버에서 원자적으로 실행한다. 읽고 판단해서 쓰는 로직을 왕복 한 번으로 끝낸다

"Redis 가 느리다" 는 진단은 대개 Redis 가 아니라 왕복 횟수의 문제다.

만료는 어떻게 처리되나

TTL 이 지났다고 그 순간 지워지는 것이 아니다. 그러면 만료 시각마다 타이머가 필요하다. Redis 는 둘을 섞어 쓴다.

  • 접근할 때(lazy) — 조회 시점에 만료됐으면 그때 지우고 없다고 답한다
  • 주기적으로(active) — 틈틈이 TTL 있는 키를 표본 추출해 만료된 것을 지운다

그래서 만료된 키가 잠시 메모리를 차지하고 있을 수 있다. 메모리 사용량이 예상보다 큰 이유가 여기일 때가 있다.

면접 함정

  • "단일 스레드라 멀티코어를 못 쓴다" → 맞지만 그래서 느린 것은 아니다. 코어를 더 쓰려면 인스턴스를 여러 개 띄운다. 최근 버전은 네트워크 I/O 만 별도 스레드로 나눌 수 있으나 명령 실행은 여전히 하나다.
  • "Redis 는 캐시다" → 캐시로 많이 쓸 뿐, 자료구조 서버이자 (영속성을 켜면) 저장소다.
  • "메모리에 있으니 무조건 빠르다" → 왕복 횟수와 느린 명령이 실제 병목이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 캐시·Redis — 어사이드·쓰기 전략