Skip to main content

Redis 캐싱 전략


Intro

백엔드 개발자라면 누구나 한 번쯤 "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 파일을 생성하고 아래 내용을 입력합니다.

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 ps docker ping

▲ Docker 실행 후 redis-cli 접속 테스트 화면

Redis 컨테이너 내부로 진입하여 잘 작동하는지 테스트해 봅니다. PONG 응답이 오면 성공입니다.


3. Cache-Aside 전략

캐싱을 구현하는 패턴 중 가장 널리 쓰이는 Look Aside (Cache Aside) 패턴을 적용해 보겠다.

Cache Aside 패턴이란?

데이터를 찾을 때 **1순위로 캐시(Redis)**를 확인하고, 없으면 2순위로 DB를 조회하는 방식입니다. 읽기 작업이 많은 서비스에 적합합니다.

3-1. 시나리오 설정

예시) "상품 상세 정보(Product)" 는 조회 빈도가 매우 높지만 정보가 자주 바뀌지는 않는다. 캐싱하기 딱 좋은 대상입니다.

3-2. 로직 구현

ProductService.java
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를 조회합니다.

docker ps

▲ 첫 요청 시: DB를 다녀오느라 응답 속도가 느리다. (약 355ms)

5-2. 두 번째 요청 (Cache Hit!!)

동일한 API를 다시 한번 호출한다. 이제 Redis 캐시가 동작합니다.

docker ps docker ps

▲ 두 번째 요청 시: 캐시가 적중하여 속도가 획기적으로 줄었다. (약 27ms, 후에는 6ms로 훨씬 더 빨라짐!!)

5-3. 비교

구분적용 전 (DB Only)적용 후 (Redis Caching)개선 효과
응답 속도 (Latency)355ms6ms약 50배 향상
처리량 (TPS)1001500약 15배 증가
DB CPU 사용률70%5%안정성 확보

마무리하며

저는 이전까지는 Redis를 로그인 세션 처리 용도로만 한정적으로 사용했었습니다. 하지만 이번 실습을 통해 데이터베이스 부하가 심한 대용량 조회 로직이나, 반복적인 연산이 필요한 데이터에 Redis 캐싱을 적용하면 성능을 획기적으로 개선할 수 있다는 것을 체감하게 되었다.

오늘의 한 줄 요약

역시 많은 프로젝트와 회사들에서 채택하여 쓰는데는 이유가 있습니다.