저번 시간에 볼륨 마운트를 통해서 Docker 환경에서도 이메일이 정상적으로 잘 보내지도록 설정했다. 그렇다면 오늘은, Redis + 프로젝트 서버를 함께 올려보자.
dependencies {
...
// SpringBoot + Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
...
}
spring:
data:
redis:
host: localhost
port: 6379
기본 디폴트 설정이다. 사실 안해줘도 됨 (그러나 해주자. 이유는 아래에)
@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, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.disableCachingNullValues()
.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(new StringRedisSerializer())
);
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(defaultConfig)
.build();
}
}
Jedis vs Lettuce
docker pull redis:latest
해당 명령어를 통해서 Docker에서 레디스(최신버전)이미지를 풀받는다.
docker run -d -p 6379:6379 --name "컨테이너 이름" redis
아까도 말했듯, StringRedisSerializer는 직렬화/역직렬화를 명시해줘야 하는데, 그 이유는 key : value 모두가 String으로만 들어가고 나오기 때문이다.
따라서 우리는 objectMapper를 통해서 Json 데이터로의 변환을 해줘야 한다. (ObjectMapper는 Jackson 라이브러리에 포함되어 있다)
public void setValues(String key, Object data, Duration duration) throws JsonProcessingException {
ValueOperations<String, String> values = redisTemplate.opsForValue();
String serializedData = objectMapper.writeValueAsString(data);
// 객체를 JSON 문자열로 직렬화
values.set(key, serializedData, duration);
}
public <T> T getValues(String key, Class<T> valueType) throws JsonProcessingException {
ValueOperations<String, String> values = redisTemplate.opsForValue();
String serializedData = values.get(key);
if (serializedData != null) {
return objectMapper.readValue(serializedData, valueType);
// JSON 문자열을 객체로 역직렬화
}
throw new NoSuchElementException("키와 대응하는 값이 없습니다");
}
setValue 부분에서는 objectMapper를 통해서 객체를 JSON 문자열로 직렬화 하고, ValueOperations 객체를 이용해서 간단한 객체의 저장이 가능하다.
null이 아니라면(즉, 값이 있다면) return value , 없다면 NoSuchElementExeption 던져준다.
그런데, 리스트 타입은? 뭉떵이들은? 컬렉션은????
StackOverFlow 참조글 - ValueOperations란?
따라서 명시적으로 따로 설정해줘야 한다. (그게 더 적절하다)
public <T> void setListValues(String key, List<T> data, Duration duration) throws JsonProcessingException {
ListOperations<String, String> listOperations = redisTemplate.opsForList();
List<String> serializedList = new ArrayList<>(data.size());
for (T datum : data) {
String serializedDatum = objectMapper.writeValueAsString(datum);
serializedList.add(serializedDatum);
}
listOperations.leftPushAll(key, serializedList);
redisTemplate.expire(key, duration);
}
public <T> List<T> getListValues(String key, Class<T> elementType) throws JsonProcessingException {
ListOperations<String, String> listOperations = redisTemplate.opsForList();
List<String> serializedData = listOperations.range(key, 0, -1);
if (serializedData != null && !serializedData.isEmpty()) {
List<T> deserializedData = new ArrayList<>();
for (String serializedDatum : serializedData) {
T deserializedDatum = objectMapper.readValue(serializedDatum, elementType);
deserializedData.add(deserializedDatum);
}
return deserializedData;
}
throw new NoSuchElementException("키와 대응하는 값이 없습니다");
}
특이한 점은, .rightPushAll() 부분일것이다.
Redis에 List타입을 저장 할 때 leftPush는 리스트의 맨 왼쪽에, rightPush는 리스트의 맨 오른쪽에 데이터를 추가한다.
rightPushAll() 매서드를 사용하게 되면 순서를 유지하면서 쭉 순서대로 들어간다.
예를 들어, 다음과 같은 데이터가 있다면
List<Member> members;
members.get(0) = mem1;
members.get(1) = mem2;
members.get(2) = mem3;
아래와 같이 들어간다.
Index Value
0 mem1
1 mem2
2 mem3
@Test
void setMoviesRedisTest() throws JsonProcessingException {
List<MovieResponseDto> movieList = movieRepository.getMoviesPaging(1L);
redisDAO.setListValues("test1", movieList, Duration.ofMillis(10000));
List<MovieResponseDto> list = redisDAO.getListValues("test1", MovieResponseDto.class);
for (MovieResponseDto movieResponseDto : list) {
System.out.println(movieResponseDto.getMovieName());
}
assertEquals(list.size(), 10);
}
그렇다면, 다시 한 번 셋하면 어떻게 될까? rightPush라면 저 테스트는 실패해야 한다 (10 + 10 = 20)
아주 잘 실패한다.
그래? 그럼 이걸 이용하면(rightPush) 회원 목록의 캐싱 관리나, 여러 "목록" 조회 캐싱이 유용할 것 같다!
@Test
void simpleSetAndGetTest() throws JsonProcessingException {
Movie movie = movieRepository.findById(3L).orElseThrow();
redisDAO.setValues("simple1", movie, Duration.ofMillis(10000));
redisDAO.getValues("simple", Movie.class);
}
즉 시 에 러!!
에러메시지를 읽어보면 movieImages를 초기화 하지 못해서(내가 lazy로 설정했기 때문에) 직렬화가 안된다고 한다. 그렇다면 나에게 두 가지 선택지가 있다.
명시적으로 movieImages 컬렉션을 초기화하기 위해서 쿼리를 발생시켜서 데이터들을 저장한다
@JsonIgnore 사용해서 직렬화에서 제외시킨다.
둘 다 한번 시도해보자!
어?? EAGER 로 바꿔도 되지 않나요?
-> 성능이 너무 나빠짐 (다른 곳에서 예상치 못한 쿼리가 발생할 것)
잘 통과하는 모습!
그런데 우리가 @JsonIgnore 해놓은 필드는 어떻게 처리되는지 궁금하다! 도커에서 레디스 클라이언트를 열어보자,
docker exec -it <container_id> redis-cli
위 명령어가 레디스 클라이언트 실행 명령어이다.
위는 클라이언트 아이디. daa~ 이부분이다. 옆 복사 버튼을 눌러 복사 후 명령어로 실행해주자!
실행된 모습. 아예 컬렉션 엔티티 필드를 가지지도 않는(직렬화 시도도 않는) 모습!
@Override
public MovieResponseDto findMovieAndCollections(Long movieId) {
return MovieResponseDto.of(jpaQueryFactory.selectFrom(movie)
.leftJoin(movie.movieImages, movieImage).fetchJoin()
.leftJoin(movie.movieVideos, movieVideo).fetchJoin()
.leftJoin(movie.castMembers, castMember).fetchJoin()
.where(movie.id.eq(movieId))
.fetchFirst());
}
조인을 통해서 연관 엔티티들을 가져와서 초기화시킨다. 타입은 ResponseDto로 바꿔줌(양방향 참조로 인한 무한 재귀호출을 막기위해)
그럴시에 이렇게 잘 초기화 되어, 직렬화된 데이터를 확인 가능하다.
이미지 아이디, 컨테이너 이름 등등 적절히 작성.
컨테이너 이름은, 이미 존재하는 컨테이너 이름과 중복되면 안된다!!
movie-batch 컨테이너의 경우에는 볼륨마운트 설정을 해줬기 때문에 volumes: 부분으로 마찬가지로 설정
위와 같이, docker-compose.yml 파일이 있는 곳으로 이동 후
docker-compose up
명령어를 사용하면 된다. -d 명령어를 추가해서 백그라운드로 실행시켜주자
잘 실행되는 모씁!
docker-compose ps
명령어를 통해 상태를 확인도 가능하다
Dockerfile에 설정한 커맨드, 이미지 등등 정보가 보인다.