캐시의 핵심 목적
응답 속도 향상
DB 부하 감소
트래픽 및 외부 API 비용 절감
캐시 히트(Hit) 시에는 메모리에서 바로 응답하므로 매우 빠름.
캐시 미스(Miss) 시에만 DB를 조회한다.
캐시 기본 구조
요청(Request)
↓
[ Cache ] → HIT → 즉시 응답
↓
MISS
↓
[ DB ] → 조회 후 캐시에 저장 → 응답
DB와 캐시의 역할 차이
| 구분 | DB | Cache |
|---|---|---|
| 목적 | 정확성, 정합성 | 속도, 효율 |
| 저장 | 영구 저장 | 임시 저장 |
| 속도 | 상대적으로 느림 | 매우 빠름 |
| 데이터 | 항상 최신 | 최신 보장 X |
비유로 이해하기
DB = 책상 서랍
→ 안전하지만 꺼내는 데 시간이 걸림
Cache = 책상 위
→ 바로 사용 가능하지만 오래되면 다시 확인 필요
Cache Hit / Miss
| 용어 | 설명 | 성능 |
|---|---|---|
| Cache Hit | 캐시에 데이터 존재 | 매우 빠름 |
| Cache Miss | 캐시에 없음 → DB 조회 | 느림 (1회성) |
예시
DB 응답: 약 50ms
Cache 응답: 1~3ms
➡ 약 15~50배 성능 차이
캐시가 필요한 이유
캐시가 없을 때
반복적인 DB 조회
외부 API 호출 지연
트래픽 증가 시 서버 병목 발생
캐시 도입 효과
응답 속도 대폭 향상
DB 및 외부 API 부하 감소
대량 트래픽 대응 가능
캐시 유형
로컬 캐시 : 애플리케이션 내부 메모리 [ConcurrentHashMap, Caffeine]
분산 캐시 : 외부 서버에서 공유 [Redis, Memcached]
로컬 캐시
매우 빠름
서버 간 데이터 공유 불가
분산 캐시
네트워크 비용 존재
여러 서버에서 캐시 공유 가능
캐시 전략(Cache Strategy)
| 전략 | 설명 | 특징 |
|---|---|---|
| Write-through | DB와 캐시 동시 갱신 | 일관성 ↑ |
| Write-back | 캐시에 먼저 쓰고 나중에 DB 반영 | 성능 ↑, 위험 ↑ |
| Cache-aside | 요청 시 캐시 확인 후 DB 조회 | 가장 일반적 |
| Cache Invalidation | 데이터 변경 시 캐시 삭제 | 일관성 핵심 |
요청 → 캐시 확인
↓
없음 → DB 조회
↓
캐시에 저장 (TTL 설정)
↓
응답 반환
캐시 일관성 문제
문제 상황
주요 원인
TTL이 너무 김
DB 업데이트 시 캐시 무효화 누락
TTL & Eviction Policy
TTL (Time-To-Live)
캐시 데이터의 유효 시간
만료 시 자동 삭제
SET key value EX 300 # 300초 유지
Eviction Policy (삭제 정책)
LRU: 최근 사용 안 한 데이터 제거
LFU: 사용 빈도 낮은 데이터 제거
FIFO: 먼저 들어온 데이터 제거
실무 캐시 활용 사례
| 서비스 | 캐시 대상 | 특징 |
|---|---|---|
| 뉴스 메인 | 인기 기사 목록 | 짧은 TTL |
| 상품 상세 | 상품 정보 | DB 접근 최소화 |
| 환율 조회 | 외부 API 응답 | 주기적 갱신 |
| 가게 리스트 | 지역별 리스트 | Key 분리 |
캐싱 설계 시 주의점
캐시는 DB의 보조 수단
TTL은 너무 짧아도, 길어도 문제
변경 잦은 데이터보다 조회 많은 데이터에 적용
캐시 Key 설계 명확히
user:42
post:1001
요약
캐시는 “정확성을 희생하지 않는 선에서 성능을 극대화하기 위한 도구”이며,
읽기 많은 시스템에서 필수적인 인프라이다.
Spring Cache
핵심 포인트
캐시 구현체와 비즈니스 로직 분리
동일한 코드로 로컬 캐시 ↔ Redis 전환 가능
캐시 접근 로직을 AOP 방식으로 처리
구조 개념
@Cacheable / @CacheEvict / @CachePut
↓
[ Spring Cache Abstraction ]
↓
CacheManager
↓
Cache 구현체 (Caffeine, Redis 등)
기본 설정
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.github.ben-manes.caffeine:caffeine'
@SpringBootApplication
@EnableCaching // 캐시 기능 활성화
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@EnableCaching 없으면
→ 어노테이션이 있어도 캐시 동작 안 함
캐시 설정 (application.yml)
spring:
cache:
caffeine:
spec: maximumSize=100,expireAfterWrite=10s
최대 캐시 엔트리: 100개
데이터 생성 후 10초 뒤 만료
TTL 동작 시나리오 이해
| 시점 | 동작 | 상태 |
|---|---|---|
| 0초 | 첫 조회 | DB 조회 + 캐시 저장 |
| 5초 | 재조회 | Cache HIT |
| 10초 | TTL 만료 도달 | 아직 삭제 X |
| 11초 | 재조회 | 캐시 만료 → DB 재조회 |
expireAfterWrite 기준
조회를 계속해도 TTL은 연장되지 않음
만료 후 접근 시 새 캐시 생성
@Cacheable — 조회 결과 캐싱
@Cacheable(value = "postCache", key = "#postId")
public PostDto getPostById(long postId) {
log.info("캐시에 없으니 DB에서 직접 조회");
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("등록된 포스트가 없습니다."));
return PostDto.from(post);
}
동작 방식
첫 호출 → DB 조회 후 캐시에 저장
동일 파라미터 재호출 → DB 접근 없이 캐시 반환
@Cacheable의 value & key 구조
value (Cache Name)
캐시 저장소의 이름
하나의 캐시 그룹(namespace)
@Cacheable(value = "postCache", key = "#postId")
key (Entry Key)
캐시 내부에서 데이터를 구분하는 식별자
메서드 파라미터 기반으로 생성
Cache name: postCache
Key: 1
Value: PostDto(...)
CacheManager
┣━━ userCache
│ ┗━━ key: 1 → UserDto
┣━━ postCache
│ ┣━━ key: 10 → PostDto
│ ┗━━ key: 11 → PostDto
┗━━ commentCache
┗━━ key: 101 → CommentDto
value & key 설계 기준
@Cacheable(value = "postCache", key = "#postId")
@Cacheable(value = "postCache", key = "#username")
postCache::1
postCache::"1"
@Cacheable(value = "postCache", key = "'id:' + #postId")
@Cacheable(value = "postCache", key = "'username:' + #username")
postCache::id:1
postCache::username:kim
캐시 Key 네이밍 규칙
기본 원칙
고유하고 일관된 형식
도메인 중심 설계
user:1
post:1001
search:keyword:page
실무 팁
: 또는 :: 구분자 사용
Redis 사용 시 prefix 기반 관리
운영 환경에서는 짧지만 의미 명확하게
@CacheEvict — 캐시 삭제
@CacheEvict(value = "postCache", key = "#postId") public void deletePost(long postId) { // 게시글 삭제 로직 }
사용 시점
삭제
상태 변경
더 이상 캐시가 유효하지 않을 때
@CachePut — 캐시 갱신
@CachePut(value = "postCache", key = "#postId")
public PostDto updatePost(long postId) {
// 게시글 수정 로직
}
특징
항상 메서드 실행
실행 결과를 캐시에 강제로 갱신
조회용(@Cacheable)과 역할 분리
요약
Spring Cache는 캐시 구현체와 비즈니스 로직을 분리해
어노테이션만으로 일관성 있고 안전한 캐싱을 가능하게 해준다.
디스크가 아닌 메모리 기반 → 매우 빠른 읽기/쓰기
주 용도: 캐시, 세션, 실시간 데이터 처리
RDB vs Key-Value DB (Redis)
| 구분 | RDB (MySQL 등) | Redis |
|---|---|---|
| 데이터 구조 | 테이블 (행/열) | Key - Value |
| 조회 방식 | SQL (복잡한 조건 가능) | Key 기반 단일 조회 |
| 저장 위치 | 디스크 | 메모리 |
| 속도 | 상대적으로 느림 | 매우 빠름 |
| 용도 | 영구 데이터 저장 | 캐시, 세션, 실시간 처리 |
Redis가 빠른 이유
| 항목 | 디스크 기반 DB | Redis |
|---|---|---|
| 데이터 위치 | 디스크 | 메모리 |
| 접근 방식 | 파일 I/O | CPU 직접 접근 |
| 응답 속도 | ms 단위 | μs 단위 |
| 안정성 | 매우 높음 | 서버 종료 시 휘발 |
Redis의 주요 특징
| 특징 | 설명 |
|---|---|
| In-memory | 모든 데이터를 메모리에 저장 |
| Key-Value | 단순하고 빠른 구조 |
| 자료구조 제공 | String, List, Set, Hash, ZSet 등 |
| TTL 지원 | Key 단위 만료 시간 설정 가능 |
Redis 자료형과 활용 사례
String
가장 기본적인 타입
문자열, 숫자 모두 저장 가능
활용
게시글/상품 캐싱
로그인 토큰
조회수, 좋아요 수 카운팅
set viewCount 10
incr viewCount
List
순서가 있는 데이터 구조
Queue / Stack 용도로 활용 가능
활용
실시간 채팅 메시지
최근 본 상품 목록
주문 대기열
lpush recent_posts "post1"
rpush recent_posts "post2"
lrange recent_posts 0 -1
Set
활용
좋아요 누른 사용자 목록
태그 관리
팔로워 / 팔로잉 관계
sadd tags "spring"
sadd tags "redis"
smembers tags
Hash
활용
사용자 정보 캐싱
상품 상세 정보
로그인 세션 데이터
hset user:1 name "Ravi" age 30
hgetall user:1
Sorted Set (ZSet)
활용
랭킹 시스템
인기 게시글 순위
실시간 순위 갱신
zadd ranking 100 "ravi"
zrevrange ranking 0 -1 WITHSCORES
Redis 데이터 영속성 (Persistence)
| 방식 | 설명 | 특징 |
|---|---|---|
| RDB | 주기적으로 전체 스냅샷 저장 | 빠름, 백업 용도 |
| AOF | 실행 명령어를 순차 기록 | 안정성 높음 |
| Hybrid | RDB + AOF 혼합 | Redis 7.x 기본 |
Redis 사용 시 주의사항
Redis는 주 저장소가 아니다
메모리 기반 → 장애 시 데이터 유실 가능
반드시 DB와 함께 사용해야 안정적
이상적인 구조
원본 데이터 : MySQL, PostgreSQL
빠른 접근 : Redis
조합 : DB는 진실, Redis는 복사본
요약
Redis는 초고속 메모리 기반 Key-Value 저장소로,
DB의 부하를 줄이고 실시간 처리를 가능하게 해주는 캐시 서버이다.
RedisTemplate
즉,
우리는 Java 코드로 set, get을 호출하고
RedisTemplate이 이를 Redis 명령어(SET, GET 등) 로 변환해 서버에 전달한다.
Redis에서 받은 결과는 다시 Java 객체로 변환되어 반환된다.
RedisTemplate 동작 흐름
[ Java 코드 ]
"user:1 = Ravi" 저장 요청
↓
[ RedisTemplate ]
SET user:1 "Ravi"
↓
[ Redis 서버 ]
메모리에 저장
↓
[ 결과 반환 ]
간단한 예제
redisTemplate.opsForValue().set("user:1", "Ravi"); String name = (String) redisTemplate.opsForValue().get("user:1"); System.out.println(name); // Ravi
SET user:1 "Ravi"
GET user:1
RedisTemplate 주요 역할 정리
| 메서드 | Redis 자료형 | Redis 명령 |
|---|---|---|
| opsForValue() | String | SET / GET |
| opsForHash() | Hash | HSET / HGET |
| opsForList() | List | LPUSH / LRANGE |
| opsForSet() | Set | SADD / SMEMBERS |
| opsForZSet() | Sorted Set | ZADD / ZRANGE |
TTL(Time To Live) 설정
redisTemplate.opsForValue()
.set("temp:data", "123", 10, TimeUnit.MINUTES);
SET temp:data "123" EX 600
Cache-Aside 패턴 (읽기 캐싱)
기본 원칙
Read: 캐시 → DB → 캐시 저장
Write: DB 변경 후 캐시 삭제(Evict)
캐시 없는 버전
public Post getPost(Long postId) {
return postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("Post not found"));
}
public Post getPost(Long postId) {
String key = PREFIX + postId;
// 1. 캐시 확인
Post cached = (Post) redisTemplate.opsForValue().get(key);
if (cached != null) {
System.out.println("Cache Hit");
return cached;
}
// 2. 캐시 미스 → DB 조회
Post post = postRepository.findById(postId)
.orElseThrow(() -> new RuntimeException("Post not found"));
// 3. 캐시에 저장
redisTemplate.opsForValue().set(key, post, 10, TimeUnit.MINUTES);
return post;
}
핵심 포인트
캐시에 있으면 DB 접근 ❌
없을 때만 DB 조회
조회 결과를 캐시에 저장
Redis 캐시 Key 네이밍 전략
{도메인}:{리소스}:{식별자}
| 용도 | Key 예시 | 설명 |
|---|---|---|
| 사용자 정보 | user:123 | userId=123 |
| 게시글 | post:987 | postId=987 |
| 사용자 피드 | feed:user:123 | 특정 사용자 피드 |
| 댓글 리스트 | comment:post:987 | 게시글 댓글 |
| 랭킹 | ranking:game:weekly | 주간 랭킹 |
: 로 계층 구조 표현
고정 prefix로 역할 구분
Key만 봐도 의미 파악 가능하게
코드에서는 상수로 관리
private static final String USER_CACHE_PREFIX = "user:";
private static final String POST_CACHE_PREFIX = "post:";
TTL 설계 전략
TTL은 캐시의 생명주기다.
예시
redisTemplate.opsForValue()
.set("post:1", post, 10, TimeUnit.MINUTES);
10분 후 자동 만료
데이터별 TTL 예시
| 데이터 | TTL | 이유 |
| ------- | ------ | ------ |
| 사용자 프로필 | 1시간~1일 | 변경 적음 |
| 게시글 상세 | 5~30분 | 수정 가능성 |
| 실시간 인기글 | 1~5분 | 빠른 변동 |
| 로그인 세션 | 10~60분 | 보안 |
| 통계/대시보드 | 5분 이내 | 최신성 중요 |
TTL 설계 팁
변경 주기 ⬆ → TTL 짧게
조회 빈도 ⬆ & 변경 적음 → 캐싱 효율 ↑
TTL 너무 짧음 ❌ → 캐시 효과 감소
TTL 너무 김 ❌ → 데이터 불일치 위험
요약
RedisTemplate은 Java 코드로 Redis 자료구조와 명령을 다룰 수 있게 해주는 도구이며,
Cache-Aside 패턴과 함께 사용하면 읽기 성능을 크게 향상시킬 수 있다.
Spring Boot + Redis 연동 목적
Redis를 캐시 서버로 사용해 조회 성능 개선
DB 부하 감소
실무에서 가장 많이 쓰이는 Cache-Aside 패턴 직접 구현
의존성 추가
// Redis 연동 implementation 'org.springframework.boot:spring-boot-starter-data-redis'
spring-boot-starter-data-redis
RedisConnectionFactory
RedisTemplate
Lettuce 클라이언트 기본 제공
RedisTemplate 설정 (중요 ⭐)
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory
) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key는 문자열
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value는 JSON (LocalDateTime 대응)
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
설정 이유
StringRedisSerializer : Redis Key를 사람이 읽을 수 있게
GenericJackson2JsonRedisSerializer : 객체(JSON) 직렬화 + LocalDateTime 대응
Object 타입 : 다양한 도메인 객체 캐싱 가능
이 설정 없으면
→ 직렬화 깨짐 / LocalDateTime 에러 자주 발생
Cache-Aside 기본 캐싱
핵심 개념
읽기(Read): 캐시 → DB → 캐시 저장
쓰기(Write): DB 수정 후 캐시 삭제
게시글 조회 흐름
[Client]
↓
[Controller]
↓
[Service]
↓
1. Redis 캐시 조회
↓
2. Cache Miss → DB 조회
↓
3. Redis 캐시에 저장
↓
4. 응답 반환
[Client PUT 요청]
↓
1. DB 데이터 수정
↓
2. Redis 캐시 삭제 (Invalidate)
조회수 기반 인기 게시글 (Sorted Set)
왜 ZSet을 쓸까?
자동 정렬
점수(score) 기반 순위 관리
실시간 랭킹에 최적화
게시글 조회 시 흐름
[게시글 조회 요청]
↓
1. Redis 캐시 조회
↓
2. Cache Miss → DB 조회 후 캐시 저장
↓
3. ZSet 조회수 +1 (ZINCRBY)
↓
4. 게시글 응답
조회수는 DB에 바로 반영하지 않고
→ Redis에서 실시간 집계
인기 게시글 조회 흐름
[인기글 조회 요청]
↓
1. ZSet에서 상위 N개 postId 조회 (ZREVRANGE)
↓
2. postId 기반 캐시 조회
↓
3. 캐시 없으면 DB 조회 후 Redis 저장
↓
4. 조회수 순서대로 PostDto 반환
이 구조의 장점
조회수 증가 = Redis 연산 (초고속)
인기글 조회 시 DB 정렬 ❌
대규모 트래픽에서도 성능 유지
전체 구조 요약
실무 관점에서 배울 점
RedisTemplate은 저수준 제어에 적합
Spring Cache(@Cacheable)는 선언적 캐싱
랭킹, 카운팅은 Redis 자료구조가 압도적으로 유리
캐시 설계는 “어디까지 Redis를 믿을지” 결정하는 작업
요약
Spring Boot에서 Redis를 연동해 Cache-Aside 패턴과 Sorted Set을 활용하면,
조회 성능 개선과 실시간 랭킹을 동시에 만족하는 구조를 만들 수 있다.
(;′⌒`) 괜찮아요 ! 저는 오늘 순공시간이 세시간 반이였는걸요!