Redis 캐싱 전략
작성일: 2025.11.06
백엔드 개발자라면 누구나 한 번쯤 "API 응답 속도가 너무 느린데?"라는 고민에 빠지게 된다. 쿼리 튜닝, 인덱스 설정 등 다양한 방법이 있지만 가장 적은 노력으로 드라마틱한 성능 향상을 이끌어낼 수 있는 방법이 바로 캐싱(Caching) 이다.
이번 글에서는 가장 대중적인 인메모리 저장소인 Redis를 Docker로 띄우고, 실제 백엔드 로직에 캐싱을 적용하여 성능을 최적화하는 과정을 정리해 보려 한다.
1. 왜 캐싱(Caching)인가?
사용자가 늘어날수록 데이터베이스(DB)의 부하는 기하급수적으로 증가한다. DB는 디스크 I/O가 발생하기 때문에 메모리에 비해 속도가 현저히 느릴 수밖에 없다.
- 문제 상황: 매번 똑같은 데이터를 조회하기 위해 무거운 DB 쿼리를 반복 실행함.
- 해결책: 자주 사용되는 데이터를 속도가 빠른 메모리(Redis) 에 임시 저장해두고 꺼내 씀.
- 효과: DB 부하 감소 + 응답 속도(Latency) 획기적 단축.
2. Docker로 Redis 환경 1분 만에 구축하기
로컬 환경에 Redis를 직접 설치하는 번거로움 없이, Docker Compose를 사용해 깔끔하게 띄워보자.
2-1. docker-compose.yml 작성
프로젝트 루트 경로에 docker-compose.yml 파일을 생성하고 아래 내용을 입력한다.
version: '3.8'
services:
redis:
image: redis:7.0-alpine # 가볍고 안정적인 Alpine 버전 사용
container_name: my-redis-cache
ports:
- "6379:6379" # 호스트:컨테이너 포트 매핑
volumes:
- ./redis_data:/data # 컨테이너가 꺼져도 데이터가 유지되도록 볼륨 설정
command: redis-server --appendonly yes # AOF(데이터 영속성) 모드 활성화
restart: always
volumes:
redis_data:
2-2. 실행 및 접속 테스트
터미널에서 다음 명령어로 Redis를 실행한다.
# 백그라운드 모드로 실행
$ docker compose up -d
# 정상 실행 확인
$ docker ps
▲ Docker 실행 후 redis-cli 접속 테스트 화면
Redis 컨테이너 내부로 진입하여 잘 작동하는지 테스트해 본다. PONG 응답이 오면 성공이다.
3. Cache-Aside 전략
캐싱을 구현하는 패턴 중 가장 널리 쓰이는 Look Aside (Cache Aside) 패턴을 적용해 보겠다.
데이터를 찾을 때 **1순위로 캐시(Redis)**를 확인하고, 없으면 2순위로 DB를 조회하는 방식이다. 읽기 작업이 많은 서비스에 적합하다.
3-1. 시나리오 설정
예시) "상품 상세 정보(Product)" 는 조회 빈도가 매우 높지만 정보가 자주 바뀌지는 않는다. 캐싱하기 딱 좋은 대상이다.
3-2. 로직 구현
public Product getProductDetail(Long productId) {
String cacheKey = "product:" + productId;
// 1. 캐시 확인
// Redis에서 해당 키의 데이터가 있는지 먼저 조회합니다.
Product cachedProduct = redisTemplate.opsForValue().get(cacheKey);
if (cachedProduct != null) {
// 히트!!! 캐시에 데이터가 있다면 DB를 거치지 않고 즉시 반환!
log.info("Cache Hit! - " + productId);
return cachedProduct;
}
// 2. 캐시 미스
// 캐시에 데이터가 없다면 DB에서 직접 조회합니다.
log.info("Cache Miss! DB 조회 - " + productId);
Product dbProduct = productRepository.findById(productId)
.orElseThrow(() -> new NotFoundException("상품이 없습니다."));
// 3. 캐시 저장
// **TTL**을 10분으로 설정하여 데이터가 영원히 남지 않도록 합니다.
redisTemplate.opsForValue().set(cacheKey, dbProduct, 10, TimeUnit.MINUTES);
return dbProduct;
}
4. 데이터 정합성과 캐시 무효화
캐싱을 할 때 가장 주의해야 할 점은 **"DB 데이터는 변했는데 캐시는 그대로인 상황(Stale Data)"**이다. 이를 막기 위해 캐시 무효화 전략이 필수적이다.
전략 1: TTL (만료 시간) 설정
위 코드에서 적용한 방식이다. 데이터를 저장할 때 expire 시간을 준다. 구현이 쉽지만, TTL이 끝나기 전까지는 변경 사항이 반영되지 않는다.
전략 2: Write-Through (수정 시 삭제)
데이터가 수정(Update)되거나 삭제(Delete)될 때, 캐시도 강제로 날려버리는 방법이다. 가장 확실하다.
@Transactional
public void updateProduct(Long productId, ProductUpdateDto updateDto) {
// 1. DB 데이터 수정
Product product = productRepository.findById(productId);
product.update(updateDto); // DB Update 쿼리 발생
// 2. 관련 캐시 삭제 (Eviction)
// 데이터가 변했으므로 기존 캐시는 더 이상 유효하지 않습니다. 즉시 지워줍니다.
String cacheKey = "product:" + productId;
redisTemplate.delete(cacheKey);
// 다음 조회 요청 시, 새로운 데이터가 DB에서 조회되어 캐시에 다시 저장될 것입니다.
}
5. 성능 측정 및 결론
Postman을 사용하여 캐싱 적용 전후의 응답 속도를 비교해 보자.
5-1. 첫 번째 요청 (Cache Miss)
서버를 띄우고 API를 처음 호출했을 때의 상황이다. 캐시가 비어있으므로 DB를 조회한다.
▲ 첫 요청 시: DB를 다녀오느라 응답 속도가 느리다. (약 355ms)
5-2. 두 번째 요청 (Cache Hit!!)
동일한 API를 다시 한번 호출한다. 이제 Redis 캐시가 동작한다.
▲ 두 번째 요청 시: 캐시가 적중하여 속도가 획기적으로 줄었다. (약 27ms, 후에는 6ms로 훨씬 더 빨라짐!!)
5-3. 비교
| 구분 | 적용 전 (DB Only) | 적용 후 (Redis Caching) | 개선 효과 |
|---|---|---|---|
| 응답 속도 (Latency) | 355ms | 6ms | 약 50배 향상 |
| 처리량 (TPS) | 100 | 1500 | 약 15배 증가 |
| DB CPU 사용률 | 70% | 5% | 안정성 확보 |
마무리하며
난 이전까지는 Redis를 로그인 세션 처리 용도로만 한정적으로 사용했었다. 하지만 이번 실습을 통해 데이터베이스 부하가 심한 대용량 조회 로직이나, 반복적인 연산이 필요한 데이터에 Redis 캐싱을 적용하면 성능을 획기적으로 개선할 수 있다는 것을 체감하게 되었다.
역시 많은 프로젝트와 회사들에서 채택하여 쓰는데는 이유가 있다.