solyrion

Spring에서의 Redis 본문

Redis

Spring에서의 Redis

ert1015 2025. 9. 1. 18:32

개요

실제 Redis를 Spring 서버에 어떻게 적용할 수 있는지 알아보겠습니다.


Redis 서버

  • AWS에서 제공하는 ElasticCache를 사용할 수 있습니다.
  • 로컬에서 사용한다면 간단하게 Docker를 활용해서 Redis 서버를 열 수 있습니다.
  • (저는 개발: Docker, 배포: ElasticCache 방식으로 사용했습니다.)

Spring 연동 (의존성 + application.yml)

build.gradle

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'

// 추가적으로 필요한 의존성도 존재합니다.

application.yml

spring:
  data:
    redis:
      host: localhost
      port: 6379
      timeout: 2000
  cache:
    type: redis

RedisConfig

Redis는 기본적으로 메모리 기반 key-value 저장소이기 때문에 내부적으로 바이트 배열만 저장이 가능합니다.

따라서 자바 객체나 문자열을 그대로 Redis에 넣을 수 없기 때문에 직렬화, 역직렬화가 필요합니다.

Spring Boot에서는 기본적으로 JDK 직렬화를 제공합니다.

그러나 해당 방식은 저장된 값이 바이너리 형태이기 때문에 관리 차원에서 불편함이 존재하고, 클래스 구조가 조금만 바뀌어도 역직렬화 시 에러가 발생할 수 있습니다.

따라서 JSON 직렬화같이 성능, 안정성 면에서 유리한 직렬화 방식을 설정해줘야 합니다.

@Configuration
@EnableCaching // @Cacheable 쓸 계획이면
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory(
            @Value("${spring.data.redis.host}") String host,
            @Value("${spring.data.redis.port}") int port
    ) {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(cf);

        StringRedisSerializer str = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer json = new GenericJackson2JsonRedisSerializer();

        template.setKeySerializer(str);
        template.setValueSerializer(json);
        template.setHashKeySerializer(str);         
        template.setHashValueSerializer(json);       

        template.afterPropertiesSet();              // 초기화
        return template;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory cf){
        return new StringRedisTemplate(cf);
    }

    // @Cacheable을 Redis로 쓰고 싶을 때만
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory cf) {
        RedisCacheConfiguration cfg = RedisCacheConfiguration.defaultCacheConfig()
            .serializeKeysWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(
            RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .entryTtl(Duration.ofMinutes(10)); 
        return RedisCacheManager.builder(cf).cacheDefaults(cfg).build();
    }
}

위의 코드처럼 설정을 해 주면 아래와 같이 사용할 수 있습니다.

private final RedisTemplate<String, Object> redisTemplate

StringRedisTemplate 같은 경우는 <String, String> 타입의 Redis Template으로 스프링에서 편의를 위해 자체적으로 제공해 주는 객체입니다.

참고로 @Cacheable을 사용하기 위해서는 RedisCacheManager가 필요합니다.

위의 코드처럼 수동으로 Bean을 등록해도 되고 application.properties에서 설정할 수도 있습니다.

사용 예시

실제 서비스에서는 Redis를 이용해 인기글 목록, 세션/토큰, 랭킹 정보 등 여러 데이터를 빠르게 처리할 수 있습니다.

아래에서는 직접 캐싱과 애노테이션 기반 캐싱 두 가지 대표적인 사용 방법을 알아보겠습니다.

직접 캐싱

@Service
@RequiredArgsConstructor
public class PopularPostCacheService {
    private final RedisTemplate<String, Object> redis;
    private final PostRepository postRepository;

    private static final String KEY = "post:popular:v1";
    private static final Duration TTL = Duration.ofMinutes(5);

    public List<Long> getPopularPostIds() {
        // 1. Redis 캐시 확인
        List<Long> cached = (List<Long>) redis.opsForValue().get(KEY);
        if (cached != null) {
            return cached;
        }

        // 2. 캐시 미스 → DB 조회
        List<Long> ids = postRepository.findTop10IdsByOrderByLikeCountDesc();

        // 3. Redis에 저장 (TTL 함께)
        redis.opsForValue().set(KEY, ids, TTL);

        return ids;
    }

    public void evict() {
        redis.delete(KEY);
    }
}

가장 흔하게 쓰이는 인기글 캐싱입니다.

RedisConfig에서 List<Long> 타입이 아닌 Object으로 설정했기 때문에 .get()을 호출하는 부분에서 타입 캐스팅이 필요합니다.

보시면 RedisTemplate에 .get(), .set()을 통해 바로 접근하는 것이 아니라 .opsForValue()를 통해 접근하고 있습니다.

.opsForValue()는 내부적으로 ValueOperations를 반환합니다.

Value 뿐만 아니라 Hash, List, Set, ZSet 같이 여러 타입이 존재합니다.

즉, RedisTemplate은 모든 걸 다루는 일종의 게이트웨이고, 자료구조별로

전용 연산 집합(ValueOperations, HashOperations …)을 따로 제공받아서 사용한다고 생각하면 될 것 같습니다.

RedisTemplate의 opsForXXX() 정리표

메서드 반환 타입 대응되는 Redis 자료구조 대표 메서드 예시

opsForValue() ValueOperations<K,V> String (단일 값) set(key, value), get(key), increment(key)
opsForHash() HashOperations<H,HK,HV> Hash (Map 구조) put(hashKey, field, value), get(hashKey, field), entries(hashKey)
opsForList() ListOperations<K,V> List (큐/스택) leftPush(key, value), rightPop(key)
opsForSet() SetOperations<K,V> Set (집합) add(key, values...), members(key)
opsForZSet() ZSetOperations<K,V> Sorted Set (랭킹) add(key, value, score), range(key, start, end)

Annotation 캐싱

@Service
@RequiredArgsConstructor
public class PostService {
    private final PostRepository repo;

    @Cacheable(cacheNames = "postById", key = "#postId", unless = "#result == null")
    public PostDto findById(Long postId) {
        return repo.findDtoById(postId);
    }

    @CacheEvict(cacheNames = "postById", key = "#postId")
    public void updateTitle(Long postId, String title) {
        repo.updateTitle(postId, title);
    }
}

@Cacheable

  • 의미:
    • postById 라는 캐시 영역(캐시 이름)에 결과를 저장
    • 캐시 키 = postId
    • 즉, postById::1, postById::2 이런 식으로 Redis에 저장됩니다.
  • unless = "#result == null" → 결과가 null이면 캐시에 넣지 않음
  • 동작 순서:
    1. 메서드 호출 시 캐시에 값이 있는지 먼저 확인
    2. 있으면 DB 안 거치고 캐시 값 반환
    3. 없으면 실제 repo.findDtoById(postId) 실행 → 반환값을 캐시에 저장

예를 들어 findById(10) 을 처음 호출하면:

  • Redis Key: postById::10
  • Redis Value (JSON 직렬화)
{ "id": 10, "title": "Hello World", "content": "This is my first post", "author": "Alice" }

실제 Redis CLI에서는

127.0.0.1:6379> GET postById::10
"{\\"id\\":10,\\"title\\":\\"Hello World\\",\\"content\\":\\"This is my first post\\",\\"author\\":\\"Alice\\"}"

👉 DB 조회 결과를 캐시에 저장해서, 같은 게시글을 다시 조회할 때는 DB를 안 거치고 캐시에서 바로 꺼냅니다.

@CacheEvict

  • 의미:
    • postById 캐시 영역에서 postId 키에 해당하는 캐시 데이터를 삭제(Evict)
  • 동작 순서:
    1. 실제 DB에서 제목을 업데이트
    2. 해당 게시글의 캐시 데이터는 지워짐
    3. 다음번에 같은 findById(postId) 호출하면 DB 다시 조회 후 새 값으로 캐시에 저장

👉 즉, 캐시에 오래된 데이터(스테일 캐시) 가 남아있지 않도록 하는 역할이라고 볼 수 있습니다.


운영 환경 주의사항

  • TTL 설정
    • 캐시 만료 시간을 반드시 지정해 메모리 누수를 방지해야 합니다..
  • KEYS 사용 금지:
    • KEYS * 는 전체 키를 블로킹 조회해 성능에 치명적입니다 → 운영에서는 SCAN 명령으로 대체할 수 있습니다..
  • 캐시 스탬피드:
    • 인기 캐시가 동시에 만료되면 요청이 DB로 몰려 과부하가 발생할 수 있습니다 → 선제적 캐시 갱신, 분산락(Redisson), 랜덤 TTL 등으로 방지할 수 있습니다.

'Redis' 카테고리의 다른 글

Redis vs Memcached  (0) 2025.11.18
Redis Cluster  (0) 2025.10.09
Redis 캐시 만료시간 (TTL & Eviction)  (0) 2025.08.22
Redis란?  (1) 2025.08.08
Comments