2024.05.08 - [Spring/대용량 트래픽] - Redis Cache로 실습하기
이전글에서는 Redis를 Cache로 활용해서 실습을 진행했다.
근데 사실 Spring 자체에 Redis를 위한 코드들이 준비가 되어있다. 이 코드들에 대해서 알아보도록하자.
스프링 시작 사이트에 들어가서 Spring Data Redis 의존성을 추가해줘야한다.
여기서는 Redis Client를 제공하고, Redis에 대해 단순화, 추상화된 인터페이스를 제공한다.
Spring Data
sspring data 라고 앞에 붙는것을 대부분 Spring Data Jpa에서 봤을 것이다. 그렇다면 Spring Data는 대체 무엇이냐?!
- 스프링 프레임워크에서 다양한 데이터 저장소에 대해 추상화된 기능, 인터페이스를 제공하는 라이브러리
- RDB, NOSQL등의 DB에 CRUD 편리하게 제공
https://docs.spring.io/spring-data/redis/reference/redis.html
Spring Data Redis에 대한 공식 문서이니 자세히 알고 싶은 사람은 참고하면 좋을것 같다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
의존성은 이와같이 추가해주면 된다.
Redis Clients
Spring Data Redis는 두개의 클라이언트를 지원한다.
둘다 추상화되어 있기 때문에, 직접적인 구현은 필요하지 않다!
- Lettuce(기본 사용)
- Jedis
RedisTemplate
Spring Data Redis 에서 제공하는 Redis Client 객체.
특징
- abstraction : 추상화
- connection : 연결관리
- serializer : 데이터 변환처리(직렬화)
ex) Rest Template에 대한 코드작성
우리가 선택한 클라이언트가 Redis에 연결하여 상호작용한다.
yml파일에 host와 port 정보를 줘야 한다. 실습을 통해서 확인해보자.
우선 yml 파일설정
spring:
datasource:
url: jdbc:mysql://localhost:3307/fastsns
username: root
password: 본인비밀번호입력
jpa:
hibernate:
ddl-auto: update
show-sql: true
jmx:
enabled: false
data:
redis:
host: 127.0.0.1
port: 6379
전과 달라진 점은 ddl-auto: update로 바꾼점이다.
create설정은 어플리케이션이 실행될때 마다 모든 테이블을 지우고 다시 만드는 설정이기 때문에 기존 테이블을 유지하고 추가또는 삭제를 하기 위해서 바꾸어 줬다.
이전 글에서의 코드 구조가 다른 부분은 Service 단을 추가해줬다.
UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User getUser(final Long id){
return userRepository.findById(id).orElseThrow();
}
}
이제 실제로 Redis를 Spring 프레임워크에서 적용시켜보자.
제일 먼저 Redis에 관해 설정파일을 만든다.
RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
RedisTemplate<String, User> userRedisTemplate(RedisConnectionFactory redisConnectionFactory){
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
RedisTemplate<String, User> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(objectMapper, User.class));
return template;
}
}
- 위 설정 코드는 Redis가 Map의 자료구조로 데이터를 다루고, Java언어는 Redis에 저장하기위해 형태에 알맞게 직렬화를 시켜줘야하는데, Key는 String, Value는 이 경우에 User라는 객체에서 Json형식으로 변환시켜줘야하므로, Jackson2Json형으로 세팅해줬다. 그리고 그냥 내장 ObjectMapper를 쓰면 위코드는 오류가 난다. 왜? 시간정보같은 경우에는 default Object Mapper가 해석할 수 없으므로, 따로 재정의해줘야한다.
- configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) : Redis에서 Java로 데이터를 전달받는 역직렬화 과정에서 해석할 수 없는 데이터가 있으면 무효화 한다.
- registerModule(new JavaTimeModule()), disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS); : 시간정보에 관해 Redis로 전달해준다.
등록된 설정파일을 가지고 Service를 만든다.
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final RedisTemplate<String, User> userRedisTemplate;
public User getUser(final Long id){
String key = "users:%d".formatted(id);
//1.cache에서 가져옴
User cachedUser = userRedisTemplate.opsForValue().get(key);
if(cachedUser != null){
return cachedUser;
}
//cache에 없으면
//2.db에서 응답하고, cache에 set
User user = userRepository.findById(id).orElseThrow();
userRedisTemplate.opsForValue().set(key, user);
return user;
}
}
실행을 시키고 모니터링을 해보면
Redis에 값이 없기때문에 직접 DB에 데이터를 요청하는 로그를 확인할 수 있고, Redis에 직접 값이 들어가는것도 확인할 수 있다.
값은 요청을 하게 되면 더 이상 DB에서 데이터를 가져오지 않고, Redis에서 바로 가져온다.
하지만 cache로써 Redis를 활용하려면 위 방식대로 영구적으로 저장하는 것이 아닌, TTL을 설정해줘야한다.
- set함수에 Duration.ofSeconds(설정하고싶은 시간)을 인자로 넣어주어 설정해줄수 있다.
이와 같이 한번 Redis를 RedisTemplate으로 사용해봤는데, 위에서 다룬것은 String에 관해서 다룬것이다. 그렇다면 다른 자료형에 대해서 하려면 매번 형식을 바꿔서 구현해줘야할까?
RedisConfig.java
@Bean
RedisTemplate<String, Object> objectRedisTemplate(RedisConnectionFactory redisConnectionFactory){
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper);
return template;
}
전에는 User에 대해서만 설정해줬다면 이번에는 자료형을 Object로 반환하는 것과 GenericJackson2JsonRedisSerializer함수를 통해서 모든 객체에 대해 직렬화 해줄수 있도록 바꿔줬다.
하지만 위 코드대로 실행시키면 에러가 난다. 전에 userRedisTemplate에서는 User.class형식을 직렬화 하라고 명시했지만, 직접 직렬화된 데이터에 관한정보를 넣어주지 않기 때문이다. Jackson2JsonRedisSerializer에서 이 정보를 필요로한다.
@Bean
RedisTemplate<String, Object> objectRedisTemplate(RedisConnectionFactory redisConnectionFactory){
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
.builder()
.allowIfSubType(Object.class)
.build();
ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL)
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper);
return template;
}
}
이와 같이 activateDefaultTyping 함수를 사용하면 해당 클래스나 패키지의 정보를 넘겨줄수 있게 되어 정상 동작한다.
이제는 추가적으로 어느 클래스인지 모니터링 할 수있다.
Redis Repository
Spring Data Jpa와 마찬가지로 Repository 인터페이스를 통해 데이터를 다룰수 있다.
- @RedisHash
@RedisHash 어노테이션과 함께활용이 가능하다. 이 어노테이션으로 선언된 값과 @Id로 선언된 KEY가 합쳐져서 Redis의 KEY가 된다.
CrudRepository<Entity, 데이터타입>
Redis Hash같은 경우 Crud 인터페이스만을 상속받아서 Repository로 사용할 수 있다.
Hash + Set 데이터 타입으로 Redis에 저장된다.
- @Indexed : 검색목적의 Set데이터 타입정보를 Redis에 추가로 저장
@RedisHash와 CrudRedisRepository를 이용해서 RedisTemplate을 대체해보자.
우선 엔티티가 필요하다.
RedisHashUser.java
@RedisHash(value = "redis-hash user", timeToLive = 30L)
@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class RedisHashUser {
@Id
private Long id;
private String name;
@Indexed
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
** TTL 값은 HMSET에만 반영이되고, SET에는 반영이 되지 않는다!
RedisHashUserRepository.java
public interface RedisHashUserRepository extends CrudRepository<RedisHashUser, Long> {
}
UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final RedisTemplate<String, User> userRedisTemplate;
private final RedisTemplate<String, Object> objectRedisTemplate;
private final RedisHashUserRepository redisHashUserRepository;
public RedisHashUser getUser2(final Long id){
RedisHashUser cachedUser = redisHashUserRepository.findById(id).orElseGet(() -> {
User user = userRepository.findById(id).orElseThrow();
return redisHashUserRepository.save(RedisHashUser.builder()
.id(user.getId())
.name(user.getName())
.email(user.getEmail())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build());
});
return cachedUser;
}
}
HMSET과 SET에 값이 DB에서 가져와 저장되는걸 확인할 수있고, HGETALL을 통해 이미 Redis에 적재되어있는 데이터일 경우 DB를 거치지 않고 바로 응답을 주는것을 확인할 수 있다.
다음글은 스프링에서 추상화된 캐시기능을 제공하는 Spring cache abstraction에 대해서 다루겠다.
'Spring > 대용량 트래픽' 카테고리의 다른 글
Spring Session (0) | 2024.05.13 |
---|---|
Spring cache abstraction, Vegeta 오픈소스 사용해보기 (0) | 2024.05.13 |
Redis Cache로 실습하기 (0) | 2024.05.08 |
Redis Cache 이론 (0) | 2024.05.07 |
Redis Key, Scan 명령어 (0) | 2024.05.07 |