JSON STREAMING API

크지 않은 json 데이터를 파싱할 때는 한번에 파싱해도 메모리 사용량이 많지 않지만,

파싱해야할 json 데이터가 굉장히 크다면 메모리 사용량이 많기 때문에 주의가 필요한 부분입니다.

 

참고삼아 제 로컬 PC에서 jackson 라이브러리로 json 파일을 객체로 파싱하는 로직을 실행해서 visualvm으로 확인해본 결과는 다음과 같았습니다.

  • 약 300 KB 파일을 한번에 객체로 파싱 : 약 60 MB 힙메모리 사용
  • 약 50 MB 파일을 한번에 객체로 파싱 : 약 150 MB 힙메모리 사용
  • 약 150 MB 파일을 한번에 객체로 파싱 : 약 750 MB 힙메모리 사용

 

jackson 사용 예제

streaming api는 json 데이터를 다음과 같은 토큰 단위로 읽어서 처리합니다.

  • 객체의 시작 ( { ), 끝 ( } )
  • 배열의 시작 ( [ ), 끝 ( ] )
  • 필드명
  • 필드값

 

토큰을 순회하며 원하는 데이터가 나왔을 때 처리를 하는 방식으로 구현하면 됩니다. (이터레이터 처리 방법과 유사)

 

예를 들어서 설명하면,

 

<sample.json>  

{  
  "contentsId": "123456",  
  "title": "꽹과리",  
  "metadata": [  
    {  
      "propertyName": "category",  
      "propertyContent": "타악기"  
    },  
    {  
      "propertyName": "category",  
      "propertyContent": "동양악기"  
    }  
  ]  
}

 

위 데이터를 객체로 파싱하는 코드는 다음과 같이 작성할 수 있습니다.

토큰 단위로만 데이터를 읽을 수 있는 것은 아니고 한번에 읽을 수도 있습니다.

metadata 파싱하는 부분을 한번에 처리하는 방식으로 다음과 같이 변경할 수 있습니다.

import com.fasterxml.jackson.core.JsonFactory;  
import com.fasterxml.jackson.core.JsonParser;  
import com.fasterxml.jackson.core.JsonToken;  
import com.fasterxml.jackson.databind.MappingJsonFactory;  
  
public class JacksonStream {  
  
    public static void main(String[] args) throws IOException {  
        JsonFactory jsonFactory = new MappingJsonFactory();  
        JsonParser jsonParser = jsonFactory.createParser(new File("sample.json")); // json 파서 생성  
  
        AudioContent audioContent = new AudioContent(); // 맵핑할 객체  
  
        while (jsonParser.nextToken() != JsonToken.END_OBJECT) { // END_OBJECT(}) 가 나올 때까지 토큰 순회  
            String fieldName = jsonParser.getCurrentName(); // 필드명, 필드값 토큰인 경우 필드명, 나머지 토큰은 null 리턴  
  
            if ("contentsId".equals(fieldName)) {  
                jsonParser.nextToken(); // 필드값 토큰으로 이동  
                audioContent.setContentsId(jsonParser.getText());  
            } else if ("title".equals(fieldName)) {  
                jsonParser.nextToken();  
                audioContent.setTitle(jsonParser.getText());  
            } else if ("metadata".equals(fieldName)) {  
                // metadata array 요소들 파싱  
                while (jsonParser.nextToken() != JsonToken.END_ARRAY) { // END_ARRAY(]) 가 나올 때까지 토큰 순회  
                    Metadata metadata = parseMetadata(jsonParser);  
                    audioContent.getMetadata().add(metadata);  
                }  
            }  
        }  
  
        jsonParser.close();  
        System.out.println(audioContent);  
    }  
  
    private static Metadata parseMetadata(JsonParser jsonParser) throws IOException {  
        Metadata metadata = new Metadata();  
  
        while (jsonParser.nextToken() != JsonToken.END_OBJECT) { // END_OBJECT(}) 가 나올 때까지 토큰 순회  
            String fieldName = jsonParser.getCurrentName();  
  
            if ("propertyName".equals(fieldName)) {  
                jsonParser.nextToken();  
                metadata.setPropertyName(jsonParser.getText());  
            } else if ("propertyContent".equals(fieldName)) {  
                jsonParser.nextToken();  
                metadata.setPropertyContent(jsonParser.getText());  
            }  
        }  
        return metadata;  
    }  
}

 

하지만 한번에 파싱하는 데이터 범위를 적절히 설정하지 않으면 이 역시 메모리를 많이 사용할 수 있으니

주의가 필요할 것 같습니다.

 

예를 들어, 위처럼 metadata 배열 중 하나의 항목이 아니라, 전체 metadata 배열을 한번에 파싱하게 했는데

배열에 항목 수가 굉장히 많다던가 하는 경우입니다.

 

그리고 파싱하는 것뿐만 아니라 객체로 json 데이터를 생성하는 것도 stream api로 처리가 가능합니다.