0623 백엔드 실무 심화 (3/N): Redis 캐싱과 성능 최적화
✅ 1. 캐싱이란 무엇인가?
- 캐싱(Caching)은 자주 사용되는 데이터를 더 빠르게 가져오기 위해 임시 저장소에 보관하는 방식입니다.
- 매번 DB를 조회하거나 외부 API를 호출하지 않고, 한 번 가져온 결과를 일정 시간 동안 저장해두고 재사용합니다.
- 웹서비스에서는 상품 목록, 배너 목록, 카테고리, 공지사항, 설정값, 메인 페이지 데이터, 통계 결과처럼 자주 조회되는 데이터를 캐싱할 수 있습니다.
➕ 1-1. 캐싱이 필요한 이유
-
응답 속도 개선
- DB 조회보다 메모리 기반 캐시 조회가 훨씬 빠릅니다.
-
DB 부하 감소
- 자주 조회되는 데이터를 캐시에서 처리하면 DB 요청 수가 줄어듭니다.
-
외부 API 비용 절감
- 외부 API 호출 결과를 캐싱하면 호출 횟수와 비용을 줄일 수 있습니다.
-
트래픽 증가 대응
- 광고 유입, 이벤트 페이지, 사전예약 페이지처럼 순간 트래픽이 몰릴 때 캐시가 큰 도움이 됩니다.
캐시 없음:
사용자 1,000명 접속
↓
DB 조회 1,000번
캐시 있음:
첫 사용자만 DB 조회
↓
나머지 사용자는 캐시 조회
✅ 2. Redis란 무엇인가?
- Redis는 메모리 기반의 Key-Value 저장소입니다.
- 데이터베이스처럼 디스크 중심으로 저장하는 것이 아니라, 주로 메모리에 데이터를 저장하기 때문에 매우 빠릅니다.
- 백엔드 실무에서는 캐시, 세션 저장소, Queue, Rate Limiting, 분산 Lock 등에 자주 사용됩니다.
➕ 2-1. Redis를 사용하는 대표 목적
| 용도 | 설명 |
|---|
| 캐싱 | 자주 조회되는 데이터를 임시 저장 |
| 세션 저장 | 로그인 세션 정보를 저장 |
| Queue | BullMQ 같은 작업 대기열 저장소 |
| Rate Limiting | API 요청 횟수 제한 |
| 분산 Lock | 동시에 하나의 작업만 처리하도록 제어 |
| 임시 데이터 저장 | 인증번호, 토큰, 일회성 값 저장 |
✅ 3. 캐싱이 적합한 데이터
- 모든 데이터를 캐싱하는 것이 좋은 것은 아닙니다.
- 자주 조회되고, 자주 바뀌지 않으며, 약간 오래된 데이터가 보여도 큰 문제가 없는 데이터가 캐싱에 적합합니다.
➕ 3-1. 캐싱하기 좋은 데이터
- 메인 페이지 배너 목록
- 상품 카테고리 목록
- 인기 상품 목록
- 이벤트 페이지 설정
- FAQ 목록
- 공지사항 목록
- 요금제 목록
- 통신사 정책 요약
- 광고 랜딩 페이지 데이터
- 관리자 대시보드 통계 일부
- 외부 API 조회 결과
예시:
메인 배너 목록은 자주 조회되지만 자주 바뀌지 않음
↓
캐싱하기 좋음
➕ 3-2. 캐싱에 조심해야 하는 데이터
- 결제 상태
- 주문 상태
- 실시간 재고
- 사용자 포인트
- 관리자 권한
- 개인정보
- 인증번호
- 로그인 토큰
- 사전예약 선착순 카운트
주의:
정확성이 매우 중요한 데이터는 캐싱을 신중하게 적용해야 함
- 캐시된 오래된 데이터가 사용자에게 보이면 문제가 되는 데이터는 캐시 만료, 무효화, DB 검증을 함께 설계해야 합니다.
✅ 4. 캐시의 기본 흐름
➕ 4-1. Cache Aside 패턴
- 실무에서 가장 흔히 사용하는 캐싱 패턴입니다.
- 먼저 캐시를 확인하고, 캐시에 없으면 DB를 조회한 뒤 캐시에 저장합니다.
1. 요청 들어옴
2. Redis 캐시 확인
3. 캐시에 있으면 캐시 데이터 반환
4. 캐시에 없으면 DB 조회
5. DB 결과를 Redis에 저장
6. 응답 반환
➕ 4-2. 흐름 예시
GET /api/banners/main
↓
Redis key: banners:main 확인
↓
있음: Redis 데이터 반환
없음: DB에서 배너 목록 조회
↓
Redis에 5분 저장
↓
응답 반환
✅ 5. Cache Hit과 Cache Miss
➕ 5-1. Cache Hit
- 요청한 데이터가 캐시에 있는 상태입니다.
- DB를 조회하지 않고 빠르게 응답할 수 있습니다.
Redis에 banners:main 있음
↓
DB 조회 없이 바로 반환
➕ 5-2. Cache Miss
- 요청한 데이터가 캐시에 없는 상태입니다.
- DB를 조회한 뒤 결과를 캐시에 저장합니다.
Redis에 banners:main 없음
↓
DB 조회
↓
Redis에 저장
↓
응답 반환
➕ 5-3. 캐시 적중률
- 캐시 적중률(Cache Hit Rate)은 전체 요청 중 캐시에서 처리된 요청 비율입니다.
전체 요청 1000개
캐시 Hit 800개
↓
캐시 적중률 80%
- 캐시 적중률이 높으면 DB 부하가 줄고 응답 속도가 좋아집니다.
- 하지만 무조건 높다고 좋은 것은 아닙니다. 오래된 데이터를 보여주는 캐시라면 문제가 될 수 있습니다.
✅ 6. TTL
- TTL(Time To Live)은 캐시 데이터가 얼마나 오래 유지될지 정하는 시간입니다.
- TTL이 지나면 Redis에서 해당 데이터가 만료됩니다.
➕ 6-1. TTL 예시
메인 배너 목록:
5분 캐싱
카테고리 목록:
30분 캐싱
공지사항 목록:
10분 캐싱
외부 API 결과:
1분 캐싱
➕ 6-2. TTL을 짧게 잡아야 하는 데이터
- 자주 바뀌는 데이터
- 이벤트 상태
- 가격/지원금 정보
- 정책 정보
- 관리자에서 수정 직후 반영되어야 하는 데이터
➕ 6-3. TTL을 길게 잡아도 되는 데이터
- 변경이 거의 없는 카테고리
- 고정 FAQ
- 정적 설정값
- 코드 테이블
- 공개 이미지 URL 목록
TTL이 너무 짧으면:
캐시 효과가 작음
TTL이 너무 길면:
오래된 데이터가 계속 보일 수 있음
✅ 7. Redis Key 설계
- Redis는 Key-Value 구조이기 때문에 Key 이름 설계가 중요합니다.
- Key 이름이 엉망이면 나중에 삭제, 조회, 디버깅이 어려워집니다.
➕ 7-1. 좋은 Key 예시
banners:main
products:list:category:iphone
products:detail:123
faqs:list
settings:public
admin:dashboard:summary:2026-06-23
➕ 7-2. Key 설계 기준
- 서비스 영역을 앞에 붙인다.
- 데이터 종류를 구분한다.
- 파라미터 값을 포함한다.
- 너무 길거나 복잡하게 만들지 않는다.
- 개인정보를 key에 직접 넣지 않는다.
- 운영/개발 환경을 분리한다.
좋은 예시:
prod:products:detail:123
나쁜 예시:
01012345678_user_private_data
✅ 8. NestJS에서 Redis 캐시 사용 흐름
- NestJS에서는 Redis 클라이언트를 직접 사용하거나, CacheModule을 사용할 수 있습니다.
- 실무에서는 복잡한 캐시 무효화나 key 관리가 필요하면 Redis 클라이언트를 직접 감싼 CacheService를 만드는 방식이 관리하기 좋습니다.
➕ 8-1. CacheService 예시
@Injectable()
export class CacheService {
constructor(private readonly redis: Redis) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.redis.get(key);
if (!value) {
return null;
}
return JSON.parse(value) as T;
}
async set<T>(key: string, value: T, ttlSeconds: number) {
await this.redis.set(key, JSON.stringify(value), 'EX', ttlSeconds);
}
async del(key: string) {
await this.redis.del(key);
}
}
get은 Redis에서 값을 가져옵니다.
set은 JSON 문자열로 변환해서 TTL과 함께 저장합니다.
del은 특정 key를 삭제합니다.
✅ 9. 상품 목록 캐싱 예시
➕ 9-1. 캐시 적용 전
async getProducts(category: string) {
return this.prisma.product.findMany({
where: {
category,
isVisible: true,
},
orderBy: {
createdAt: 'desc',
},
});
}
- 사용자가 상품 목록을 볼 때마다 DB를 조회합니다.
- 트래픽이 많으면 DB 부하가 커질 수 있습니다.
➕ 9-2. 캐시 적용 후
async getProducts(category: string) {
const cacheKey = `products:list:category:${category}`;
const cached = await this.cacheService.get(cacheKey);
if (cached) {
return cached;
}
const products = await this.prisma.product.findMany({
where: {
category,
isVisible: true,
},
orderBy: {
createdAt: 'desc',
},
});
await this.cacheService.set(cacheKey, products, 60 * 5);
return products;
}
- 첫 요청은 DB를 조회합니다.
- 이후 5분 동안은 Redis에서 데이터를 가져옵니다.
- 상품 목록처럼 자주 조회되는 API에 적용하면 효과가 큽니다.
✅ 10. 캐시 무효화
- 캐시 무효화(Cache Invalidation)는 오래된 캐시 데이터를 삭제하거나 갱신하는 작업입니다.
- 캐싱에서 가장 어려운 부분 중 하나가 바로 무효화입니다.
➕ 10-1. 무효화가 필요한 상황
- 상품 정보 수정
- 배너 노출 여부 변경
- 이벤트 상태 변경
- FAQ 수정
- 공지사항 등록
- 관리자 설정 변경
- 가격/지원금 정보 변경
상품 수정:
DB에는 새 데이터 저장됨
↓
Redis에는 이전 상품 데이터 남아 있음
↓
사용자에게 오래된 정보가 보임
➕ 10-2. 상품 수정 후 캐시 삭제 예시
async updateProduct(productId: number, dto: UpdateProductDto) {
const product = await this.prisma.product.update({
where: {
id: productId,
},
data: dto,
});
await this.cacheService.del(`products:detail:${productId}`);
await this.cacheService.del(`products:list:category:${product.category}`);
return product;
}
- 상품 상세 캐시와 상품 목록 캐시를 함께 삭제합니다.
- 다음 요청이 들어오면 DB에서 최신 데이터를 조회하고 다시 캐싱합니다.
✅ 11. 캐시 무효화 전략
➕ 11-1. TTL 기반
- 캐시가 일정 시간이 지나면 자동으로 만료되게 합니다.
장점:
구현이 단순함
단점:
TTL 동안 오래된 데이터가 보일 수 있음
- 배너, 상품 목록, FAQ처럼 조금 늦게 반영되어도 괜찮은 데이터에 적합합니다.
➕ 11-2. 수동 삭제
- 데이터가 수정될 때 관련 캐시를 직접 삭제합니다.
상품 수정
↓
상품 상세 캐시 삭제
상품 목록 캐시 삭제
- 최신 데이터 반영이 빠릅니다.
- 하지만 관련된 캐시 key를 빠뜨리면 오래된 데이터가 남을 수 있습니다.
➕ 11-3. 버전 기반 캐시
- key에 버전 값을 포함해서 새 버전의 캐시를 사용하게 만드는 방식입니다.
products:list:v1
products:list:v2
- 대규모 캐시 갱신이 필요할 때 유용합니다.
- 오래된 key 정리 전략이 필요합니다.
✅ 12. Cache Stampede
- Cache Stampede는 캐시가 만료된 순간 많은 요청이 동시에 DB로 몰리는 현상입니다.
- 트래픽이 많은 서비스에서 자주 문제가 됩니다.
➕ 12-1. 문제 상황
메인 배너 캐시 만료
↓
동시에 사용자 1,000명 접속
↓
1,000개 요청이 모두 DB 조회
↓
DB 부하 급증
➕ 12-2. 완화 방법
- TTL을 약간 랜덤하게 설정합니다.
- Lock을 사용해 한 요청만 DB를 조회하게 합니다.
- 백그라운드에서 미리 캐시를 갱신합니다.
- 중요한 데이터는 TTL을 너무 짧게 잡지 않습니다.
예시:
TTL을 항상 300초로 두지 않고
270초 ~ 330초 사이로 랜덤 설정
✅ 13. 캐시와 개인정보
- 캐시는 빠르지만, 개인정보를 잘못 저장하면 보안 문제가 됩니다.
- 특히 공용 캐시에 사용자별 민감정보를 저장할 때는 매우 조심해야 합니다.
➕ 13-1. 캐싱에 조심해야 할 정보
- 전화번호
- 이름
- 주민등록번호
- 인증번호
- Access Token
- Refresh Token
- 결제 정보
- 관리자 권한 정보
- 고객 상담 메모
나쁜 예시:
consult:01012345678
user:token:eyJhbGciOi...
➕ 13-2. 개인정보 캐싱 기준
- 꼭 필요한 경우에만 저장한다.
- TTL을 짧게 설정한다.
- key에 개인정보를 직접 넣지 않는다.
- 암호화나 마스킹을 고려한다.
- 접근 권한을 제한한다.
- 로그에 캐시 값이 출력되지 않게 한다.
✅ 14. Redis를 세션 저장소로 사용하기
- 서버가 여러 대가 되면 메모리 세션을 서버 내부에 저장하는 방식이 문제가 됩니다.
- 사용자가 A 서버에서 로그인했는데 다음 요청이 B 서버로 가면 로그인 상태를 찾지 못할 수 있습니다.
- 이때 Redis를 세션 저장소로 사용할 수 있습니다.
➕ 14-1. 구조
사용자 로그인
↓
API 서버
↓
Redis에 세션 저장
다음 요청
↓
어떤 API 서버로 가도 Redis에서 세션 조회
➕ 14-2. Redis 세션의 장점
-
서버 여러 대에서 세션 공유 가능
-
세션 만료 시간 관리 가능
-
강제 로그아웃 처리 가능
-
서버 재시작 후에도 세션 유지 가능
-
단, JWT 기반 인증을 사용한다면 Redis 세션이 반드시 필요한 것은 아닙니다.
-
Refresh Token 관리, 블랙리스트, 관리자 강제 로그아웃 등에 Redis를 사용할 수 있습니다.
✅ 15. Redis를 Rate Limiting에 사용하기
- Rate Limiting은 특정 사용자나 IP가 일정 시간 안에 너무 많은 요청을 보내지 못하도록 제한하는 기능입니다.
- 로그인 시도, 인증번호 발송, 상담 신청, 검색 API 등에 필요합니다.
➕ 15-1. 인증번호 발송 제한 예시
같은 전화번호로 인증번호 발송:
1분에 1회
하루 5회까지만 허용
➕ 15-2. Redis Key 예시
rate:sms:phone:hashValue
rate:login:ip:123.123.123.123
rate:consult:ip:123.123.123.123
- 전화번호를 그대로 key에 넣기보다 해시값을 사용하는 것이 좋습니다.
- Redis의 TTL을 사용하면 일정 시간 뒤 자동으로 제한 카운트를 초기화할 수 있습니다.
✅ 16. Redis 운영 시 주의점
➕ 16-1. Redis는 메모리 기반이다
- Redis는 매우 빠르지만 메모리를 사용합니다.
- 너무 많은 데이터를 무제한으로 넣으면 메모리가 부족해질 수 있습니다.
나쁜 예시:
모든 주문 목록을 무기한 Redis에 저장
좋은 예시:
자주 조회되는 공개 데이터만 TTL과 함께 저장
➕ 16-2. Redis 장애 대비
- Redis가 장애 나면 캐시 조회가 실패할 수 있습니다.
- 캐시 서버가 죽었다고 서비스 전체가 죽으면 안 됩니다.
캐시 조회 실패
↓
DB 조회로 fallback
↓
서비스는 계속 동작
- 단, Redis를 Queue나 세션 저장소로 쓰고 있다면 장애 영향이 더 커질 수 있습니다.
- 캐시 용도와 핵심 인프라 용도를 구분해서 생각해야 합니다.
➕ 16-3. 캐시에 의존한 비즈니스 로직 주의
- 캐시는 보조 저장소입니다.
- 중요한 원본 데이터는 DB에 있어야 합니다.
좋지 않은 구조:
사전예약 신청 수를 Redis에만 저장
더 안전한 구조:
DB에 원본 저장
Redis는 빠른 조회나 임시 카운트 용도로 사용
✅ 17. 실무 체크리스트
➕ 17-1. 캐시 적용 체크리스트
- 이 데이터가 자주 조회되는가?
- DB 조회 비용이 큰가?
- 약간 오래된 데이터가 보여도 괜찮은가?
- TTL을 몇 초로 둘지 정했는가?
- 데이터 수정 시 캐시 무효화가 가능한가?
- 캐시 key 규칙이 명확한가?
- 개인정보가 캐시에 들어가지 않는가?
- Redis 장애 시 DB fallback이 가능한가?
➕ 17-2. Redis 운영 체크리스트
- Redis 메모리 사용량을 확인하는가?
- TTL 없는 key가 무제한 쌓이지 않는가?
- 운영/개발 Redis가 분리되어 있는가?
- Redis 접속 권한이 제한되어 있는가?
- Queue와 Cache 용도를 같은 Redis에서 쓴다면 부하를 확인하는가?
- 중요한 데이터 원본을 Redis에만 저장하지 않는가?
- 장애 시 서비스가 어떻게 동작할지 정해져 있는가?
➕ 17-3. 캐시 무효화 체크리스트
- 상품 수정 시 상품 상세 캐시가 삭제되는가?
- 상품 목록 캐시도 함께 삭제되는가?
- 배너 수정 시 메인 배너 캐시가 삭제되는가?
- 관리자 설정 변경 시 공개 설정 캐시가 갱신되는가?
- TTL만 믿고 너무 오래된 데이터를 보여주지 않는가?
- 캐시 key를 빠뜨려서 일부 화면만 오래된 데이터가 보이지 않는가?
✅ 18. AI를 활용해 Redis 캐싱을 설계할 때 질문법
- Redis 캐싱을 AI에게 물어볼 때는 어떤 데이터를 캐싱할지, 얼마나 자주 바뀌는지, 오래된 데이터 허용 여부를 같이 알려줘야 합니다.
- 단순히 “Redis 붙여줘”라고 하면 잘못된 곳에 캐시를 넣을 수 있습니다.
➕ 18-1. 좋은 질문 예시
NestJS + Prisma + PostgreSQL 서비스에서 Redis 캐싱을 적용하고 싶어.
상황:
1. 고객 메인 페이지에서 배너 목록, 인기 상품, FAQ를 조회함
2. 배너와 FAQ는 관리자에서 가끔 수정됨
3. 인기 상품은 10분 단위로 바뀌어도 괜찮음
4. 상품 상세는 수정 직후 최대한 빨리 반영되어야 함
5. 개인정보가 포함된 상담 신청 데이터는 캐싱하고 싶지 않음
6. Redis 장애 시 DB 조회로 fallback하고 싶음
요청:
- 어떤 API에 캐시를 적용하면 좋은지
- Redis key 이름 설계
- TTL 추천
- 관리자 수정 시 캐시 무효화 방식
- NestJS CacheService 예시
- 주의해야 할 보안 이슈
를 설명해줘.
➕ 18-2. AI 답변 검증 기준
- 모든 데이터를 무조건 캐싱하라고 하지 않는가?
- 자주 조회되고 자주 바뀌지 않는 데이터 위주로 추천하는가?
- 개인정보 캐싱 주의점을 설명하는가?
- TTL과 수동 무효화를 함께 고려하는가?
- Redis 장애 시 fallback을 설명하는가?
- 캐시 key 규칙을 명확히 제안하는가?
- 오래된 데이터가 보여도 되는지 기준을 나누는가?
📌 요약
- 캐싱은 자주 조회되는 데이터를 임시 저장소에 보관해 응답 속도를 높이고 DB 부하를 줄이는 전략입니다.
- Redis는 메모리 기반 Key-Value 저장소로, 캐시, 세션, Queue, Rate Limiting, Lock 등에 사용됩니다.
- 캐싱은 메인 배너, 카테고리, FAQ, 상품 목록, 공개 설정값처럼 자주 조회되고 자주 바뀌지 않는 데이터에 적합합니다.
- 결제 상태, 주문 상태, 포인트, 개인정보, 인증 정보처럼 정확성과 보안이 중요한 데이터는 캐싱을 신중하게 적용해야 합니다.
- 실무에서 자주 쓰는 방식은 먼저 Redis를 조회하고, 없으면 DB를 조회한 뒤 Redis에 저장하는 Cache Aside 패턴입니다.
- 캐시에는 TTL이 필요하며, 데이터가 수정될 때 관련 캐시를 삭제하는 무효화 전략도 함께 설계해야 합니다.
- Redis key는 서비스 영역, 데이터 종류, 파라미터를 기준으로 명확하게 설계해야 하며, key에 개인정보를 직접 넣으면 안 됩니다.
- Redis 장애가 발생해도 DB 조회로 fallback할 수 있도록 설계해야 캐시 장애가 전체 서비스 장애로 번지는 것을 막을 수 있습니다.