Remote Dictionary Server의 약자로, Key-Value 구조의 비정형 데이터를 저장하고 관리하기 위한 DBMS이다. 인메모리 방식의 데이터 저장소로, 일반적인 DB에 비해 속도가 빠르다.
레디스 서버를 따로 설정하고 서버의 모든 세션 데이터를 저장하는 방식으로 인증/인가에 활용한다. 서버 자체 자원을 소모하지 않으며 서버간 인증/인가 데이터를 공유하여 사용할 수 있어 효율적이다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
data:
redis:
host: localhost
port: 6379
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가지 방법을 통해 사용할 수 있다.
Repository를 이용하면 엔티티를 만들어서 쉽게 사용 가능하다.
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
: 만료시간을 설정한다.(초 단위)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~
구문을 사용할 수 있다.
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);
}
}
레디스 템플릿은 사용하는 자료구조마다 제공하는 메서드가 다르기 때문에 객체를 만들어서 레디스의 자료구조 타입에 맞는 메서드를 사용하면 된다.
메서드 명 | 레디스 타입 |
---|---|
opsForValue | String |
opsForList | List |
opsForSet | Set |
opsForZSet | Sorted Set |
opsForHash | Hash |
데이터 저장시 만료 시간을 지정할 경우, 해당 시간의 단위까지 지정해주면 된다.
캐시란, 한 번 처리한 데이터를 임시 저장소에 저장해 임시 데이터에 대한 동일/유사 요청이 올 경우 임시 저장소에 바로 조회하여 응답해 성능 및 응답속도를 향상시킬 수 있는 기술이다.
대규모 서비스에서 캐싱은 필수라고 할 수 있다. 다만, 별도의 연산 수행 없이 동일 응답 값을 전달해도 되는 대상을 선정하고, 어떤 기준으로 캐시를 적용할 지, 캐시의 유지 기간(TTL), 저장 공간 등 고려 사항이 많다.
다음 예제를 통해 Spring에 Redis를 연동하여 캐싱 기능을 구현하는 방법을 알아보자.
package com.example.springallinoneproject.cache_test.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class CacheTestDTO {
private String id;
private String content;
}
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");
}
}
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);
}
}
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);
}
}
첫 호출시에는 서비스 메서드가 호출된다. 하지만 이후 호출에는 서비스까지 가지 않고 캐싱된 값이 제공된다.