ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Jedis를 활용하여 Redis에 JSON Path 기능을 테스트
    Spring Boot/기타 2024. 12. 11. 23:00
    반응형

     

    목차

       

       

      Jedis 라이브러리를 사용하여 Redis에 JSON Path 기능을 활용하는 방법을 설명하겠습니다.

       

      1. JSONPath & Redis JSON 개요

      JSONPath는 JSON 문서에서 데이터를 검색하거나 필터링할 때 유용한 쿼리 언어입니다. Redis는 JSON 데이터 타입을 직접 지원하며, JSONPath를 통해 데이터를 효과적으로 쿼리할 수 있습니다.

       

      JSONPath 주요 표현식

      방식 표현식 예제 및 설명
      전체 매칭 $ JSON 문서의 루트 노드를 선택합니다.
      예: $
      경로 접근 . 특정 속성을 선택합니다.
      예: $.store.book
      배열 처리 [index] 또는 [*] 배열 처리 [index] 또는 [*] 배열 인덱스로 접근하거나 배열의 모든 요소를 처리합니다.
      예: $.store.book[0] (첫 번째 책), $.store.book[*] (모든 책)
      재귀 탐색 .. 모든 하위 노드에서 속성을 재귀적으로 검색합니다.
      예: $..author (JSON 내 모든 author 속성 검색)
      조건 필터링 ?(expression) 조건식에 따라 데이터를 필터링합니다.
      예: $..book[?(@.price < 10)] (가격이 $10 미만인 책만 검색)
      존재 체크 @ 조건 식에서 현재 항목의 값을 참조합니다.
      예: $..book[?(@.isbn)] (ISBN 값이 존재하는 책만 검색)
      슬라이스(Slice) [start:end], [start:], [:end] 배열의 특정 범위 데이터를 가져옵니다.
      예: $.store.book[0:2] (처음 두 권의 책)

       

      Redis JSONPath 활용 예제
      다음은 Redis에 저장된 JSON 데이터를 바탕으로 JSONPath를 활용하는 예제입니다.

      {
        "store": {
          "book": [
            {
              "category": "reference",
              "author": "Nigel Rees",
              "title": "Sayings of the Century",
              "isbn": null,
              "price": 8.95,
              "inStock": true,
              "sold": true
            },
            {
              "category": "fiction",
              "author": "Evelyn Waugh",
              "title": "Sword of Honour",
              "isbn": null,
              "price": 12.99,
              "inStock": false,
              "sold": true
            },
            {
              "category": "fiction",
              "author": "Herman Melville",
              "title": "Moby Dick",
              "isbn": "0-553-21311-3",
              "price": 8.99,
              "inStock": true,
              "sold": false
            },
            {
              "category": "fiction",
              "author": "J. R. R. Tolkien",
              "title": "The Lord of the Rings",
              "isbn": "0-395-19395-8",
              "price": 22.99,
              "inStock": false,
              "sold": false
            }
          ],
          "bicycle": {
            "color": "red",
            "price": 19.95,
            "inStock": true,
            "sold": false
          }
        }
      }

       

      JSONPath 쿼리 및 출력 결과

       

      위의 결과는 제공된 JSON 데이터를 대상으로 JSONPath 쿼리를 실행한 결과입니다. Redis JSON 모듈과 Jedis를 사용하면 이러한 JSONPath 쿼리를 사용해 데이터를 Redis에서 효율적으로 조회할 수 있습니다.

      JSONPath 쿼리 설명 출력 결과
      $.store.book[*].author store의 각 book의 author를 가져옵니다. ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
      $..author 전체 author를 가져옵니다. ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
      $.store.* store 객체의 모든 속성을 가져옵니다. [[{"sold":true,"author":"Nigel Rees","price":8.95,"inStock":true,"category":"reference","title":"Sayings of the Century"},{"sold":true,"author":"Evelyn Waugh","price":12.99,"inStock":false,"category":"fiction","title":"Sword of Honour"},{"sold":false,"author":"Herman Melville","price":8.99,"isbn":"0-553-21311-3","inStock":true,"category":"fiction","title":"Moby Dick"},{"sold":false,"author":"J. R. R. Tolkien","price":22.99,"isbn":"0-395-19395-8","inStock":false,"category":"fiction","title":"The Lord of the Rings"}],{"sold":false,"color":"red","price":19.95,"inStock":true}]
      $..book[?(@.isbn)] ISBN이 있는 책을 가져옵니다. [{"sold":false,"author":"Herman Melville","price":8.99,"isbn":"0-553-21311-3","inStock":true,"category":"fiction","title":"Moby Dick"},{"sold":false,"author":"J. R. R. Tolkien","price":22.99,"isbn":"0-395-19395-8","inStock":false,"category":"fiction","title":"The Lord of the Rings"}]
      $..book[?(@.price < 10)] 가격이 $10 미만인 모든 책을 가져옵니다. [{"sold":true,"author":"Nigel Rees","price":8.95,"inStock":true,"category":"reference","title":"Sayings of the Century"},{"sold":false,"author":"Herman Melville","price":8.99,"isbn":"0-553-21311-3","inStock":true,"category":"fiction","title":"Moby Dick"}]
      $..book[?(@.price >= 10 && @.price <= 100)] 가격이 $10 이상 $100 이하인 모든 책을 가져옵니다. [{"sold":true,"author":"Evelyn Waugh","price":12.99,"inStock":false,"category":"fiction","title":"Sword of Honour"},{"sold":false,"author":"J. R. R. Tolkien","price":22.99,"isbn":"0-395-19395-8","inStock":false,"category":"fiction","title":"The Lord of the Rings"}]
      $.store.book[?(@.["category"] == "fiction")] category가 fiction인 모든 책을 가져옵니다. [{"sold":true,"author":"Evelyn Waugh","price":12.99,"inStock":false,"category":"fiction","title":"Sword of Honour"},{"sold":false,"author":"Herman Melville","price":8.99,"isbn":"0-553-21311-3","inStock":true,"category":"fiction","title":"Moby Dick"},{"sold":false,"author":"J. R. R. Tolkien","price":22.99,"isbn":"0-395-19395-8","inStock":false,"category":"fiction","title":"The Lord of the Rings"}]

       

      참고

      Redis JSONPath 문서

      JsonPath GitHub Repository

       

      2. Docker를 활용하여 Redis 환경 설정

      Docker를 사용하여 Redis 서버를 설치 및 실행합니다.

      docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest
      • 포트 6379: Redis 서버와의 기본 연결
      • 포트 8001: RedisInsight와 같은 GUI 도구와의 연결

      참고

      Docker Hub - redis/redis-stack

       

      3. 의존성 추가

      build.gradle 파일에 Redis 및 Jedis와의 연동에 필요한 의존성을 추가합니다.

      build.gradle

      dependencies {
          // Redis
          implementation('org.springframework.boot:spring-boot-starter-data-redis') {
              exclude group: 'io.lettuce', module: 'lettuce-core'
          }
      
          // Jedis
          implementation 'redis.clients:jedis:5.1.0'
      }

      참고

      Maven Repository - Jedis

       

      4. RedisConfig 설정

      Redis 서버와의 연동을 위해 JedisPooled 객체를 빈으로 등록합니다.

       

      RedisConfig.java

      package com.example.jedis.config;
      
      import java.util.Objects;
      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.jedis.JedisConnectionFactory;
      import redis.clients.jedis.JedisPooled;
      
      @Configuration
      public class RedisConfig {
      
        @Value("${redis.stand-alone.host}")
        private String standAloneHost;
      
        @Value("${redis.stand-alone.port}")
        private String standAlonePort;
      
        /**
         * RedisConnectionFactory 빈을 생성
         *
         * @return RedisConnectionFactory 빈
         */
        @Bean
        public RedisConnectionFactory redisConnectionFactory() {
          return new JedisConnectionFactory(
              new RedisStandaloneConfiguration(standAloneHost, Integer.parseInt(standAlonePort)));
        }
      
        /**
         * RedisConnectionFactory를 사용하여 JedisPooled 빈을 생성
         *
         * @param redisConnectionFactory RedisConnectionFactory
         * @return JedisPooled 빈
         */
        @Bean
        public JedisPooled jedisPooled(RedisConnectionFactory redisConnectionFactory) {
          JedisConnectionFactory jedisConnectionFactory =
              (JedisConnectionFactory) redisConnectionFactory;
          return new JedisPooled(
              Objects.requireNonNull(jedisConnectionFactory.getPoolConfig()),
              jedisConnectionFactory.getHostName(),
              jedisConnectionFactory.getPort());
        }
      }

       

      5. RedisComponent 구현

      RedisComponent 클래스는 Jedis를 활용해 객체의 JSON 변환 및 저장, 조회와 같은 기능을 제공합니다.

      RedisComponent에서 사용한 Jedis 메서드

      • set(String key, String value) : 주어진 키에 문자열 값을 저장합니다. 기존 값이 있다면 덮어씁니다.
      • expire(String key, long seconds) : 특정 키에 대한 만료 시간을 설정합니다. 초 단위로 설정되며, 해당 시간이 지나면 키와 값이 삭제됩니다.
      • get(String key) : 특정 키와 연관된 값을 조회합니다. 키가 존재하지 않으면 null을 반환합니다.
      • jsonSetWithEscape(String key, Object t) : 객체를 JSON 형식으로 변환하고, 지정된 키에 저장합니다.
      • jsonGet(String key) : JSON 형태로 저장된 데이터를 객체로 변환하여 가져옵니다. 저장된 데이터가 없는 경우 null을 반환합니다.

      참고

      Jedis GitHub Repository

      Jedis JavaDocs (공식 API 문서)

      Redis 공식 사이트 (Redis Commands)

       

      RedisComponent의 메서드
      RedisComponent는 Jedis 메서드들을 활용하여 Redis 서버와의 연동을 위해 필요한 메서드를 제공합니다.

      • setJson(String key, T t, long ttl) : jsonSetWithEscape를 사용하여 객체를 JSON으로 변환한 후 저장하고, expire를 활용하여 키의 만료 시간을 설정합니다.
      • getJsonArray(String key, String path) : jsonGet 메서드 호출 시 경로(path)를 추가하여 지정된 경로의 JSON 배열을 조회합니다. 데이터가 없는 경우 null을 반환합니다.
      • getJsonObject(String key, Class<T> clazz) : jsonGet을 사용해 특정 키의 데이터를 가져오고, 지정된 클래스 타입(clazz)으로 변환하여 반환합니다.
      • getJsonList(String key, Class<T> clazz, String path) : jsonGet을 활용하여 경로(path)에 해당하는 데이터를 조회하고, 이를 리스트 형태로 변환하여 반환합니다.

       

      RedisComponent.java

      package com.example.jedis.component;
      
      import com.fasterxml.jackson.core.JsonProcessingException;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import com.fasterxml.jackson.databind.type.TypeFactory;
      import java.util.Collections;
      import java.util.List;
      import lombok.RequiredArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.json.JSONArray;
      import org.springframework.stereotype.Component;
      import org.springframework.util.ObjectUtils;
      import redis.clients.jedis.JedisPooled;
      import redis.clients.jedis.json.Path2;
      
      @Slf4j
      @RequiredArgsConstructor
      @Component
      public class RedisComponent {
      
        private final JedisPooled jedisPooled;
        private final ObjectMapper objectMapper;
      
        /**
         * 객체를 JSON 형식으로 저장하고, 지정된 TTL(만료 시간)을 설정
         *
         * @param key 저장할 키
         * @param t   저장할 객체
         * @param ttl 만료 시간(초 단위)
         * @param <T> 저장할 객체의 유형
         */
        public <T> void setJson(String key, T t, long ttl) {
          jedisPooled.jsonSetWithEscape(key, t);
          jedisPooled.expire(key, ttl);
        }
      
        /**
         * 키와 경로를 사용하여 JSONArray를 조회
         *
         * @param key  검색할 키
         * @param path JSONPath 표현식으로, JSON 내에서 데이터를 검색할 경로
         * @return 키와 경로에 해당하는 JSON 배열을 반환. 키 또는 경로가 유효하지 않으면 null을 반환
         */
        public JSONArray getJsonArray(String key, String path) {
          return (JSONArray) jedisPooled.jsonGet(key, Path2.of(path));
        }
      
        /**
         * 키를 사용하여 저장된 객체를 조회
         *
         * @param key   검색할 키
         * @param clazz 검색할 객체의 클래스 유형
         * @param <T>   검색할 객체의 유형
         * @return 저장된 객체를 반환. JSON 파싱 실패 시 null을 반환
         */
        public <T> T getJsonObject(String key, Class<T> clazz) {
      
          try {
            Object object = jedisPooled.jsonGet(key);
      
            if (object != null && !ObjectUtils.isEmpty(object.toString())) {
              String jsonString = objectMapper.writeValueAsString(object);
              return objectMapper.readValue(jsonString, clazz);
            }
          } catch (JsonProcessingException e) {
            log.error("getJson key : {}", key, e);
          }
      
          return null;
        }
      
        /**
         * 키와 경로를 사용하여 저장된 리스트를 조회
         *
         * @param key   검색할 키
         * @param clazz 검색할 객체의 클래스 유형
         * @param path  JSONPath 표현식으로, JSON 내에서 데이터를 검색할 경로
         * @param <T>   검색할 객체의 유형
         * @return 저장된 리스트를 반환. JSON 파싱 실패 시 빈 리스트를 반환
         */
        public <T> List<T> getJsonList(String key, Class<T> clazz, String path) {
      
          try {
            Object object = jedisPooled.jsonGet(key, Path2.of(path));
      
            if (object != null && !ObjectUtils.isEmpty(object.toString())) {
              TypeFactory typeFactory = objectMapper.getTypeFactory();
              return objectMapper.readValue(object.toString(),
                  typeFactory.constructCollectionType(List.class, clazz));
            }
      
          } catch (JsonProcessingException e) {
            log.error("getJsonArray key : {}", key, e);
          }
      
          return Collections.emptyList();
        }
      }

       

      6. 테스트 클래스 구현

      JsonDataTypeTest 클래스를 통해 Redis에 JSON Path 기능을 테스트합니다.

       

      메서드 설명

      • testSetAndGetJsonObject() : Redis에 객체 데이터를 JSON 형식으로 저장(setJson)하고, 이를 객체(getJsonObject)로 조회하여 유효성을 확인합니다.
      • testSetAndGetJsonArray() : Redis에서 특정 JSON Path 경로를 사용하여 JSONArray 데이터를 조회(getJsonArray)합니다. 여러 JSON Path를 통해 데이터를 확인하며, 유효성을 테스트합니다.
      • testSetAndGetJsonList() : Redis에서 특정 JSON Path 경로를 통해 리스트 형태의 데이터를 조회(getJsonList)하여 유효성을 확인합니다.


      JsonDataTypeTest.java

      package com.example.jedis.service;
      
      import static org.junit.jupiter.api.Assertions.assertAll;
      import static org.junit.jupiter.api.Assertions.assertFalse;
      import static org.junit.jupiter.api.Assertions.assertNotNull;
      
      import com.example.jedis.component.RedisComponent;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import java.util.List;
      import lombok.AllArgsConstructor;
      import lombok.Builder;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      import lombok.extern.slf4j.Slf4j;
      import org.json.JSONArray;
      import org.junit.jupiter.api.DisplayName;
      import org.junit.jupiter.api.MethodOrderer;
      import org.junit.jupiter.api.Order;
      import org.junit.jupiter.api.Test;
      import org.junit.jupiter.api.TestMethodOrder;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.boot.test.context.SpringBootTest;
      
      /**
       * Jedis를 활용하여 Redis에 JSON Path 기능을 테스트
       * <p>
       * <a
       * href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/json-document-overview.html">Json
       * data type overview</a>
       */
      @Slf4j
      @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
      @SpringBootTest
      public class JsonDataTypeTest {
      
        private static final String REDIS_KEY = "k1";
        private static final long TTL_SECONDS = 60 * 10;
      
        @Autowired
        RedisComponent redisComponent;
      
        @Autowired
        ObjectMapper objectMapper;
      
        @Order(1)
        @DisplayName("객체를 JSON 형식으로 저장하고, 키를 사용하여 객체를 조회")
        @Test
        void testSetAndGetJsonObject() throws Exception {
      
          // Given
          StoreDto storeDto = createStoreDto();
          redisComponent.setJson(REDIS_KEY, storeDto, TTL_SECONDS);
      
          // When
          StoreDto retrievedStore = redisComponent.getJsonObject(REDIS_KEY, StoreDto.class);
          log.debug("testSetAndGetJsonObject retrievedStore : {}",
              objectMapper.writeValueAsString(retrievedStore));
      
          // Then
          assertAll(
              () -> assertNotNull(retrievedStore),
              () -> assertFalse(retrievedStore.getStore().getBook().isEmpty()),
              () -> assertNotNull(retrievedStore.getStore().getBicycle())
          );
        }
      
        @Order(2)
        @DisplayName("키와 경로를 사용하여 JsonArray를 조회")
        @Test
        void testSetAndGetJsonArray() {
      
          // Given
          StoreDto storeDto = createStoreDto();
          redisComponent.setJson(REDIS_KEY, storeDto, TTL_SECONDS);
      
          // When & Then
          for (String jsonPath : initJsonPaths()) {
      
            JSONArray jsonArray = redisComponent.getJsonArray(REDIS_KEY, jsonPath);
            log.debug("testSetAndGetJsonArray jsonPath {} : jsonArray : {}",
                jsonPath, jsonArray.toString());
      
            assertNotNull(jsonArray);
          }
        }
      
        @Order(3)
        @DisplayName("키와 경로를 사용하여 JsonList를 조회")
        @Test
        void testSetAndGetJsonList() throws Exception {
      
          // Given
          StoreDto storeDto = createStoreDto();
          redisComponent.setJson(REDIS_KEY, storeDto, TTL_SECONDS);
      
          // When
          List<StoreDto> storeList = redisComponent.getJsonList(REDIS_KEY, StoreDto.class, "$");
          log.debug("storeList : {}", objectMapper.writeValueAsString(storeList));
      
          // Then
          assertNotNull(storeList);
        }
      
        private StoreDto createStoreDto() {
      
          List<StoreDto.Store.Book> books = List.of(
              StoreDto.Store.Book.of("reference", "Nigel Rees", "Sayings of the Century",
                  null, 8.95, true, true),
              StoreDto.Store.Book.of("fiction", "Evelyn Waugh", "Sword of Honour",
                  null, 12.99, false, true),
              StoreDto.Store.Book.of("fiction", "Herman Melville", "Moby Dick",
                  "0-553-21311-3", 8.99, true, false),
              StoreDto.Store.Book.of("fiction", "J. R. R. Tolkien", "The Lord of the Rings",
                  "0-395-19395-8", 22.99, false, false)
          );
      
          StoreDto.Store.Bicycle bicycle = StoreDto.Store.Bicycle.of("red", 19.95, true, false);
          return StoreDto.of(StoreDto.Store.of(books, bicycle));
        }
      
        private String[] initJsonPaths() {
          return new String[]{
              "$.store.book[*].author",         // store의 각 book의 author
              "$..author",                      // JSON의 전체 author
              "$.store.*",                      // store 객체의 전체 속성
              "$[\"store\"].*",                 // store 객체의 전체 속성 (다른 접근 방법)
              "$.store..price",                 // store 내 전체 속성의 price
              "$..*",                           // JSON 구조의 전체 요소
              "$..book[*]",                     // 각각의 book 객체
              "$..book[0]",                     // 첫 번째 book
              "$..book[-1]",                    // 마지막 book
              "$..book[0:2]",                   // 처음 두 권의 book
              "$..book[0,1]",                   // 첫 번째와 두 번째 book
              "$..book[0:4]",                   // 인덱스 0부터 3까지의 book
              "$..book[0:4:2]",                 // 인덱스 0과 2의 book
              "$..book[?(@.isbn)]",             // ISBN 있는 각각의 book
              "$..book[?(@.price<10)]",         // 가격이 $10 미만인 각각의 book
              "$..book[?(@[\"price\"] < 10)]",  // 가격이 $10 미만인 각각의 book (다른 접근 방법)
              "$..book[?(@.price>=10&&@.price<=100)]",  // 가격이 $10 이상 $100 이하인 각각의 book
              "$..book[?(@.sold==true||@.in-stock==false)]",  // sold가 true이거나 in-stock이 false인 각각의 book
              "$.store.book[?(@.[\"category\"] == \"fiction\")]", // fiction 카테고리의 각 book
              "$.store.book[?(@.[\"category\"] != \"fiction\")]"  // fiction 카테고리에 속하지 않는 각각의 book
          };
        }
      
        @Getter
        @Builder
        @AllArgsConstructor(staticName = "of")
        @NoArgsConstructor
        public static class StoreDto {
      
          private Store store;
      
          @Getter
          @Builder
          @AllArgsConstructor(staticName = "of")
          @NoArgsConstructor
          public static class Store {
      
            private List<Book> book;
            private Bicycle bicycle;
      
            @Getter
            @Builder
            @AllArgsConstructor(staticName = "of")
            @NoArgsConstructor
            public static class Book {
      
              private String category;
              private String author;
              private String title;
              private String isbn;
              private Double price;
              private Boolean inStock;
              private Boolean sold;
            }
      
            @Getter
            @Builder
            @AllArgsConstructor(staticName = "of")
            @NoArgsConstructor
            public static class Bicycle {
      
              private String color;
              private Double price;
              private Boolean inStock;
              private Boolean sold;
            }
          }
        }
      }

       

      자세한 소스 코드는 Github Repository를 참조하세요.

       

      관련 글

      [Spring Boot] Jedis를 활용하여 Redis에 대한 CRUD 작업을 테스트

      JSONPath로 JSON 데이터 다루기

      반응형

      댓글

    Designed by Tistory.