Spring Boot, Redis 연동(+Redis Cache 활용)

Minjae An·2024년 1월 14일
0

Spring ETC

목록 보기
4/8

Redis란?

Remote Dictionary Server의 약자로, Key-Value 구조의 비정형 데이터를 저장하고 관리하기 위한 DBMS이다. 인메모리 방식의 데이터 저장소로, 일반적인 DB에 비해 속도가 빠르다.

특징

  • 데이터를 디스크가 아닌 메모리에서 처리하기 때문에 속도가 매우 빠르다.
  • String, Set, Sorted Set, Hash, List와 같이 다양한 데이터 타입을 지원한다.
  • 싱글 쓰레드 구조이기 때문에 처리 시간이 긴 요청이 들어올 경우 해당 요청 처리시까지 다른 요청을 응답하지 못한다.
  • 마스터 레디스 서버의 데이터를 슬레이브 레디스 서버에 복제할 수 있다.
  • 메시지 큐 용도로 사용할 수 있다.

사용 사례

레디스 서버를 따로 설정하고 서버의 모든 세션 데이터를 저장하는 방식으로 인증/인가에 활용한다. 서버 자체 자원을 소모하지 않으며 서버간 인증/인가 데이터를 공유하여 사용할 수 있어 효율적이다.

Redis 연동 설정

의존성 추가 - build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

application.yml

data:
    redis:
      host: localhost
      port: 6379

RedisConfig

package com.example.springallinoneproject.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String host;
    @Value("${spring.data.redis.port}")

    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

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

        // 일반적인 key:value의 경우 시리얼라이저
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        // Hash를 사용할 경우 시리얼라이저
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        // 모든 경우
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}

레디스는 크게 2가지 방법을 통해 사용할 수 있다.

RedisRepository 사용하기

Repository를 이용하면 엔티티를 만들어서 쉽게 사용 가능하다.

RefreshToken

package com.example.springallinoneproject.user.dto;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;
import org.springframework.data.redis.core.index.Indexed;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
@RedisHash(value = "refresh_token")
public class RefreshToken {
    @Id
    private String authId;

    @Indexed
    private String token;
    private String role;
    @TimeToLive
    private long ttl;

    public RefreshToken update(String token, long ttl) {
        this.token = token;
        this.ttl = ttl;
        return this;
    }
}
  • @Id : 키 값이 되며, 후술할 @RedisHash - value 에 prefix에 덧붙여져 위 예제의 경우 refresh_token:{id} 형태로 형성된다.
  • @RedisHash : value 속성을 통해 설정한 값을 키 값 prefix로 사용한다. timeToLive 속성을 사용할 수도 있다.
  • @Indexed : 해당 값으로 검색을 할 시에 추가한다.
  • @TimeToLive : 만료시간을 설정한다.(초 단위)

RefreshTokenRepository

package com.example.springallinoneproject.user.repository;

import com.example.springallinoneproject.user.dto.RefreshToken;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
    Optional<RefreshToken> findByToken(String token);

    Optional<RefreshToken> findByAuthId(String authId);
}

CrudRepository 를 상속 받아 구현할 수 있다. @Id 또는 @Indexed 어노테이션을 적용한 필드들만 해당 인터페이스가 제공하는 findBy~ 구문을 사용할 수 있다.

RedisTemplate 사용

RedisUtil

package com.example.springallinoneproject.util;

import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class RedisUtil {
    private final RedisTemplate<String, Object> redisTemplate;

    public void putData(String key, String value, long expiredTime) {
        redisTemplate.opsForValue().set(key, value, expiredTime, TimeUnit.MICROSECONDS);
    }

    public String getData(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    public void deleteData(String key) {
        redisTemplate.delete(key);
    }
}

레디스 템플릿은 사용하는 자료구조마다 제공하는 메서드가 다르기 때문에 객체를 만들어서 레디스의 자료구조 타입에 맞는 메서드를 사용하면 된다.

메서드 명레디스 타입
opsForValueString
opsForListList
opsForSetSet
opsForZSetSorted Set
opsForHashHash

데이터 저장시 만료 시간을 지정할 경우, 해당 시간의 단위까지 지정해주면 된다.

Redis 활용 API 캐싱 구현

API 캐시

캐시란, 한 번 처리한 데이터를 임시 저장소에 저장해 임시 데이터에 대한 동일/유사 요청이 올 경우 임시 저장소에 바로 조회하여 응답해 성능 및 응답속도를 향상시킬 수 있는 기술이다.

대규모 서비스에서 캐싱은 필수라고 할 수 있다. 다만, 별도의 연산 수행 없이 동일 응답 값을 전달해도 되는 대상을 선정하고, 어떤 기준으로 캐시를 적용할 지, 캐시의 유지 기간(TTL), 저장 공간 등 고려 사항이 많다.

다음 예제를 통해 Spring에 Redis를 연동하여 캐싱 기능을 구현하는 방법을 알아보자.

CacheTestDTO

package com.example.springallinoneproject.cache_test.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class CacheTestDTO {
    private String id;
    private String content;
}

CacheTestService

package com.example.springallinoneproject.cache_test.service;

import com.example.springallinoneproject.cache_test.dto.CacheTestDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class CacheTestService {
    public CacheTestDTO getResult(String id) {
        log.info("Call get Result");
        return new CacheTestDTO(id, id + "example content");
    }
}

CacheTestController

package com.example.springallinoneproject.cache_test.controller;

import com.example.springallinoneproject.cache_test.dto.CacheTestDTO;
import com.example.springallinoneproject.cache_test.service.CacheTestService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class CacheTestController {
    private final CacheTestService cacheTestService;

    @GetMapping("/result")
    public CacheTestDTO result(@RequestParam String id){
        return cacheTestService.getResult(id);
    }
}

Redis Cache 적용

Application start 클래스

@SpringBootApplication
@EnableCaching // 추가
public class SpringAllinoneProjectApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringAllinoneProjectApplication.class, args);
	}

}

application.yml

spring:
	cache:
		type: redis

캐싱 기능을 커스텀된 형태로 사용하기 위해 RedisConfig 에 Redis CacheManager 설정을 해주어야 한다. 캐싱할 API의 응답 값 타입이 String 이 아닌 예제처럼 다른 데이터 타입이라면 직렬화 에러가 발생한다. 거의 대부분의 API 서비스는 응답을 JSON 형태로 사용하고, 이 포맷을 지원할 수 있게 Jackson2JsonRedisSerializer 또는 GenericJackson2JsonRedisSerializer 로 변경해주어야 한다.

RedisConfig - cacheManager

@Bean
public CacheManager customCacheManager() {
    RedisCacheManagerBuilder builder = RedisCacheManagerBuilder
            .fromConnectionFactory(redisConnectionFactory());
    RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
            .serializeValuesWith(RedisSerializationContext.
                    SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
            .prefixCacheNameWith("test:") // key prefix 로 test: 를 사용
            .entryTtl(Duration.ofMinutes(30));
    builder.cacheDefaults(configuration);
    return builder.build();
}

@Cacheable 어노테이션을 Service 혹은 Controller에 추가하여 캐싱 사용을 등록할 수 있다.

@Cacheable(value = "CacheTestDTO", key = "#id", cacheManager = "customCacheManager", unless = "#id==''", condition = "#id.length > 2")
  • cacheName : 케시 이름(설정 메서드 리턴값이 저장되는)
  • value="CacheTestDTO" : 캐시 이름의 별칭
  • key=#id : 이 API에서 id 에 따라 응답값이 달라지므로 저장될 키로 id 파라미터 값을 선언, 동일한 캐시 이름을 사용하지만 구분될 필요가 있을 경우 사용되는 값
  • cacheManager=cacheManager : config 에서 작성한 cacheManager 사용
  • unless="#id==''" : id"" 일 때 캐시를 저장하지 않는다.
  • condition="#id.length>2" : id 의 길이가 3이상일 때만 캐시 저장
@RestController
@RequiredArgsConstructor
public class CacheTestController {
    private final CacheTestService cacheTestService;

    @Cacheable(value = "CacheTestDTO", key = "#id", cacheManager = "customCacheManager", unless = "#id==''", condition = "#id.length > 2")
    @GetMapping("/result")
    public CacheTestDTO result(@RequestParam String id) {
        return cacheTestService.getResult(id);
    }
}

실행 결과

첫 호출시에는 서비스 메서드가 호출된다. 하지만 이후 호출에는 서비스까지 가지 않고 캐싱된 값이 제공된다.

참고

profile
먹고 살려고 개발 시작했지만, 이왕 하는 거 잘하고 싶다.

0개의 댓글