Spring Batch 이용한 API 호출횟수 집계 - 6. Docker-Compose 이용한 컨테이너 관리 + Redis init

Kim Dong Kyun·2023년 6월 17일
1

Spring Batch

목록 보기
6/6
post-thumbnail

Batch 마지막!

저번 시간에 볼륨 마운트를 통해서 Docker 환경에서도 이메일이 정상적으로 잘 보내지도록 설정했다. 그렇다면 오늘은, Redis + 프로젝트 서버를 함께 올려보자.


Redis 설정 해주기 (프로젝트)

  1. build.gradle
dependencies {
	...
    // SpringBoot + Redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    ...
}
  1. application.yml
spring:
  data:
    redis:
      host: localhost
      port: 6379

기본 디폴트 설정이다. 사실 안해줘도 됨 (그러나 해주자. 이유는 아래에)

3. RedisConfig (Configuration class)

@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();
    }
}
  • host, port 는 우리가 application.yml 에 설정한 부분들
  • 저걸 위해 명시적 선언했다고 봐도 됨 거의
  1. ConnectionFactory는 lettuce를 사용한다

Jedis vs Lettuce

  • Lettuce는? : 높은 성능, 비동기 및 논블로킹 지원(놀랍다!), 클러스터 및 Sentinel 모드와 같은 기능 제공
  • 초기 설정이 어렵다(Jedis에 비해)
  1. RedisTemplate 설정
  • key,value 모두 StringRedisSerializer 사용한다
  • objectMapper 사용해서 직렬/역직렬화 해야하는 귀찮음은 있지만, 안정적이라는 장점이 있다 (직렬,역직렬화 잘 해줄 시)
  1. @Cachable, @CacheEvict 등의 사용 설정 - ReidsCacheManager
  • 마찬가지로 StringRedisSerializer 사용한다.
  • null값은 캐싱하지 않도록 초기 설정했다.

Docker 환경에서 레디스 띄우기

1. Docker에서 Redis 이미지 pull받기

docker pull redis:latest

해당 명령어를 통해서 Docker에서 레디스(최신버전)이미지를 풀받는다.

2. Docker run(컨테이너 생성)

docker run -d -p 6379:6379 --name "컨테이너 이름" redis
  • -d 명령어를 통해서 백그라운드에 실행
  • -p 명령어를 통해서 포트는 6379로 맞춤
  • --name 명령어를 통해 "컨테이너 이름" 설정
  • run 시킬 이미지인 "redis" 명시해줌

StringRedisSerializer 설정해주기

아까도 말했듯, StringRedisSerializer는 직렬화/역직렬화를 명시해줘야 하는데, 그 이유는 key : value 모두가 String으로만 들어가고 나오기 때문이다.

따라서 우리는 objectMapper를 통해서 Json 데이터로의 변환을 해줘야 한다. (ObjectMapper는 Jackson 라이브러리에 포함되어 있다)

1. setValue, getValue 부분 만져주기

   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란?

  • 따라서 명시적으로 따로 설정해줘야 한다. (그게 더 적절하다)


2. setListValues, getListValues 만져주기

    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

3. 정말로 순서대로 들어가는지 테스트해보자.

 	@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) 회원 목록의 캐싱 관리나, 여러 "목록" 조회 캐싱이 유용할 것 같다!


4. setValue, getValue도 테스트 해보자.

    @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로 설정했기 때문에) 직렬화가 안된다고 한다. 그렇다면 나에게 두 가지 선택지가 있다.

  1. 명시적으로 movieImages 컬렉션을 초기화하기 위해서 쿼리를 발생시켜서 데이터들을 저장한다

  2. @JsonIgnore 사용해서 직렬화에서 제외시킨다.

둘 다 한번 시도해보자!

어?? EAGER 로 바꿔도 되지 않나요?
-> 성능이 너무 나빠짐 (다른 곳에서 예상치 못한 쿼리가 발생할 것)

  1. @JsonIgnore

잘 통과하는 모습!

그런데 우리가 @JsonIgnore 해놓은 필드는 어떻게 처리되는지 궁금하다! 도커에서 레디스 클라이언트를 열어보자,

docker exec -it <container_id> redis-cli

위 명령어가 레디스 클라이언트 실행 명령어이다.

위는 클라이언트 아이디. daa~ 이부분이다. 옆 복사 버튼을 눌러 복사 후 명령어로 실행해주자!

실행된 모습. 아예 컬렉션 엔티티 필드를 가지지도 않는(직렬화 시도도 않는) 모습!

  1. 연관된 엔티티 조인으로 처리
   @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로 바꿔줌(양방향 참조로 인한 무한 재귀호출을 막기위해)

그럴시에 이렇게 잘 초기화 되어, 직렬화된 데이터를 확인 가능하다.


Docker Compose 활용하기

1. docker-compose.yml 작성

이미지 아이디, 컨테이너 이름 등등 적절히 작성.

컨테이너 이름은, 이미 존재하는 컨테이너 이름과 중복되면 안된다!!

movie-batch 컨테이너의 경우에는 볼륨마운트 설정을 해줬기 때문에 volumes: 부분으로 마찬가지로 설정

2. 올리기

위와 같이, docker-compose.yml 파일이 있는 곳으로 이동 후

docker-compose up

명령어를 사용하면 된다. -d 명령어를 추가해서 백그라운드로 실행시켜주자

잘 실행되는 모씁!

docker-compose ps

명령어를 통해 상태를 확인도 가능하다

Dockerfile에 설정한 커맨드, 이미지 등등 정보가 보인다.

0개의 댓글