Spring Boot와 Redis연동하여 사용하기
DB에 있는 다량의 데이터를 지속적으로 조회하면 서버에 큰 부하가 발생할 수 있다. 물론 DB 서버를 Scale-out하는 방식으로 서버의 부하 분산 처리를 진행할 수도 있지만, 자주 조회되는 데이터는 Redis에 캐시로 저장하여 사용하는 것이 좋은 방안 중 하나일 수 있다. Redis는 메모리 기반 데이터베이스로, 데이터를 빠르게 읽을 수 있어 데이터베이스에 직접 접근하는 횟수를 줄여준다. 이렇게 캐시로 활용함으로써 서버 부하를 줄이고, 애플리케이션의 응답 속도를 크게 향상시킬 수 있다.
Redis란?
Redis는 Remote Dictionary Server의 약자로, 오픈 소스 기반의 메모리 키-값 저장소이다. NoSQL 데이터베이스로, 관계형 데이터베이스(RDBMS)와 달리 고정된 스키마가 없으며 유연한 데이터 모델을 제공한다. 이를 통해 다양한 형식의 데이터를 효율적으로 관리할 수 있다.
Redis의 가장 큰 특징은 데이터를 메모리에 저장함으로써 매우 빠른 읽기 및 쓰기 성능을 제공한다는 점이다. 이 덕분에 실시간 응답이 중요한 시스템에서 캐시, 세션 관리, 실시간 데이터 처리와 같은 목적에 많이 사용된다.
공통 환경 설정
pom.xml
Spring Boot에서는 Redis와의 연결을 간편하게 설정할 수 있는 기능을 제공하며, 주로 spring-boot-starter-data-redis 의존성을 통해 Redis 관련 기능을 사용할 수 있다. 이 의존성을 추가하면 Lettuce라는 비동기 Redis 클라이언트가 기본적으로 포함된다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties
Redis는 도커로 올려놓은 상태이다.
spring.data.redis.host=localhost
spring.data.redis.port=6379
RedisTemplate
RedisTemplate은 Redis와의 상호작용을 위한 클래스로, Redis 데이터베이스에 대한 CRUD(생성, 읽기, 업데이트, 삭제) 작업을 지원한다. 이 클래스는 Redis와의 통신을 위해 필요한 설정 및 기능을 제공하지만, Redis Repository와는 달리, 개발자가 더 많은 자유도를 가지고 설정을 제어할 수 있도록 돕는다.
Redis Repository는 특정 데이터 구조에 맞춰 메서드를 정의해야 하므로, 사용자가 원하는 형태로 설정하기 어렵다. 반면, RedisTemplate은 개발자가 필요에 따라 다양한 방식으로 Redis와 통신할 수 있도록 유연성을 제공한다. 이를 통해 개발자는 자신의 서비스에 적합한 방법으로 Redis를 활용할 수 있으며, 복잡한 설정이나 커스텀 로직을 구현할 수 있다.
코드예시
RedisCacheConfig
직렬화 인터페이스는 StringRedisSerializer를 사용하는데, 해당 serializer는 다른 serializer 들과 다르게 자동으로 json 기반의 문자열로 변환하는 기능은 존재하지 않는다. 그래서 redis에 데이터를 적재할 때부터 문자열의 형태로 변환이 되어 있어야 한다. 하지만 이런 불편함을 감소하면 다른 serializer들이 가지고 있는 문제점들을 모든 상황에서 걱정하지 않아도 된다. 문자열을 변환하는 과정은 Service 단계에서 수행한다.
@Configuration
public class RedisCacheConfig {
// Redis 서버 호스트를 application.properties에서 주입
@Value("${spring.data.redis.host}")
private String host;
// Redis 서버 포트를 application.properties에서 주입
@Value("${spring.data.redis.port}")
private int port;
// RedisConnectionFactory 빈을 정의
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// LettuceConnectionFactory를 사용하여 Redis 서버와 연결을 생성
return new LettuceConnectionFactory(host, port);
}
// RedisTemplate 빈을 정의
@Bean
public RedisTemplate<?, ?> redisTemplate() {
// RedisTemplate 인스턴스를 생성
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// RedisConnectionFactory를 설정하여 Redis와 연결
redisTemplate.setConnectionFactory(redisConnectionFactory());
// Redis에서 사용할 키의 직렬화 방식을 설정
redisTemplate.setKeySerializer(new StringRedisSerializer());
// Redis에서 사용할 값의 직렬화 방식을 설정
redisTemplate.setValueSerializer(new StringRedisSerializer());
// Redis 해시에서 사용할 키의 직렬화 방식을 설정
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// Redis 해시에서 사용할 값의 직렬화 방식을 설정
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
// 설정된 RedisTemplate을 반환
return redisTemplate;
}
}
이러한 redisTemplate은 아래와 같은 메서드를 제공한다.
메서드 설명 사용 예시
메서드 | 설명 | 사용 예시 |
opsForValue() | 문자열 데이터에 대한 작업을 수행하는 메서드 | redisTemplate.opsForValue().set("key", "value"); String value = redisTemplate.opsForValue().get("key"); |
opsForList() | 리스트 데이터에 대한 작업을 수행하는 메서드 | redisTemplate.opsForList().leftPush("listKey", "value"); List<String> list = redisTemplate.opsForList().range("listKey", 0, -1); |
opsForSet() | 집합 데이터에 대한 작업을 수행하는 메서드 | redisTemplate.opsForSet().add("setKey", "value1", "value2"); Set<String> members = redisTemplate.opsForSet().members("setKey"); |
opsForHash() | 해시 데이터에 대한 작업을 수행하는 메서드 | redisTemplate.opsForHash().put("hashKey", "field", "value"); String value = (String) redisTemplate.opsForHash().get("hashKey", "field"); |
delete() | 특정 키를 삭제하는 메서드 | redisTemplate.delete("key"); |
hasKey() | 특정 키의 존재 여부를 확인하는 메서드 | boolean exists = redisTemplate.hasKey("key"); |
expire() | 특정 키의 만료 시간을 설정하는 메서드 | redisTemplate.expire("key", 10, TimeUnit.MINUTES); |
multi() | 트랜잭션을 시작하는 메서드 | redisTemplate.multi(); |
RedisDTO
@Getter
@Setter
@Data
public class RedisDTO {
@Id
private String id;
private String password;
}
RedisController
@RestController
@RequestMapping("api/v1/redis-api")
public class RedisController {
private final RedisService redisService;
public RedisController(RedisService redisService) {
this.redisService = redisService;
}
@PostMapping(value = "/redisTest")
public RedisDTO RedisTest(@Valid @RequestBody RedisDTO redisDto) {
String id = redisDto.getId();
String password = redisDto.getPassword();
redisService.setRedisData(redisDto.getId(), redisDto);
return redisDto;
}
}
RedisService, RedisServiceImpl
RedisServiceImpl에서 ObjectMapper를 이용해 redisDto객체를 JSON 형식으로 변환해준다.
public interface RedisService {
// void setRedisData(String id, String password);
void setRedisData(String id, RedisDTO redisDto);
}
@Service
public class RedisServiceImpl implements RedisService {
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
@Autowired
public RedisServiceImpl(RedisTemplate<String, String> redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
@Override
public void setRedisData(String id, RedisDTO redisDto) {
try {
// DTO를 JSON 문자열로 변환하여 Redis에 저장
String jsonValue = objectMapper.writeValueAsString(redisDto);
redisTemplate.opsForValue().set(id, jsonValue);
} catch (JsonProcessingException e) {
throw new RuntimeException("Error while saving data to Redis", e);
}
}
}
swagger를 통해 값을 넣어준 뒤 redis를 확인하면, 다음과 같이 값이 저장되는 것을 볼 수 있다.
또한, key의 타입이 Json 형식의 Stirng 구조로 되어있으며, 저장된 값을 보려면 get 명령어로 접근할 수 있다.
CrudRepository
레파지토리를 이용한 redis 사용은 CrudRepository를 상속받는 인터페이스를 생성함으로써, redis의 CRUD 처리를 수행하는 방식을 제공한다. 이와 유사한 방식으로는 jpa repository가 있는데, jpa repository또한 CrudRepository를 상속받고 있기 때문에, jpa와 동일하게 상위 인터페이스로부터 상속 받은 메서드들을 레디스 에서도 사용할 수 있다.
코드 예시
RedisRepository
redis의 CRUD 처리를 수행하기 위해 CrudRepository를 상속받는 인터페이스를 생성한다. CrudRepository를 이용하여 redis에 저장하게 되면, hash 자료구조로 변환되어 저장하게 된다.
public interface RedisRepository extends CrudRepository<RedisDTO, String> {
}
RedisDTO
redis에 저장할 엔티티에는 @RedisHash어노테이션을 붙여주어야 하는데, 이는 해당 엔티티가 redis에 저장됨을 나타낸다.
value 속성에 특정 값을 지정하면, 이후 해당 데이터의 key를 생성할 때 접두사(prefix)를 지정할 수 있다. 또한 @Id 어노테이션을 통해 prefix:구분자 형태(key:@id)로 데이터에 대한 키를 저장하여 각 데이터를 구분할 수 있다. 이때, @Id가 생략될 경우, 랜덤 값으로 설정된다.
추가적인 기능으로는 @Indexed 어노테이션을 통해 secondary indexes를 적용할 수 있으며, @TimeToLive 어노테이션을 통해 TTL을 적용할 수 있다.
@Getter
@Setter
@Data
@RedisHash(value = "RedisTest", timeToLive = 1209600)
public class RedisDTO {
@Id
private String id;
private String password;
}
RedisController
@RestController
@RequestMapping("api/v1/redis-api")
public class RedisController {
private final RedisService redisService;
public RedisController(RedisService redisService) {
this.redisService = redisService;
}
@PostMapping(value = "/redisTest")
public String RedisTest(@Valid @RequestBody RedisDTO redisDto) {
String id = redisDto.getId();
String password = redisDto.getPassword();
redisService.setRedisData(id, password);
return "pass";
}
}
RedisService, RedisServiceImpl
RedisController에서 받은 값을 이용하여, RedisDTO 객체를 생성하고, 받은 id와 password를 설정한 후, redisRepository를 통해 해당 데이터를 Redis에 저장한다
public interface RedisService {
void setRedisData(String id, String password);
}
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private final RedisRepository redisRepository;
public RedisServiceImpl(RedisRepository redisRepository) {
this.redisRepository = redisRepository;
}
public void setRedisData(String id, String password) {
RedisDTO redisDTO = new RedisDTO();
redisDTO.setId(id);
redisDTO.setPassword(password);
redisRepository.save(redisDTO);
}
}
swagger를 통해 값을 넣어준 뒤 redis를 확인하면, 다음과 같이 값이 저장되는 것을 볼 수 있다.
또한, key의 타입이 hash 자료 구조로 되어있으며, hash 구조의 키에 저장된 값을 확인하려면 get이 명령어 대신 HGETALL 명령어를 사용해야 한다.
Redis Cache Manager
Redis Cache Manager는 Spring Framework에서 Redis를 캐시 저장소로 사용하는데 필요한 기능을 제공하는 컴포넌트이다.
Redis Repository와 Redis Template은 엔티티와 리포지토리 또는 템플릿을 활용하여 Redis 데이터를 처리하며, 이 방식은 메서드 내부에 데이터 관리 로직이 포함되어 있다. 반면, Redis Cache Manager는 어노테이션 기반으로 캐싱을 처리하여 메서드 내부에 로직이 포함되지 않다. 대신, 메서드의 return 값을 활용한다.
어노테이션을 통해 Redis의 키, ID 등의 정보를 설정하면, 이 정보와 return 값을 매핑하여 Redis에 저장하는 방식이다.
이러한 Redis Cache Manager는 3가지의 어노테이션을 제공한다.
어노테이션 설명 사용 예시
어노테이션 | 설명 | 사용 예시 |
@Cacheable | 메서드의 결과를 캐시에 저장하고, 동일한 인자로 호출 시 캐시된 결과를 반환 | @Cacheable("myCache") |
@CachePut | 메서드를 실행하고, 항상 캐시에 결과를 저장합니다. (메서드가 호출될 때마다 실행됨) | @CachePut(value = "myCache", key = "#id") |
@CacheEvict | 캐시에서 특정 데이터를 제거합니다.allEntries를 사용하면 모든 캐시를 비울 수 있음 | @CacheEvict(value = "myCache", key = "#id") |
코드 예시
CacheConfig
Spring Data Redis 를 사용한다면 Spring Boot 가 RedisCacheManager 를 자동으로 설정해주지만, Cache Manager를 통해 캐시의 유효 시간(TTL)이나 직렬화 방법 등 다양한 설정을 조정할 수 있기 때문에 따로 설정해 주었다.
1. 캐시 기능 활성화
먼저, Spring에서 어노테이션 기반의 캐시 기능을 사용하기 위해서는 @EnableCaching 어노테이션을 설정 클래스에 추가해야 한다.
@Configuration
@EnableCaching
public class CacheConfig {
...
}
2. 캐시 구성 설정
RedisCacheConfiguration을 사용하여 기본 캐시 설정을 정의한다. 다음은 캐시 항목의 유효 시간을 10분으로 설정하고, GenericJackson2JsonRedisSerializer를 이용하여 객체를 JSON으로 직렬화하는 예시이다.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 캐시 항목의 유효 시간을 10분으로 설정
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer())); // JSON 직렬화 설정
}
}
3. Cache Manager 설정
마지막으로, CacheManager를 설정하여 Redis와 연결한다. 다음과 같이 RedisConnectionFactory를 사용하여 캐시 매니저를 구성한다.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
// 기본 캐시 설정을 가져온다.
return RedisCacheConfiguration.defaultCacheConfig()
// 캐시 항목의 유효 시간을 10분으로 설정
.entryTtl(Duration.ofMinutes(10))
// 캐시에 저장될 값의 직렬화 방법을 설정
// GenericJackson2JsonRedisSerializer를 사용하여 객체를 JSON으로 직렬화
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// RedisCacheManager의 빌더를 사용하여 새 캐시 매니저를 구성
return RedisCacheManager.builder(connectionFactory)
// 이전에 정의한 cacheConfiguration()을 사용하여 기본 캐시 설정을 적용
.cacheDefaults(cacheConfiguration())
// 최종적으로 설정된 캐시 매니저를 빌드하여 반환
.build();
}
}
RedisDTO
@Getter
@Setter
@Data
@RedisHash(value = "RedisTest", timeToLive = 1209600)
public class RedisDTO {
@Id
private String id;
private String password;
}
RedisController
@RestController
@RequestMapping("api/v1/redis-api")
public class RedisController {
private final RedisService redisService;
public RedisController(RedisService redisService) {
this.redisService = redisService;
}
@PostMapping(value = "/redisTest")
@Cacheable(value = "products", key = "#redisDto.id") // 캐시 적용
public RedisDTO RedisTest(@Valid @RequestBody RedisDTO redisDto) {
String id = redisDto.getId();
String password = redisDto.getPassword();
redisService.setRedisData(id, password);
return redisDto;
}
}
RedisService, RedisServiceImpl
public interface RedisService {
RedisDTO setRedisData(String id, String password);
}
@Service
public class RedisServiceImpl implements RedisService {
@Override
public RedisDTO setRedisData(String id, String password) {
RedisDTO redisDto = new RedisDTO(); // RedisDTO 객체 생성
redisDto.setId(id); // ID 설정
redisDto.setPassword(password); // 비밀번호 설정
return redisDto; // 저장된 DTO 반환
}
swagger를 통해 값을 넣어준 뒤 redis를 확인하면, 다음과 같이 값이 저장된다.
또한, 객체를 JSON으로 직렬화해서 넣어주었기 때문에, JSON 형식으로 저장된 것을 확인할 수 있다.
참고
Spring Boot 에서 Redis Cache 사용하기 :: 뱀귤 블로그 (tistory.com)
[SpringBoot] Redis 사용하기 (2) - Redis Repository 사용하기 (tistory.com)
(spring boot) RedisRepository 사용하는 방법, @RedisHash (tistory.com)