안녕하세요 오늘은 Spring Boot에서 Redis를 적용하는 내용에 대해서 포스팅하려고 합니다.
현재 투다의 경우 인메모리DB로 Redis를 사용하고 있습니다. Memcached에 비해 다양한 기능을 제공하며 싱글 스레드 방식으로 동작하여 가볍기 때문입니다. 이런 Redis의 Java 클라이언트로는 크게 lettuce와 Jedis가 존재합니다.
출처 : https://redis.com/blog/jedis-vs-lettuce-an-exploration/
Jedis란 성능과 사용성을 위해 만들어진 클라이언트 라이브러리입니다. 하지만 동기 방식으로 작동하여 Blocking 이슈가 발생 가능하다는 단점이 있습니다.
출처 : https://redis.com/blog/jedis-vs-lettuce-an-exploration/
Lettuce 역시 클라이언트 라이브러리이며 동기, 비동기 방식을 둘 다 지원하여 non-blocking하게 요청을 처리할 수 있고 확장성이 뛰어나다는 장점이 있습니다. 하지만 사용성이 Jedis에 비해 어렵다는 단점이 있습니다.
출처 : https://redis.com/blog/jedis-vs-lettuce-an-exploration/
현재 투다는 빠르게 쉽게 개발할 필요성보다 추후 변경 사항 및 튜닝 등을 대비하여 확장성이 높으며 비동기 처리를 통해 안정적으로 요청을 처리할 수 있는 Lettuce가 더 적합하다고 판단하여 Lettuce를 이용하여 Redis를 사용하기로 하였습니다.
implementation('org.springframework.boot:spring-boot-starter-data-redis')
우선 build.gradle에 다음의 내용을 추가해줍니다.
package com.example.test.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.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.port}")
public int port;
@Value("${spring.data.redis.host}")
public String host;
// TCP 통신
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(host, port));
}
// // Unit 소켓 통신
// @Bean
// public LettuceConnectionFactory redisConnectionFactory() {
// return new LettuceConnectionFactory(new RedisSocketConfiguration("/socket-dir"));
// }
// 커넥션 위에서 조작 가능한 메소드 제공
// 공식 문서에서는 <String, String>으로 되어 있지만
// 출력값을 String으로 제한두지 않으려고 <String, Object>로 변경
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// setKeySerializer, setValueSerializer 사용 이유
// RedisTemplate 사용 시에 Spring-Redis 간 데이터 직렬화, 역직렬화에 사용하는 방식이 Jdk 직렬화 방식
// 직렬화 : 자바 시스템 내부에서 사용되는 Object 또는 Data를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 데이터를 변환하는 기술
// 역직렬화 : byte로 변환된 Data를 원래대로 Object나 Data로 변환하는 기술
// 직렬화/역직렬화 사용 이유
// 복잡한 데이터 구조의 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화, 역직렬화가 가능
// 데이터 타입이 자동으로 맞춰지기 때문에 관련 부분을 크게 신경 쓰지 않아도 됨
// 출처
// https://go-coding.tistory.com/101
// https://velog.io/@yoojkim/Spring-Boot-Redis-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // JSON 포맷으로 저장
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
// 문자열에 특화한 메소드 제공
@Bean
StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
Spring Data Redis 공식 문서를 참고하여 RedisConfig 클래스를 생성합니다. 우선 LettuceConnectionFactory를 사용하여 Lettuce로 Redis와의 연결을 진행합니다. 이 때 Redis의 주소와 포트를 파라미터로 넣어줍니다. 이 값들은 application.properties에 추가하거나 실행 시 환경 변수로 넣어 진행할 수 있습니다.
RedisTemplate의 경우 Redis와 연결된 커넥션 위에서 조작 가능하도록 메소드를 제공합니다. 따라서 해당 객체를 통해 Spring Boot에서 Redis에 데이터를 추가 조회 등 작업을 수행할 수 있습니다. 이 때 SpringRedisTemplate는 문자열에 특화한 메소드를 제공하기 때문에 상황에 맞는 객체를 사용하시면 되겠습니다.
여기서 직렬화 및 역직렬화 설정을 위해 setKeySerializer, setValueSerializer 옵션을 사용했습니다. 직렬화란 Java 내부의 데이터들을 외부에서 사용 가능하도록 바이트 형태로 데이터를 변환하는 기술이며, 역직렬화의 경우 그 역, 즉 바이트 데이터를 원래 Object로 변환하는 기술입니다. 이를 통해 복잡한 데이터 구조의 클래스의 객체라도 직렬화 기본 조건만 지키면 큰 작업 없이 바로 직렬화, 역직렬화가 가능하며 이를 통해 데이터 타입이 자동적으로 맞춰지기 때문에 타입에 대해 크게 신경쓰지 않아도 된다는 장점이 있습니다.
@Override
public GetUserInfoResponseDTO getUserInfo(String ID) {
UserDAO userDAO = userRepository.getUserInfo(ID);
// RedisTemplate 사용 타입
// opsForValue() ValueOperations Strings를 쉽게 Serialize / Deserialize 해주는 Interface
// opsForList() ListOperations List를 쉽게 Serialize / Deserialize 해주는 Interface
// opsForSet() SetOperations Set를 쉽게 Serialize / Deserialize 해주는 Interface
// opsForZSet() ZSetOperations ZSet를 쉽게 Serialize / Deserialize 해주는 Interface
// opsForHash() HashOperations Hash를 쉽게 Serialize / Deserialize 해주는 Interface
// Redis와 연결
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
// 데이터 추가 시 set
valueOperations.set("id",userDAO.getID());
valueOperations.set("email",userDAO.getEmail());
valueOperations.set("password",userDAO.getPassword());
valueOperations.set("name",userDAO.getName());
valueOperations.set("code",userDAO.getCode());
// 데이터 출력 시 get
return new GetUserInfoResponseDTO(
Long.parseLong(String.valueOf(valueOperations.get("id"))),
String.valueOf(valueOperations.get("email")),
String.valueOf(valueOperations.get("password")),
String.valueOf(valueOperations.get("name")),
String.valueOf(valueOperations.get("code"))
);
}
다음은 유저의 아이디값에 따른 유저 정보 조회 Service입니다. Redis 연동 여부를 테스트하기 위해 코드를 위와 같이 변경했습니다. 우선 ValueOperation 타입을 통해 String 타입을 사용하도록 Redis와 연결한 후 set 메소드를 통해서 키값과 그에 맞는 값을 Redis에 저장합니다. 이후 get 메소드를 통해 Redis에 저장되어 있는 데이터를 읽어 리턴합니다.
Servive 단위 테스트 역시 성공적으로 진행됐습니다.
redis-cli에 접속해 redis에 저장된 키를 확인해보면 다음과 같이 5개의 데이터가 Redis에 등록된 것을 확인할 수 있습니다. 이를 통해 Validation 작업 또는 로그인 토큰 등의 정적 데이터를 저장하여 DB의 부하를 줄이는 방식으로 사용할 예정입니다. 또한 redis unix socket을 통하여 통신하는 방법, 고가용성을 위한 redis sentinel, redis transaction 시 사용되는 connection pool 설정 등 추후 redis 튜닝 및 최적화 작업도 같이 진행해보도록 하겠습니다. 이상으로 포스팅 마치겠습니다. 감사합니다!