ebson

[JSON 라이브러리 pull request] JSON 분리 기능 개발해보기 본문

HANDS-ON

[JSON 라이브러리 pull request] JSON 분리 기능 개발해보기

ebson 2023. 2. 24. 15:36

실무에서 JSON 데이터를 외부 API ENDPOINT로 POST 전송하는 것은 흔한 요구사항이다. XML 포맷에 비해 JSON 포맷을 사용할 때의 이점은 최소한의 용량으로 가독성이 더 좋은 데이터를 주고 받을 수 있다는 것이다.

 

내 경우는 기존 XML파일로 생성하던 데이터들을 JSON 방식으로 변환해 전송하는 요구사항과 데이터베이스 등에 적재된 데이터들을 검색 페이지에 사용하기 위한 용도로 JSON 방식으로 전송하는 요구사항이 있었다. 두 요구사항 모두 외부 API ENDPOINT 으로 JSON을 POST 요청하는 요구사항이다.

 

첫번째 요구사항에 대해 유틸 클래스를 개발해본 경험에 대한 내용은 다른 글에서 정리하도록 하겠다. 이 글에서는 최소한의 용량으로 압축한 JSON 포맷의 데이터마저 API ENDPOINT 서버에서 감당하지 못하거나 413 Request Entity Too Large 를 유발하는 경우를 대비해 유틸 클래스를 개발한 경험을 정리하겠다.

 

중요한 것은 이것은 실무에 반영하고자 개발한 것은 아니었다. 실무에서는 대용량 JSON을 보내야 하는 경우에 대비해 먼저 서버 버퍼 사이즈 증강을 요청했고, 그렇게 해도 해결이 안되는 것은 보내는 측에서 여러번에 걸쳐 나눠서 보내는 방법으로 간단하게 해결되었다. 그럼에도 불구하고 JSON을 분해하고 분리하는 기능을 구현해본 이유는 실무에서 사용중이던 JSON 라이브러리가 오픈 소스였고 pull request를 보내보기 위함이었다. 출퇴근 전후 시간 등을 활용했다. 결과적으로 2번 요청했고 2번 comment를 받았고 request가 수락되지는 않았다.

 

자바 JSON 라이브러리에서 제공하지 않는 JSON 분해, 분할 기능

JSON 데이터 용량을 서버에서 감당 못하는 경우에 대비하기 위해 먼저 시도한 것은 사용 중인 JSON 라이브러리 내부에서 해당 기능을 제공하는지 뒤져본 것이다. XML 파일을 JSON 포맷으로 변경하기 위해 사용한 방법들 중 하나이기 때문이다. 그러나 프로젝트에서 사용중인 어떤 JSON 라이브러리들(org.json, jsonsimple, gson 등등)에서도 JSON을 분해, 분할하거나 그것을 돕는 기능을 찾을 수 없었다.

 

그래서 내 몫의 작업들을 빨리 완료하고 위 기능을 직접 개발 시도 했다. 테스트 일정은 2달 정도 남았었기 때문이다. 작업들을 모두 마쳐놓고 JSON 라이브러리에서 제공하는 기능들과 웹에 공유된 JSON 라이브러리를 응용한 소스들을 분석했다. 웹에서도 JSON 분해, 분할 기능이 구현된 소스가 공유된 곳은 못찾고 그것에 도움이 될만한 소스가 있어 참조했다.

 

원했던 동작은 첫째, JSON 문자열이 주어지면 마지막 깊이의 JSONObject까지 검사하는 것이고 둘째, 특정 바이트 크기 이상으로 검사될 경우에 내부의 JSONArray를 분리해 내는 것이었고 셋째, 분리해낸 JSONArray이 특정 바이트 크기 이상으로 검사될 경우 그것도 여러개의 JSONArray로 분할하는 것이었다. 그리고 이 조각난 JSON들이 모두 결과적으로는 외부 API ENDPOINT 에서 요구하는 JSON 포맷에 맞도록 포장되어서 빠짐없이 전송 가능한 것이었다.

 

 

기본 제공하는 기능들을 응용해 구현하기

그래서 JSON 문자열을 인자로 받아서 첫번째 깊이의 JSON 값들을 모두 순회하면서 JSONArray 형식인지 JSONObject 형식인지 단순 문자열인지 확인하는 함수를 작성했다. 그리고 JSONArray 형식인 경우에는 바이트길이를 검사해 특정 길이를 초과하는 경우 여러개의 SubJsonList 으로 분할하도록 하고 JSONObject 형식인 경우에는 자기 자신을 한번 더 호출해 다음 깊이를 검사한 후 결과를 반환하도록 하며 단순 문자열인 경우에는 원본 그대로 반환하도록 했다. 

			...
            currJson.put(key, new JSONArray());
            if(getValue instanceof JSONArray) {
                List<Map<String, Object>> subList = JSONDivider.toList((JSONArray)getValue);
                JSONObject subJson = new JSONObject();
                subJson.put(key, subList);
                List<String> temp = funcDividingOrNot(subJson.toString(), key, MAX_SIZE-FIT_SIZE);
                for(String jsonStr : temp) {
                    Map<String, String> map = new HashMap<String, String>();
                    String ListKey = getUpperKeyString(key);
                    checkContainsListKey(key, ListKey, new JSONObject(jsonStr), map);
                    JsonStrMapList.add(map);
                }
            } else if(getValue instanceof JSONObject) {
                Object rtnValue = funcDisassembling(getValue.toString(), MAX_SIZE);
                currJson.put(key, rtnValue);
            } else {
                currJson.put(key, getValue);
            }
            ...

 

 

다음으로 분리 검사 대상이 된 JSONArray가 특정 바이트 크기를 초과하는 경우에 몇개의 SubJSONArray 으로 분할해야 하는지를 계산하고 , 분할한 결과 SubJSONArray 문자열 모두를 자바 List 으로 반환하도록 했다. 그리고 계산된 SubJSONArray의 요소 개수로 나누어떨어지지 않는 JSONArray 인 경우에는, java.lang.IndexOutOfBoundsException 이 발생하는 것을 방지하기 위해 마지막 SubJSONArray는 원본 JSONArray의 마지막 요소까지만 추출하도록 했다. 그리고 SubJSONArray를 최종 전송 대상 JSON 문자열 List에 추가했다. 특정 바이트 크기를 초과하지 않는 경우에는 JSONArray를 분할하지 않은 채로 최종 전송 대상 JSON 문자열 List에 추가했다.

		...
        JSONObject json = new JSONObject(dividedJsonArrayStr);
        JSONArray dividedJa = json.getJSONArray(key);
        int dividedJaStrLength = dividedJa.toString().getBytes().length;
        json.put(key, new JSONArray());
        List<String> tempList = new ArrayList<String>();

        if(dividedJaStrLength > MAX_SIZE) {
            boolean isUnderMaxSize = false;
            int cnt = (int) Math.ceil(((double)dividedJaStrLength/MAX_SIZE));
            while(isUnderMaxSize==false){
                tempList = new ArrayList<String>();
                List<Map<String, Object>> list = JSONDivider.toList(dividedJa);
                int size = list.size();
                int idx = 0;
                for(int i = 1; i<=cnt; i++) {
                    List<Map<String, Object>> subList = new ArrayList<Map<String, Object>>();
                    int eIdx = i==cnt ? size : idx+(size + 1) / cnt;
                    subList = list.subList(idx, eIdx);
                    idx = eIdx;
                    JSONObject subJson = new JSONObject();
                    subJson.put(key, subList);
                    String sendingJsonStr = subJson.toString();
                    tempList.add(sendingJsonStr);
                }
                for (int i=0; i<=tempList.size(); i++){
                    if(i == tempList.size()){
                        isUnderMaxSize = true; break;
                    }
                    if (tempList.get(i).getBytes().length > MAX_SIZE){
                        cnt += 1; break;
                    }
                }
            }
        } else {
            JSONObject subJson = new JSONObject();
            subJson.put(key, dividedJa);
            String sendingJsonStr = subJson.toString();
            tempList.add(sendingJsonStr);
        }
        ...

 

마지막으로 API ENDPOINT에서 요구하는 형식에 맞춰진 원본 JSON 문자열을 다시 마지막 깊이까지 검사하면서, 분할된 JSONArray 또는 SubJSONArray 를 각각 포함하면서 그 외 JSON 값들은 제외된 JSONObject를 생성하고, 생성된 N개의 JSON 문자열을 List에 저장해 반환하도록 하는 함수를 작성했다. 이 함수 역시 마지막 깊이까지 검사해야 하기 때문에 JSONObject 형식인 경우에는 자기 자신을 한번 더 호출하고 문자열인 경우에는 원본 그대로 반환하도록 했다.

			...
            Object getValue = currJson.get(key);
            currJson.put(key, new JSONArray());

            if(getValue instanceof JSONArray) {
                List<Map<String, Object>> subList = (List<Map<String, Object>>)JSONDivider.toList((JSONArray)getValue);
                JSONDivider.checkContainsKey(currJson, map, key);
            } else if(getValue instanceof JSONObject) {
                Object rtnValue = setParsedJsonObjects(getValue.toString(), map);
                JSONDivider.checkContainsKey(currJson, map, key);
                if(!map.containsKey(key)){ currJson.put(key, rtnValue);}
            } else {
                JSONDivider.checkContainsKey(currJson, map, key);
                if(!map.containsKey(key)){ currJson.put(key, getValue);}
            }
            ...

 

 

주어진 JSON 데이터셋으로 실행 결과 확인하기

위 함수들을 순서대로 작성하면서 주어진 JSON 데이터셋으로 여러번 돌려보면서 원하는 형식으로 분해, 분할될 때까지 수정하고 추가했다. 주어진 데이터셋이 크게 두 종류였기 때문에 각각의 케이스에 맞게 분리, 분할 되도록 해야했다. 그리고 유틸 클래스로 분리해서 어디서나 호출할 수 있도록 했다. 유틸 클래스에 최종 분해, 분할된 JSON문자열을 담을 List를 초기화하고 분해, 분할, 저장 기능을 차례대로 실행할 수 있도록 했다.

 

데이터셋 케이스A의 경우 가장 낮은 깊이의 JSONArray 안에 JSONObject 요소가 단 하나였다. 이 경우에는 그 JSONObject를 마지막 깊이까지 검사하면서 JSONArray를 분할했다. 예를 들어, 아래와 같다.

{ ... "items" : { ... "a" : [{ ... }], ... , "b" : [{ ... }] ...  }} 형식의 JSON 문자열을 { ... "items" : { ... "a" : [{ … }], ... , "b" : [] ...  }}  ,
{ ... "items" : { ... "a" : [], ... , "b" : [{ … }] ...  }} ... 과 같이 분할했다. 

[ CASE A 분할결과 - 첫째문단:앞부분, 둘째문단:뒷부분, 셋째줄:로그 ]

 

데이터셋 케이스B의 경우 가장 낮은 깊이의 JSONArray안에 JSONObject가 2개 이상이었다. 이 경우에는 더 내부 깊이의 JSONArray가 최대 바이트 크기를 넘을 가능성이 적었고 가장 낮은 깊이의 JSONArray를 여러개의 SubJSONArray 으로 분할했다. 예를 들어, 다음과 같다. { ... "items" : [{ ... }, { ... }, { ... }, { ... } ... ] ... } 형식의 JSON 문자열을  { “items” : [{ … }, { … }, … ] } ,  { “items” : [{ … }, { … }, … ] } ... 과 같이 분할했다.

 

[ CASE B 분할결과 - 첫째줄:앞부분, 둘째줄:로그 ]

 

JUNIT 테스트

위 기능을 클래스에서 메서드 호출로 간단히 사용할 수 있도록 추가했다. 세번째 인자로 isLogging 값을 주어서 로그 여부를 선택할 수 있다. isLogging 값을 true으로 주면, 각 분리된 json string과 그 길이를 출력한다. 이 메서드를 사용해서 분리된 모든 JSONObject 들이 MAX_SIZE 이하의 길이를 갖는지 검사하는 단위 테스트 코드를 작성했다. 단위 테스트 코드를 작성한 이유는 이것이 org.json 오픈 소스 라이브러리 문서에서 pull request에 대해 일반적으로 요구하는 것이기 때문이다. 

 

package org.json.junit;

import org.json.JSONDivider;
import org.json.JSONObject;
import org.junit.Test;

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;

import static org.junit.Assert.assertTrue;

/**
 * You can get details about JSONDivider in /JSON-java/docs/JSONDivider/README.md
 *
 * First of all, JSONDivider take off JSONArray from original JSONObject if it has more length then MAX_SIZE.
 * The divided target would be JSONArray. if your JSONObject has multiple JSONArray, JSONDivider examine each length of them.
 * If a JSONArray has more length then MAX_SIZE, JSONDivider would divide it.
 * Divided JSONObjects whould maintain original structure but its inside JSONArray(s) may be divided.
 * If you want use it appropriately and want to get benefit from it,
 * !! JSONDivider need specific JSONObject structure, not random one. !!
 * */
public class JSONDividerTest {

    /**
     * JSONDivider can divide a JSON String which has structure like below.
     * - {"items":{"Products":{"Product":[{"key1":"value1", "key2":"value2"}, {"key1":"value1", "key2":"value2"}, {"key1":"value1", "key2":"value2"}]}}}
     * */
    @Test
    public void testAItems(){
        try {
            int MAX_SIZE = 100000;
            String AItems = new String(Files.readAllBytes(Paths.get("docs/JSONDivider/AItems.json"))).toString();
            JSONObject AJson = new JSONObject(AItems);

            List<String> strList = JSONDivider.divideJsonStr(AJson.toString(), MAX_SIZE, false);

            for(String jsonStr : strList){
                assertTrue("It is resized under MAX_SIZE you give.", jsonStr.getBytes().length < MAX_SIZE);
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * JSONDivider can divide a JSON String which has structure like below.
     * - {"items":[{"key1":"value1", "key2":"value2"}, {"key1":"value1", "key2":"value2"}, {"key1":"value1", "key2":"value2"}]}
     * */
    @Test
    public void testBItems(){
        try {
            int MAX_SIZE = 100000;
            String BItems = new String(Files.readAllBytes(Paths.get("docs/JSONDivider/BItems.json"))).toString();
            JSONObject BJson = new JSONObject(BItems);

            List<String> strList = JSONDivider.divideJsonStr(BJson.toString(), MAX_SIZE, false);

            for(String jsonStr : strList){
                assertTrue("It is divided under MAX_SIZE you give.", jsonStr.getBytes().length < MAX_SIZE);
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

}

 

[ testAItems() 실행결과로그 - 캡처기준 1, 2, 3 번째 JsonStr에 prduct 배열이 여러개로 분리되었고 4,5 번째 JsonStr에는 각각 Category, Brand 배열만 갖는다. ]

 

[ testBItems() 실행결과로그 - items 배열이 여러개로 분리되었다. ]

https://github.com/stleary/JSON-java 를 fork한 레포지터리에 이상과 같이 테스트 코드를 추가한 후에 pull request를 시도했다. (https://github.com/stleary/JSON-java/pull/736)

 

피드백

이상 JSON 라이브러리에서 제공하는 기본 기능을 응용해 JSON을 분해하고 분할하는 기능을 구현해본 과정을 공유했다. 이 과정에서 자바 라이브러리에서 모든 기능을 제공하는 것은 아니고 자바 JSON 라이브러리 종류가 다양하다는 것을 알게 되었다.  그리고 라이브러리 문서에서 이야기하는 바와 같이 JSON 관련해서는 내부 정렬 등 구현에 한계가 있다는 것을 알게 되었다.

 

 

 

 

 

 

Comments