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.