쏭의 개발 블로그
Spring Boot와 Redis 연동 (+연동 시 고민사항) 본문
프로젝트에서 Redis 관련 전역 환경 설정을 위해 Spring Boot와 Redis를 연동했다. 연동 방법과 그 과정에서 어떤 것을 고민했는지 기록하려고 한다.
[1] Redis 의존성 추가 및 application.yml 설정
먼저 Spring Boot에서 Redis를 사용하기 위한 의존성을 추가해보자. build.gradle에 Spring Data Redis 의존성을 추가해주면 된다. Spring Data Redis는 Spring에서 Redis연동을 쉽게 해주는 라이브러리로, RedisTemplate, StringRedisTemplate, Spring Cache 등의 기능을 제공한다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
이후 application.yml에 host와 port를 설정한다.
spring:
data:
redis:
host: ${REDIS_HOST}
port: ${REDIS_PORT}
# password : ${REDIS_PASSWOARD}
Spring Boot 3.2 이후 버전부터는 기존의 spring.redis 가 Deprecated 되어서 쓸 수가 없고 spring.data.redis를 사용해야한다.
[2] Redis Configuration 추가
Redis Configuration을 추가한다. RedisConfig 를 생성하고 다음과 같이 작성한다.
1) Configuration 기본 설정
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
}
Redis의 설정 정보를 Configuration 클래스에서 주입한다.
2) Redis 연결 설정 : RedisConnectionFactory(Lettuce)
*️⃣ RedisConnection
RedisConnection은 Redis 서버와 통신하기 위한 핵심 기능을 제공하며, Redis 클라이언트 라이브러리에서 발생하는 예외들을 Spring의 Dao 예외 계층으로 자동변환해준다. 이 RedisConnection 객체를 RedisConnectionFactory를 통해 생성할 수 있다.
*️⃣ RedisConnectionFactory
RedisConnectionFactory는 Spring Data Redis에서 Redis 서버와의 연결을 생성하는 역할을 하는 인터페이스이다. 여러 구현체를 가지고 있으며 각 구현체는 특정 유형의 Redis서버나 연결 방식을 지원한다. 이 인터페이스의 구현체로 Redis 클라이언트 라이브러리(Lettuce, Jedis 등)를 선택해서 사용할 수 있다.
*️⃣Redis 클라이언트 라이브러리
Redis 클라이언트에는 Lettuce와 Jedis가 있다. Jedis는 Java 기반 기본 Redis 클라이언트로, 싱글스레드 기반, 동기방식을 지원하고 단순한 캐시 사용에 권장된다. Lettus는 Netty 기반의 고성능 Redis 클라이언트로, 멀티스레드, 비동기 및 동기 방식을 모두 지원한다는 장점을 가지고 있다. Spring Boot에서는 Lettuce 사용을 권장한다.
여기서는 RedisConnectionFactory의 구현체로 Lettuce를 사용하여 RedisDB와 연결한다. Redis에 비밀번호를 설정한 경우와 설정하지 않은 경우를 다르게 설정해준다.
(1) host와 port만 지정하는 경우 : LettuceConnectionFactory(host,port)
먼저, host와 port만 직접 지정해주는 경우는 아래와 같이 설정하면 된다. LettuceConnectionFactory(host,port)를 사용하여 host와 port를 직접 지정한다. 이 경우는 비밀번호는 설정이 불가능하다.
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// 직접 host와 port를 지정해서 Redis에 연결
return new LettuceConnectionFactory(host,port);
}
(2) host, port, password 모두 지정하는 경우 : RedisStandaloneConfiguration
비밀번호가 설정된 Redis 서버 연결에 사용한다. 비밀번호가 설정된 Redis 서버에 접근해야할 경우 필수적으로 설치해야한다.
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
config.setPassword("password"); // Redis 비밀번호 설정 시 추가
return new LettuceConnectionFactory(config);
}
RedisStandaloneConfiguration은 단일 노드 Redis 인스턴스 에 연결하기 위한 RedisConnection 설정 에 사용되는 구성 클래스다. 이 객체를 통해 host, port, password 등redis 접속정보를 가지고 있는 객체를 생성한다. RedisStandaloneConfiguration 생성자로 객체를 생성하여 Redis서버의 host와 port를 설정할 수 있다. 비밀번호의 경우 setPassword로만 설정이 가능하다. Redis가 비밀번호 인증을 요구하는 경우에는 반드시 setPassword를 추가해야한다.
현재는 비밀번호를 설정하지 않았지만 추후 설정할 가능성이 있어 두 번째 방법을 선택했다.
3) Redis 데이터 접근 : RedisTemplate와 StringRedisTemplate
(1) RedisTemplate
Spring Boot에서 RedisTemplate를 사용하여 Redis 데이터에 접근할 수 있다. RedisTemplate은 Java 객체를 Redis에 저장할 수 있도록 지원하는 Spring의 주요 클래스이다. 기본적으로 직렬화/역직렬화를 통해 데이터를 변환하여 Redis에 저장한다. Redis에서 다양한 데이터 타입을 저장하고 조작할 수 있는 범용적인 템플릿 클래스로, Key와 Value의 타입을 자유롭게 설정이 가능하다. String, Hash, List, Set, ZSet을 지원한다.
RedisTemplate을 사용하는 메서드는 이렇게 생성할 수 있다.
// 다양한 자료구조 및 객체 저장을 위한 RedisTemplate<K,V>
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// RedisTemplate 객체 생성 - String 타입의 키와 Object 타입의 값을 저장할 수 있도록 설정
RedisTemplate<String, Object> template = new RedisTemplate<>();
// RedisConnectionFactory 설정
template.setConnectionFactory(redisConnectionFactory);
// JSON 직렬화/역직렬화를 위한 직렬화기 생성 - 자바 객체를 JSON으로 변환
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(new StringRedisSerializer()); // 키 직렬화 설정
template.setValueSerializer(serializer); // 값 직렬화 설정 - 객체를 JSON으로 직렬화
template.setHashKeySerializer(new StringRedisSerializer()); // 해시 자료구조의 키 직렬화 설정
template.setHashValueSerializer(serializer); // 해시 자료구조의 값 직렬화 설정 - 객체를 JSON으로 직렬화
template.afterPropertiesSet(); // 설정 후 초기화 작업 수행
return template;
}
이 코드에서는 Redis<String, Object>를 사용했다. Key는 일반적으로 String을 사용하므로 K는 String으로 설정하고 Value는 다양한 타입을 지정해야하므로 Object로 설정했다. 이렇게 하면 key를 String으로 고정하여 실수를 방지할 수 있으며, value에는 문자열, json, 리스트, 해시 등 모두 저장 가능하다. 코드 가독성과 유지보수를 위해 이렇게 설정하는 것이다.
RedisTemplate은 기본적으로 JdkSerializationRedisSerializer를 사용하지만 나는 GenericJackson2JsonRedisSerializer를 사용하여 Value를 직렬화/역직렬화했다. JdkSerializationRedisSerializer는 JDK직렬화를 사용해서 자바 시스템에서만 직렬화/역직렬화가 가능하며 serialVersionUID를 설정하지 않았을 경우 예외가 발생할 수 있다. GenericJackson2JsonRedisSerializer는 JSON형태여서 사람일 읽기가 쉽고, 성능도 JdkSerializationRedisSerializer보다 좀 더 좋다. 이러한 이유로 나는 GenericJackson2JsonRedisSerializer을 사용했다. (자세한 Redis Serializer에 대한 내용은 다음 포스트에서 다루겠다.)
🤔 RedisTemplate의 Object에 LocalDateTime을 넣고 싶다면?
앞선 방법과 같이 redisTemplate을 구현하면 대부분의 데이터 타입을 저장할 수 있다. 하지만 LocalDateTime의 경우, redis에 저장한 후 꺼냈을 때 ArrayList 타입으로 나온다. GenericJackson2JsonRedisSerializer은 기본적으로 LocalDateTime을 ISO 8601형식의 문자열로 직렬화한다. 역직렬화 시 문자열로 변환된 값을 LocalDateTime으로 변환하는 과정에서 실패할 수 있다.
이를 해결하기 위해 LocalDateTime의 직렬화 및 역직렬화를 위한 커스터마이징을 추가하면 된다. LocalDateTime에 대해 적절한 Module을 등록하여 LocalDateTime을 직렬화할 수 있도록 해야하는 것이다.
코드는 아래이 추가해주면 된다.
// 다양한 자료구조 및 객체 저장을 위한 RedisTemplate<K,V>
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// Jackson의 다형성 타입 검증기 생성
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator
.builder()
// 모든 Object 타입의 하위 타입에 대해 역직렬화를 허용하는 규칙 설정
.allowIfSubType(Object.class)
.build();
// JSON 처리를 위한 Jackson ObjectMapper 생성 및 설정
ObjectMapper objectMapper = new ObjectMapper()
// 날짜 및 시간을 타임스탬프(long 값) 대신 ISO 8601 문자열 형식으로 직렬화하도록 설정
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// JSON에 알 수 없는 속성이 있어도 역직렬화 실패를 발생시키지 않도록 설정
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
// 객체의 타입 정보를 JSON에 포함시켜 역직렬화 시 원래 타입을 복원할 수 있도록 활성화
// typeValidator를 사용하여 허용되는 타입만 역직렬화하도록 제한
.activateDefaultTyping(typeValidator, DefaultTyping.NON_FINAL_AND_ENUMS)
// Java 8 날짜 및 시간 API (LocalDateTime 등)를 지원하는 모듈 등록
.registerModule(new JavaTimeModule());
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
PolymorphicTypeValidator을 설정한다.
PolymorphicTypeValidator는 Redis에 저장할 때 객체가 어떤 타입인지 알 수 있도록 타입 정보를 JSON에 포함시키는 설정이다. Jackson은 기본적으로 클래스 타입을 저장하지 않아 역직렬화할 때 어떤 클래스로 만들어야할지 모르기 때문에 타입정보를 넣어줘야한다.
다음 ObjectMapper를 설정해야한다.
- disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) : 기본적으로 Jackson은 LocalDateTime을 timestamp(long)으로 저장하려고 한다. 이렇게되면 사람이 읽기도 힘들고 복원 시 의도와 다를 수 있어 ISO 형식 문자열로 저장하도록 하게 한다
- configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) : 역직렬화 시에 JSON에 예상치 못한 필드가 있어도 무시하고 넘어가게 만드는 설정이다. Redis에 저장된 데이터 구조가 바뀌었을 때, 예전 데이터가 오류가 나지 않고 로드될 수 있도록 해주는 방어적인 설정이라고 할 수 있다.
- activateDefaultTyping(typeValidator, DefaultTyping.NON_FINAL _AND_ENUMS) : Jackson라이브러리에서 기본 타입 정보 활성화를 설정하는 데 사용된다. 객체를 JSON으로 직렬화할 때 타입 정보를 함께 포함시켜 역직렬화 시 원래 객체 타입을 정확하게 복원할수 있도록 설정하는 것이다. 나는 final 클래스가 아닌 모든 클래스와 Enum타입의 타입 정보도 JSON에 포함시키기 위해 DefaultTyping을 NON_FINAL _AND_ENUMS으로 설정했지만, 보통을 NON_FINAL으로 해서 final이 아닌 모든 클래스만 포함시키면 된다.
- registerModule(new JavaTimeModule()) : LocalDate, LocalDateTime, ZonedDateTime과 같은 Java 8 Date/Time API 타입을 Jackson이 인식하고 처리할 수 있도록 도와주는 모듈이다. 이게 없으면 LocalDateTime 직렬화/역직렬화 시 예외가 발생한다.
이렇게 하면 RedisTemplate의 Value인 Object에 LocalDateTime이 포함되어도 직렬화/역직렬화가 정상적으로 동작한다.
(2) StringRedisTemplate
나는 추가적으로 문자열 저장을 위해 StringRedisTemplate도 사용했다.
StringRedisTemplate은 문자열 저장에 특화된 RedisTemplate의 구현체이다. Redis에 대한 대부분의 연산이 문자열 기반으로 이루어진다는 점을 고려해, 문자열 데이터 처리에 필요한 설정 과정을 최소화한 특화된 클래스라고 할 수 있다. Key-Value를 무조건 String 타입으로 저장한다. 아래와 같이 설정하면 된다.
// 문자열 저장을 위한 StringRedisTemplate
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
RedisTemplate<string,object>를 사용하여 문자열을 저장할 수도 있지만, 이 경우 JdkSerializationRedisSerializer 이나 GenericJackson2JsonRedisSerializer 를 사용해야하므로 ClassType을 포함하여 직렬화/역직렬화를 하게 된다. 이는 성능, 용량, 언어호환성에서 StringRedisSerializer보다 좋지 않으며, 대용량 데이터를 저장하게 될 경우 크게 문제가 될 수 있다.
StringRedisTemplate과 같은 역할을 수행하는 RedisTemplate<String, String>를 사용하면 되지 않을까라는 의문이 생길수도 있다. 하지만 이 경우에도 기본 직렬화 방식에서 차이가 있다. 물론 RedisTemplate<String, String>에서 키랑 값 직렬화를 StringRedisSerializer로 설정하면 StringRedisTemplate과 동일해지기는 하지만, 추가적인 직렬화 설정을 하는 것이기 때문에 굳이 할 필요가 없다.
RedisConfig 전체 코드는 아래와 같다.
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
return new LettuceConnectionFactory(config);
}
// 문자열 저장을 위한 StringRedisTemplate
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
// 다양한 자료구조 및 객체 저장을 위한 RedisTemplate<K,V>
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator
.builder()
.allowIfSubType(Object.class)
.build();
ObjectMapper objectMapper = new ObjectMapper()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.activateDefaultTyping(typeValidator, DefaultTyping.NON_FINAL_AND_ENUMS)
.registerModule(new JavaTimeModule());
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
[3] RedisTemplate 테스트 코드 작성
위 RedisConfig의 RedisTemplate을 테스트하는 테스트 코드를 작성해보았다. String,Object, Set, List, Hash 등 다양한 자료구조를 테스트하도록 했다.
@SpringBootTest
public class RedisConfigTest {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@BeforeEach
void setUp() {
stringRedisTemplate.delete("testStringKey");
redisTemplate.delete("testObjectKey");
redisTemplate.delete("testListKey");
redisTemplate.delete("testHashKey");
}
@Test
void testStringWithStringRedisTemplate() {
stringRedisTemplate.opsForValue().set("testStringKey", "testStringValue");
String result = stringRedisTemplate.opsForValue().get("testStringKey");
assertEquals("testStringValue",result);
}
@Test
void testObjectWithRedisTemplate() {
TestObject testObject = new TestObject("John", LocalDateTime.now());
redisTemplate.opsForValue().set("testObjectKey",testObject);
TestObject result = (TestObject) redisTemplate.opsForValue().get("testObjectKey");
assertNotNull(result);
assertEquals(testObject.getName(), result.getName());
assertEquals(testObject.getCreatedAt(), result.getCreatedAt());
}
@Test
void testListWithRedisTemplate() {
ListOperations<String, Object> listOps = redisTemplate.opsForList();
listOps.rightPush("testListKey","Item1");
listOps.rightPush("testListKey","Item2");
listOps.rightPush("testListKey","Item3");
assertEquals(3, listOps.size("testListKey"));
assertEquals("Item3",listOps.rightPop("testListKey"));
assertEquals("Item2",listOps.rightPop("testListKey"));
assertEquals("Item1",listOps.rightPop("testListKey"));
}
@Test
void testHashWithRedisTemplate() {
Date birthday = new Date();
HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash();
Map<String, Object> map = new HashMap<>();
map.put("name","John");
map.put("age",28);
map.put("birthday",birthday);
hashOps.putAll("testHashKey",map);
assertEquals("John",hashOps.get("testHashKey","name"));
assertEquals(28,hashOps.get("testHashKey","age"));
assertEquals(birthday,hashOps.get("testHashKey","birthday"));
}
private static class TestObject {
private String name;
private LocalDateTime createdAt;
public TestObject() {}
public TestObject(String name,LocalDateTime createdAt) {
this.name = name;
this.createdAt = createdAt;
}
public String getName() {
return name;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
}
}
이런 방법으로 Redis 사용을 위한 설정은 완료됐다!
참고자료
https://programmingiraffe.tistory.com/171
https://revi1337.tistory.com/267
https://velog.io/@mj3242/Spring-Boot-Redis-연동Redis-Cache-활용
'Back-end > Spring' 카테고리의 다른 글
Spring Boot와 Redis 연동(2) : Redis Serializer (0) | 2025.05.04 |
---|---|
WebFlux에 대해서 (0) | 2023.11.07 |
DAO, DTO, VO, Domain 차이 (0) | 2023.10.07 |
@Controller와 @RestController 차이 (0) | 2023.10.07 |
Spring AOP (0) | 2023.10.07 |