← Back to list

Spring Data Redis: Repository vs RedisTemplate — 실전 성능 비교

들어가며: Redis Repository 프로덕션 장애에서 배운 교훈

Philip Park in 여기어때 기술블로그 · 2025-12-08 15:19 · 151 claps · 32.2 min read
Open on Medium ↗

Spring Data Redis: Repository vs RedisTemplate — 실전 성능 비교

글. 박성진(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 명령이 실행됩니다:

  1. HMSET — 1,600개 필드 저장 (상당한 데이터 전송량)
  2. 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:조이하우스]

새로운 객체를 저장하면:

  1. 해당 객체의 키(ID)가 StayPdp Set에 추가
  2. 삭제 시 Set에서 제거

이렇게 하면 SMEMBERSSCARD 등을 통해 전체 키 목록이나 개수를 빠르게 조회할 수 있습니다.

@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) 같은 파생 쿼리 호출 시:
  1. 미리 구축된 인덱스(Set 또는 Sorted Set)에서 ID 목록 조회
  2. 다시 실제 해시를 조회

요컨대, 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 사용 전 체크리스트:

  1. ❓ 정말로 count(), findAll()이 필요한가?
  2. @Indexed 검색이 필수 기능인가?
  3. ❓ 부가 메모리 사용과 성능 저하를 감수할 가치가 있는가?
  4. ❓ 데이터 규모가 관리 가능한 수준인가?

하나라도 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: 단순 캐싱참고 자료

참고 사례

예제 코드

  • GitHub: 추후 제공

긴 글 읽어주셔서 감사합니다 :)


메타데이터
post_id
3e1a6ab8bda3
slug
spring-data-redis-repository-vs-redistemplate-실전-성능-비교-3e1a6ab8bda3
url
https://techblog.gccompany.co.kr/spring-data-redis-repository-vs-redistemplate-%EC%8B%A4%EC%A0%84-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-3e1a6ab8bda3
canonical_url
https://techblog.gccompany.co.kr/spring-data-redis-repository-vs-redistemplate-%EC%8B%A4%EC%A0%84-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90-3e1a6ab8bda3
author_url
https://medium.com/@philip_park
status
ok
fetched_at
2026-07-09 09:01:30