Backend
Spring Data Redis: Repository vs RedisTemplate — 실전 성능 비교
Philip Park여기어때
2025년 12월 9일
원문에서 보기 ↗글. 박성진(Philip) / 전시개발팀

들어가며: Redis Repository 프로덕션 장애에서 배운 교훈
안녕하세요 여기어때 전시 개발 팀에서 백엔드를 담당하고 있는 필립입니다.
전시 개발 팀에서는 Redis 를 운영하는 과정에서 두 번의 심각한 장애를 겪었습니다. 이번 시리즈에서는 그 중 하나인 Redis CPU 100% 이슈를 해결한 경험을 공유하려 합니다.
피크 타임마다 Redis CPU 사용률이 100%에 도달해 서비스 응답이 느려지고 타임아웃이 발생했던 문제를 어떻게 분석하고 해결했는지 실제 코드와 모니터링 데이터를 바탕으로 살펴보겠습니다.
(현재): Repository vs RedisTemplate — Redis CPU 100% 장애 해결기
발생한 문제

**피크 타임(오후 9시~12시)**마다 Redis CPU가 계속해서 85%를 이상을 찍다가 7월 성수기에 100%을 찍었고, 그 결과 전체 서비스 응답 시간이 평소보다 3~5배까지 증가했습니다. 심지어 일부 요청에서는 타임아웃 에러까지 발생하는 상황이었습니다.
모니터링 결과:
- 오후 10시~12시: Redis CPU 지속적으로 100% 유지
- 응답 시간: 정상 대비 3~5배 증가
- 간헐적인 타임아웃 에러 발생
원인 분석
해당 API 에서는 성능 향상을 위해 Redis 캐시를 도입했고, **Spring Data Redis의 CrudRepository (@RedisHash)**를 사용했습니다. 초기엔 문제가 없었지만 시간이 지남에 따라 다음과 같은 변화가 있었습니다:
- 데이터 규모 증가: 제휴점 하나당 캐시되는 데이터의 크기가 커짐
- 사용자 증가: 동시 접속자 수가 급증하여 캐시 저장 요청 빈도 상승
- Repository 저장 방식의 부하: Spring Data Redis Repository가 내부적으로 한 번 저장에 여러 Redis 명령을 수행하면서 부하 누적 → 결국 100% 돌파
특히 Spring Data Redis의 CrudRepository는 객체를 Hash 자료구조로 변환해 저장하며, 보조 인덱스용 Set도 관리합니다.
데이터가 복잡해질수록 한 번의 저장에 매우 많은 필드가 HMSET으로 전송되고, 부가로 SADD 등의 명령이 추가됩니다. 그 결과 Redis 엔진이 처리해야 할 작업량이 폭증해 CPU가 포화된 것이 근본 원인이었습니다.
해결 방향
CrudRepository에서 RedisTemplate로 전환한 결과:
- Redis CPU 사용률: 100% → 10% 수준으로 감소
- 응답 시간: 정상 수준으로 회복
- 안정적인 서비스 운영 가능
핵심 결론 먼저 보기
Repository 방식:
- Redis 명령어: HMSET + SADD 등 최소 2개 이상
- 성능: ️ 느림, CPU 부하 높음
RedisTemplate 방식:
- Redis 명령어: SET 단 1개
- 성능: 빠름, CPU 효율적
이 글에서는 실제 프로덕션 코드와 Redis Monitor 로그를 통해 두 방식의 차이를 명확히 보여드립니다.
1. 도메인 모델 정의
숙박 상품 PDP 데이터를 Redis에 캐싱한다고 가정합니다.
실제 프로덕션에서는 객체 깊이와 필드 수가 훨씬 많습니다. 여기서는 이해를 돕기 위해 숙소 제휴점 정보 + 객실 최저가 정도로 간소화한 예제입니다.
StayPdp 모델
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
@RedisHash("StayPdp")
public record StayPdp(
@Id Long placeId,
String placeName,
Double reviewRate,
Integer reviewCount,
PlaceLowestProductPrice placeLowestProductPrice
) {}
public record PlaceLowestProductPrice(
StayLowestProductPrice stayLowestProductPrice,
boolean shortStay,
boolean overnight
) {}
public record StayLowestProductPrice(
int discountPrice,
long roomId,
boolean haveEliteDiscountPolicy
) {}
중첩된 구조의 도메인 객체 — 실제 서비스에서 흔히 볼 수 있는 형태입니다.
실제 프로덕션 객체는 훨씬 더 복잡했습니다..
실제로 캐싱하던 객체는 다음과 같은 구조였습니다:
StayPdp (숙박 상품 상세)
├─ 기본 정보 (id, name, category 등): 10개 필드
├─ roomGroups (14개 객실 그룹):
│ └─ 각 그룹당:
│ ├─ 기본 정보: 10개
│ ├─ stayInfo: 20개
│ ├─ additionalInfo: 15개
│ ├─ facilityInfo: 10개
│ └─ bedInfoList (14종류): 42개
│ → 그룹당 약 100개 필드
│ → 14개 × 100 = 1,400개 필드
├─ filterRoomGroups (20개): 100개 필드
└─ 기타 (placeUseTime 등): 100개 필드
최대 Hash 필드 개수: 약 1,600~2,000개
Repository로 이 객체를 저장하면:
# HMSET 명령어에 1,600~2,000개 필드를 한번에 전송
HMSET "StayPdp:72244"
"_class" "com.example.StayPdp"
"id" "72244"
"roomGroups[0].roomId" "454425"
"roomGroups[0].roomName" "[20시 레이트 체크인] 랜덤 배정"
"roomGroups[0].stayInfo.checkInHour" "20"
"roomGroups[0].stayInfo.checkOutHour" "12"
"roomGroups[0].stayInfo.strikePrice" "40000"
"roomGroups[0].stayInfo.discountPrice" "40000"
"roomGroups[0].facilityInfo.bedInfoList[0].type" "1"
"roomGroups[0].facilityInfo.bedInfoList[0].count" "0"
"roomGroups[0].facilityInfo.bedInfoList[1].type" "2"
... (1,600개 필드 계속)
# 그 다음 SADD
SADD "StayPdp" "72244"
위 명령어들은 단일 객체 저장 시 실행되는 것입니다. 실제 운영 환경에서 피크 타임에 초당 수백 건의 캐시 저장이 발생하면서, Redis CPU가 100%까지 치솟았던 이유를 알 수 있었습니다.
2. Repository 방식 구현
Repository 인터페이스
import com.example.rediscompare.model.StayPdp;
import org.springframework.data.repository.CrudRepository;
public interface StayPdpRepository extends CrudRepository<StayPdp, Long> {}
코드는 간결하지만, 내부적으로는 복잡한 작업이 수행됩니다.
실제 Redis Monitor 로그
1765191739.585064 [0 127.0.0.1:63700] "HMSET" "StayPdp:88146"
"_class" "com.example.demo.rediscompare.model.StayPdp"
"placeId" "88146"
"placeLowestProductPrice.overnight" "1"
"placeLowestProductPrice.shortStay" "0"
"placeLowestProductPrice.stayLowestProductPrice.discountPrice" "16290"
"placeLowestProductPrice.stayLowestProductPrice.haveEliteDiscountPolicy" "0"
"placeLowestProductPrice.stayLowestProductPrice.roomId" "752543"
"placeName" "\xed\x99\x8d\xeb\x8c\x80..."
"reviewCount" "26"
"reviewRate" "90.0"
1765191739.588857 [0 127.0.0.1:63700] "SADD" "StayPdp" "88146"
위는 간단한 예제 객체 기준입니다.
실제 프로덕션 객체(roomGroups 14개 포함)는:
HMSET "StayPdp:72244"
"_class" "..."
"id" "72244"
"categoryTypeValue" "HOTEL"
"roomGroups[0].objectId" "m_room_group_454425"
"roomGroups[0].roomId" "454425"
"roomGroups[0].roomName" "[20시 레이트 체크인] 랜덤 배정"
"roomGroups[0].roomServiceType" "1"
"roomGroups[0].stayInfo.checkInHour" "20"
"roomGroups[0].stayInfo.checkOutHour" "12"
"roomGroups[0].stayInfo.strikePrice" "40000"
"roomGroups[0].stayInfo.discountPrice" "40000"
"roomGroups[0].stayInfo.stockCount" "3"
"roomGroups[0].additionalInfo.basePersonalCount" "2"
"roomGroups[0].additionalInfo.maxPersonalCount" "2"
"roomGroups[0].facilityInfo.bedInfoList[0].type" "1"
"roomGroups[0].facilityInfo.bedInfoList[0].count" "0"
"roomGroups[0].facilityInfo.bedInfoList[1].type" "2"
"roomGroups[0].facilityInfo.bedInfoList[1].count" "1"
... (계속해서 1,600개 필드)
"roomGroups[13].facilityInfo.bedInfoList[13].count" "0"
"filterRoomGroups[0].roomGroupId" "454437"
... (계속)
→ 단 1개의 HMSET 명령에 1,600~2,000개 필드
한 번의 save() 호출에 최소 2개 이상의 Redis 명령이 실행됩니다:
HMSET— 1,600개 필드 저장 (상당한 데이터 전송량)SADD— 키 목록에 추가
@Indexed 필드를 사용하면 추가 명령이 더 발생합니다.
3. RedisTemplate 방식 구현
Cache 클래스
import com.example.rediscompare.model.StayPdp;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
@Repository
@RequiredArgsConstructor
public class StayPdpCache {
private final RedisTemplate<String, StayPdp> redisTemplate;
public void save(StayPdp pdp) {
redisTemplate.opsForValue().set(
"staypdp:" + pdp.placeId(),
pdp,
Duration.ofMinutes(10)
);
}
public StayPdp get(Long id) {
return redisTemplate.opsForValue().get("staypdp:" + id);
}
}
실제 Redis Monitor 로그
1765191789.358857 [0 127.0.0.1:63700] "SET" "staypdp:88146"
"{\"placeId\":88146,
\"placeName\":\"홍대...\",
\"reviewRate\":90.0,
\"reviewCount\":26,
\"placeLowestProductPrice\":{
\"stayLowestProductPrice\":{
\"discountPrice\":16290,
\"roomId\":752543,
\"haveEliteDiscountPolicy\":false
},
\"shortStay\":false,
\"overnight\":true
}
}"
"EX" "600"
단 1개의 SET 명령으로 저장과 TTL 설정이 동시에 처리됩니다.
4. 성능 비교: 실제로 얼마나 차이날까?
두 방식의 성능 차이를 확인하기 위해 간단한 JMH 벤치마크 테스트를 작성했습니다.
Redis 연산은 기본적으로 빠르지만, Spring Data Redis Repository는 내부적으로 보조 인덱스 관리, Hash 변환, Reflection 기반 직렬화 등 여러 추가 작업을 수행하기 때문에 실제로 어느 정도의 오버헤드가 발생하는지 수치로 검증해보고자 했습니다.
이번 벤치마크에서는 다음과 같은 세 가지 시나리오를 측정했습니다.
✔ 1) 저장(Save)
객체 하나를 Redis에 저장하는 연산을 측정했습니다.
Repository는 Hash 저장 + Set 인덱스 업데이트가 추가로 발생하고,
RedisTemplate은 단순 set(key, value) 방식으로 저장합니다.
✔ 2) 조회(Get)
Redis에서 key로 객체를 읽어오는 연산을 측정했습니다.
Repository는 역직렬화 후 매핑 과정이 추가되며,
RedisTemplate은 JSON 역직렬화만 수행합니다.
✔ 3) 저장 + 조회(Save + Get)
실제 서비스 캐싱에서 발생하는 패턴이라 두 연산을 연달아 수행하는 비용도 함께 측정했습니다.
두 방식의 오버헤드 차이가 누적될 때 어떤 차이가 나타나는지 판단하기 위한 항목입니다.
JMH 벤치마크 설정
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Warmup(iterations = 1, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Fork(1)
public class RedisBenchmark {
private ConfigurableApplicationContext context;
private StayPdpRepository repository;
private StayPdpCache cache;
private StayPdp testData;
@Setup(Level.Trial)
public void setup() {
// Spring Boot 애플리케이션 컨텍스트 시작
context = SpringApplication.run(DemoApplication.class,
"--spring.profiles.active=test",
"--spring.data.redis.host=localhost",
"--spring.data.redis.port=6379");
repository = context.getBean(StayPdpRepository.class);
cache = context.getBean(StayPdpCache.class);
// 테스트 데이터 생성
testData = new StayPdp(
999L,
"벤치마크 테스트 호텔",
4.5,
100,
new PlaceLowestProductPrice(
new StayLowestProductPrice(89000, 12345L, false),
false,
true
)
);
}
@TearDown(Level.Trial)
public void tearDown() {
if (context != null) {
context.close();
}
}
@Benchmark
public void saveViaRepository() {
repository.save(testData);
}
@Benchmark
public void saveViaTemplate() {
cache.save(testData);
}
@Benchmark
public StayPdp getViaRepository() {
return repository.findById(999L).orElse(null);
}
@Benchmark
public StayPdp getViaTemplate() {
return cache.get(999L);
}
@Benchmark
public void saveAndGetViaRepository() {
repository.save(testData);
repository.findById(999L);
}
@Benchmark
public void saveAndGetViaTemplate() {
cache.save(testData);
cache.get(999L);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(RedisBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
벤치마크 결과
Benchmark Mode Cnt Score Error Units
RedisBenchmark.saveViaRepository avgt 5 2271.554 ± 2129.441 us/op
RedisBenchmark.saveViaTemplate avgt 5 925.581 ± 916.906 us/op
RedisBenchmark.getViaRepository avgt 5 2034.616 ± 2634.211 us/op
RedisBenchmark.getViaTemplate avgt 5 1508.530 ± 1935.112 us/op
RedisBenchmark.saveAndGetViaRepository avgt 5 3290.914 ± 2300.012 us/op
RedisBenchmark.saveAndGetViaTemplate avgt 5 3191.825 ± 1834.103 us/op
성능 비교 (마이크로초, 낮을수록 좋음)
저장 (Save) 작업:
- Repository: 2,271.554 μs
- RedisTemplate: 925.581 μs
- 성능 차이: 🔥 2.45배 빠름
조회 (Get) 작업:
- Repository: 2,034.616 μs
- RedisTemplate: 1,508.530 μs
- 성능 차이: ⚡ 1.35배 빠름
저장+조회 작업:
- Repository: 3,290.914 μs
- RedisTemplate: 3,191.825 μs
- 성능 차이: ⚡ 1.03배 빠름
결과 분석
1. 저장 작업에서 가장 큰 차이 (2.45배)
이유:
# Repository: 최소 2개 명령
HMSET StayPdp:999 ... # ~1000μs
SADD StayPdp 999 # ~100μs
+ Spring Data 오버헤드 # ~1000μs
# RedisTemplate: 1개 명령
SET staypdp:999 ... # ~900μs
Repository는 여러 Redis 명령 + 추상화 레이어 오버헤드로 인해 훨씬 느립니다.
2. 조회 작업도 차이 존재 (1.35배)
# Repository
HGETALL StayPdp:999 # Hash 전체 조회
+ 역직렬화 오버헤드
# RedisTemplate
GET staypdp:999 # 단일 값 조회
+ 역직렬화
조회는 둘 다 1개 명령이지만, Hash 전체를 가져오는 HGETALL이 GET보다 무겁습니다.
3. 복합 작업에서는 차이 감소 (1.03배)
저장+조회를 함께 하면 네트워크 왕복 시간(RTT)이 전체 시간의 큰 비중을 차지하기 때문에, Repository의 추가 명령어 영향이 상대적으로 줄어듭니다.
측정 결론
명확한 결론:
- 저장 작업: Repository가 확실히 느림 (2.45배)
- 조회 작업: RedisTemplate이 더 빠름 (1.35배)
- 트렌드: 일관되게 RedisTemplate이 우수
실제 프로덕션 환경에서:
- 동시 요청 수 증가 시 차이는 더 벌어짐
- CPU 부하로 인한 누적 효과
- 우리 팀 경험: Redis CPU 100% → 10%로 감소
메모리 사용 비교
Repository 방식
# Redis CLI에서 확인
127.0.0.1:6379> KEYS StayPdp*
1) "StayPdp:999" # Hash (실제 데이터)
2) "StayPdp" # Set (키 목록)
3) "StayPdp:999:idx" # Set (인덱스 관리)
127.0.0.1:6379> MEMORY USAGE StayPdp:999
(integer) 1248
127.0.0.1:6379> MEMORY USAGE StayPdp
(integer) 96
총 메모리: 데이터 + 부가 구조
RedisTemplate 방식
bash
127.0.0.1:6379> KEYS staypdp:*
1) "staypdp:999" # String (데이터만)
127.0.0.1:6379> MEMORY USAGE staypdp:999
(integer) 856
총 메모리: 데이터만
차이:
- Repository: ~1,400 bytes (데이터 + Set + 인덱스)
- RedisTemplate: ~860 bytes (데이터만)
- 약 1.6배 메모리 차이
수십만 건의 데이터를 캐싱하면 수 GB의 메모리 차이 발생
실제 프로덕션 영향
Before (Repository)
- 피크 타임: Redis CPU 100%
- 평균 응답 시간: 150ms
- 간헐적 타임아웃 발생
After (RedisTemplate)
- 피크 타임: Redis CPU 10%
- 평균 응답 시간: 50ms
- 안정적인 서비스 운영
개선율:
- CPU 사용률: 90% 감소****
- 응답 시간: 3배 개선
- 안정성: 타임아웃 에러 제거, 레디스 CPU 안정화
5. 내부 동작 원리 분석
Spring Data Redis Repository의 편의성과 그 대가
Spring Data Redis는 RDB의 JPA처럼 Redis에서도 Repository 형태로 데이터를 다룰 수 있도록 CrudRepository 인터페이스를 제공합니다.
public interface StayPdpRepository extends CrudRepository<StayPdp, Long> {
// 메서드 정의 없이도 기본 CRUD 사용 가능
}
이를 사용하면 도메인 객체에 @RedisHash 어노테이션만 달면 손쉽게 save(), findById(), delete() 등의 CRUD 연산을 수행할 수 있습니다.
하지만 이러한 편의 뒤에는 Redis의 한계를 보완하기 위한 여러 부가 작업이 숨어있습니다.
🔍 부가 작업 1: 보조 Set 및 인덱스 생성
Key-Value DB인 Redis는 기본적으로 Primary Key(키)로만 접근이 가능 합니다. count()(전체 개수)나 특정 필드로 조회하는 기능이 없죠.
Spring Data Redis Repository는 이러한 한계를 극복하기 위해, 모든 키를 보관하는 Set 과 필드 인덱스를 추가로 활용합니다.
Redis에 생성되는 구조
# 1. 실제 데이터 (Hash)
StayPdp:88146
# 2. 전체 키 목록 (Set)
StayPdp → [60931, 88147, 88148, ...]
# 3. @Indexed 필드 사용 시 인덱스 (Set/Sorted Set)
StayPdp:placeName:조이 → [60931]
StayPdp:60931:idx → [StayPdp:placeName:조이하우스]
새로운 객체를 저장하면:
- 해당 객체의 키(ID)가
StayPdpSet에 추가 - 삭제 시 Set에서 제거
이렇게 하면 SMEMBERS나 SCARD 등을 통해 전체 키 목록이나 개수를 빠르게 조회할 수 있습니다.
@Indexed를 사용하면?
@RedisHash("Product")
public class Product {
@Id
private Long id;
@Indexed // 추가 Set 생성
private String category;
private String name;
}
도메인 객체 필드에 @Indexed를 지정하면:
- 별도의 인덱스 키(
Product:category:electronics같은 형태)가 만들어짐 - 그 안에 해당 객체의 ID가 저장됨
findByCategory(String category)같은 파생 쿼리 호출 시:
- 미리 구축된 인덱스(Set 또는 Sorted Set)에서 ID 목록 조회
- 다시 실제 해시를 조회
요컨대, Repository를 사용할 때 Redis에는 우리의 데이터 이외에 부가기능을 위한 자료구조들이 더 생기게 됩니다.
부가 작업 2: 저장 시 다중 Redis 명령 실행
CrudRepository.save()를 호출하여 객체를 저장하는 내부 과정을 살펴봅시다.
실제 실행되는 명령어 순서
# 1. 기존 데이터가 있다면 삭제
DEL "StayPdp:60931"
# 2. Hash에 객체 필드들 저장
HMSET "StayPdp:60931"
"_class" "com.example.demo.rediscompare.model.StayPdp"
"placeId" "88146"
"placeName" "조이하우스"
"reviewRate" "90.0"
"reviewCount" "26"
...
# 3. 키 목록 Set에 ID 추가
SADD "StayPdp" "60931"
# 4. @Indexed 필드가 있다면 인덱스 Set에 ID 추가
SADD "StayPdp:placeName:조이하우스" "60931"
# 5. 인덱스 키 목록 관리 (삭제 대비)
SADD "StayPdp:60931:idx" "StayPdp:placeName:조이하우스"
위와 같이 5개 이상의 Redis 명령이 연쇄적으로 실행되어야 객체 한 개 저장이 완료됩니다.
반면, 같은 객체를 Redis에 단순 캐시하는 경우에는 SET 한 번이면 끝납니다.
조회(findById) 연산도 복잡합니다
# Repository 방식
HGETALL "StayPdp:88146" # Hash 전체 조회
# + 필요에 따라 존재 여부 확인, TTL 체크 등 추가
# RedisTemplate 방식
GET "staypdp:88146" # 단일 조회
Repository 추상화의 편의성 대가로 많은 네트워크 왕복과 Redis 연산이 발생합니다.
명령어 수 비교
저장 작업:
- Repository: SISMEMBER + DEL + HMSET + SADD (최소 4개)
- RedisTemplate: SET (1개)
- @Indexed 사용 시 더 증가
조회 작업:
- Repository: HGETALL (1개)
- RedisTemplate: GET (1개)
- 동일하지만 HGETALL이 더 무거움
삭제 작업:
- Repository: HGETALL + DEL + SREM + … (3개 이상)
- RedisTemplate: DEL (1개)
- 인덱스 정리 포함
카운트:
- Repository: SCARD (1개) — Repository만 가능
- RedisTemplate: ❌ 불가능
RedisTemplate의 단순함
Key → Value (직렬화된 객체)
RedisTemplate은 필요한 데이터만 저장하고 조회합니다. 추가적인 자료구조나 부가 명령어 없이 단순하게 동작합니다.
핵심 요약
Repository 방식:
- ✅ 편리한 인터페이스 (
count(),findAll(), 파생 쿼리) - ❌ 많은 Redis 명령어 실행
- ❌ 부가 메모리 사용 (Set, 인덱스)
- ❌ 네트워크 왕복 증가
RedisTemplate 방식:
- ✅ 최소한의 Redis 명령어
- ✅ 메모리 효율적
- ✅ 빠른 성능
- ❌ 직접 구현 필요 (
count()등)
6. 실무 가이드
🤔 실제로 언제 무엇을 써야 할까?
일반적으로 캐싱 목적이라면 Spring Data Redis Repository 대신 **RedisTemplate 또는 Spring Cache (@Cacheable)**를 쓰는 것이 권장됩니다. Repository는 편리하지만 앞서 본 대로 추가 부하와 오버헤드가 큽니다. 아래에 각 방식을 사용해야 하는 경우를 정리합니다.
나쁜 예: 단순 캐시에 Repository 사용
@RedisHash("ProductCache")
public class Product {
@Id private Long id;
private String name;
// ...
}
좋은 예: RedisTemplate 또는 @Cacheable
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productService.findById(id);
}
이런 경우 Repository는 불필요한 오버헤드를 발생시킵니다:
- ✗ API 응답 캐싱 (PDP, 상품 목록, 검색 결과 등)
- ✗ DB 조회 결과 캐싱
- ✗ 외부 API 호출 결과 캐싱
- ✗ 계산 결과 캐싱
- ✗ 단순 세션 데이터
Repository를 써도 되는 특수한 경우
1. Redis를 실제 데이터베이스로 사용하는 경우
캐시가 아니라 Redis가 Primary Storage일 때:
// 예: 실시간 투표 시스템
@RedisHash("Vote")
public class Vote {
@Id
private String id;
@Indexed // 사용자별 투표 조회
private Long userId;
@Indexed // 투표 항목별 집계
private String pollId;
private LocalDateTime votedAt;
}
// 필요한 이유: 복잡한 쿼리가 필수
List<Vote> findByUserId(Long userId);
List<Vote> findByPollIdAndVotedAtAfter(String pollId, LocalDateTime since);
long countByPollId(String pollId);
특징:
- 데이터 영구 보관 (TTL 없음)
- 다양한 조건으로 조회 필요
count(),exists()빈번하게 사용- 규모가 작고 관리 가능한 수준 (수만 건 이하)
2. 메타데이터/설정 관리 시스템
// 예: Feature Flag 관리
@RedisHash("FeatureFlag")
public class FeatureFlag {
@Id
private String flagName;
@Indexed
private String environment; // dev, staging, prod
private boolean enabled;
private String description;
}
// 필요한 이유: 환경별로 그룹 조회
List<FeatureFlag> findByEnvironment(String env);
boolean existsByFlagName(String name);
특징:
- 소량의 설정 데이터 (수백~수천 건)
- 그룹별 조회 필요
- 자주 변경되지 않음
- 관리 인터페이스 구현 용이
3. 임시 작업 큐/상태 관리
// 예: 배치 작업 상태 추적
@RedisHash(timeToLive = 86400) // 24시간
public class BatchJob {
@Id
private String jobId;
@Indexed
private String status; // PENDING, RUNNING, COMPLETED, FAILED
@Indexed
private LocalDateTime createdAt;
private int progress;
}
// 필요한 이유: 상태별 작업 목록 조회
List<BatchJob> findByStatus(String status);
List<BatchJob> findByCreatedAtAfter(LocalDateTime since);
long countByStatus(String status); // 대시보드용
특징:
- TTL로 자동 정리
- 상태별 집계/모니터링 필요
- 실시간 대시보드 제공
- 임시 데이터 (영구 저장은 DB에)
RedisTemplate/Cacheable을 써야 하는 경우 (대부분)
1. 모든 캐싱 시나리오
// API 응답 캐싱
@Cacheable(value = "pdp", key = "#placeId")
public StayPdp getStayPdp(Long placeId) {
return stayService.getDetail(placeId);
}
// 계산 결과 캐싱
@Cacheable(value = "recommendations", key = "#userId")
public List<Product> getRecommendations(Long userId) {
return recommendationEngine.calculate(userId);
}
// 외부 API 캐싱
@Cacheable(value = "weather", key = "#city", ttl = 1800)
public WeatherInfo getWeather(String city) {
return externalWeatherApi.fetch(city);
}
2. 대량 데이터 캐싱
// 수십만 ~ 수백만 건의 캐시 데이터
public class ProductCacheRepository {
private final RedisTemplate<String, Product> redisTemplate;
public void cache(Product product) {
redisTemplate.opsForValue().set(
"product:" + product.getId(),
product,
Duration.ofHours(1)
);
}
}
Repository 사용 시 문제:
- 수백만 개의 키를 담은 거대한 Set 생성
- 메모리 낭비 + 성능 저하
count()호출도 느려짐
3. 고성능이 중요한 경우
// 피크 타임 트래픽 처리
@Service
public class HighTrafficCacheService {
private final RedisTemplate<String, Object> redisTemplate;
// 1ms 이내 응답 필요
public Product getProduct(Long id) {
return (Product) redisTemplate.opsForValue()
.get("product:" + id);
}
}
우리 팀의 경험:
- Repository 사용 시: Redis CPU 100% → 응답 지연
- RedisTemplate 전환 후: CPU 10% → 정상 응답
핵심 원칙
캐싱이 목적이라면 Spring Cacheable을, 복잡한 쿼리가 필요한 특수한 경우에만 Repository를 사용하는 것이 권장됩니다.
Repository 사용 전 체크리스트:
- ❓ 정말로
count(),findAll()이 필요한가? - ❓
@Indexed검색이 필수 기능인가? - ❓ 부가 메모리 사용과 성능 저하를 감수할 가치가 있는가?
- ❓ 데이터 규모가 관리 가능한 수준인가?
하나라도 NO라면 RedisTemplate 사용을 권장합니다.
7. 모니터링과 성능 최적화
Redis Monitor로 실제 명령어 확인하기
# Redis CLI 접속
redis-cli
# Monitor 모드 실행
MONITOR
# 애플리케이션에서 저장 작업 수행 후 로그 확인
확인 포인트:
- 예상보다 많은 명령어가 실행되는가?
- SADD, SREM 같은 Set 연산이 불필요하게 발생하는가?
- TTL이 제대로 설정되는가?
Redis 키 구조 확인하기
# Repository 방식
KEYS StayPdp*
# 결과: StayPdp:88146, StayPdp, StayPdp:88147, ...
TYPE StayPdp:88146 # → hash
TYPE StayPdp # → set
# RedisTemplate 방식
KEYS staypdp:*
# 결과: staypdp:88146, staypdp:88147, ...
TYPE staypdp:88146 # → string
성능 최적화 체크리스트
현재 Repository를 사용 중이라면 다음을 확인하세요:
- ☐ 필요하지 않은 기능 사용 중?
count(),findAll()실제로 사용하는가?@Indexed필드가 정말 필요한가?- ☐ Redis Monitor 로그 확인
- 예상보다 많은 명령어가 실행되는가?
- SADD/SREM이 불필요하게 발생하는가?
- ☐ 메모리 사용량 모니터링
- Set 구조로 인한 추가 메모리 사용 확인
INFO memory명령으로 메모리 사용 추적- ☐ 응답 시간 측정
- Repository vs Template 벤치마크
- P50, P95, P99 레이턴시 비교
위 항목 중 하나라도 문제가 있다면 RedisTemplate 전환을 고려해볼 필요가 있습니다.
마치며
핵심 정리
Redis 명령:
- Repository: 2개 이상
- RedisTemplate: 1개
메모리:
- Repository: Hash + Set
- RedisTemplate: String만
성능:
- Repository: 상대적으로 느림
- RedisTemplate: 빠름
사용 난이도:
- Repository: 쉬움
- RedisTemplate: 보통
추천 상황:
- Repository: 복잡한 쿼리 필요
- RedisTemplate: 단순 캐싱참고 자료
참고 사례
- Hyperconnect — Spring Data Redis Repository 미숙하게 사용해 발생한 장애 극복기
- Salesforce — Lessons Learned using Spring Data Redis
예제 코드
- GitHub: 추후 제공
긴 글 읽어주셔서 감사합니다 :)