Skip to content

74. JSON


1. 키워드

  • JSON(JavaScript Object Notation)
  • 파싱(Parsing)과 파서(Parser)


2. JSON 파일 읽고 쓰기

  • 데이터를 저장하거나 주고 받을 때 JSON 형식을 자주 사용한다.
  • JSON은 JavaScript의 자료형을 텍스트로 표현한 포맷이다.
  • 키-값 쌍으로 이루어져 있으며 사람이 쉽게 읽을 수 있도록 구성되어 있다.


  • 먼저 JSON은 객체를 {}(중괄호)로 표현한다.


{}


  • 여기서 키의 이름은 상황에 맞게 원하는대로 지으면 되는데, 보통 프로그램에서 원활하게 처리하기 위해 영문을 사용한다.
  • 또한, JSON은 대소문자를 구분하므로 “name”“Name”은 다른 키를 의미한다.


{
  "name": "홍길동"
}


  • 키와 값을 여러 개 쓰고 싶을 때는 ,(콤마)로 구분한다.


{
  "name": "홍길동",
  "age": 30,
  "address": "서울시 용산구 한남동"
}


  • 객체는 {}로 표현하는데 객체 안에서 객체를 표현할 때도 {}를 쓰면 된다.


{
  "dateOfBirth": {
    "year": 1980,
    "month": 10,
    "date": 21
  }
}


  • 배열을 사용할 때는 [](대괄호)를 쓰면 된다.


{
  "dayOfWeek": [
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday"
  ],
  "day": [
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
    22, 23, 24, 25, 26, 27, 28, 29, 30, 31
  ]
}


  • 배열 안에는 값뿐만 아니라 객체를 넣을 수 있다.


{
  "dayOfWeek": [
    { "Sunday": 0 },
    { "Monday": 1 },
    { "Tuesday": 2 },
    { "Wednesday": 3 },
    { "Thursday": 4 },
    { "Friday": 5 },
    { "Saturday": 6 }
  ]
}


  • JSON 문서의 내용이 모두 영문이라면 상관이 없지만, 한글이나 기타 다른 나라의 언어가 들어갈 때는 반드시 파일을 UTF-8 인코딩으로 저장해야 한다.


3. JSON 구조체 작성하기

  • 특정 형식으로 된 텍스트 문서를 처리하여 프로그램에서 사용할 수 있도록 하는 것을 구문 분석 또는 파싱이라고 하며 파싱을 하는 프로그램을 파서라고 한다.
  • 간단한 형식의 JSON 문서를 이용하여 텍스트 처리 방법을 알아보자.


  • 먼저 JSON을 처리하기 위한 구조체를 만들어야 한다.
  • 파싱은 문장을 분해하여 토큰(Token)으로 만드는 작업이므로 다음과 같이 토큰 구조체를 작성해야 한다.
  • 토큰은 문자열 토큰과 숫자 토큰으로 나뉘는데 토큰 종류를 구분하기 위한 TOKEN_TYPE 열거형을 정의해 준다.
  • 그리고 토큰 구조체 TOKEN은 토큰 종류, 문자열 포인터, 숫자 변수, 배열의 요소인지 표시하는 변수가 들어간다.
  • 여기서 토큰의 값은 문자열 또는 숫자 한 종류만 저장하므로 union을 사용하여 공용체로 만든다.


// 토큰 종류 열거형
typedef enum _TOKEN_TYPE
{
    TOKEN_STRING, // 문자열 토큰
    TOKEN_NUMBER  // 숫자 토큰
} TOKEN_TYPE;

// 토큰 구조체
typedef struct _TOKEN
{
    TOKEN_TYPE type; // 토큰 종류
    union            // 두 종류 중 한 종류만 저장할 것이므로 공용체로 만듦
    {
        char *string;  // 문자열 포인터
        double number; // 실수형 숫자
    };
    bool isArray; // 현재 토큰이 배열인지 표시
} TOKEN;


  • JSON 문서에 들어있는 숫자가 정수라고 하더라도 double 타입 변수에 저장한다.
  • 실수는 값을 그대로 사용하면 되고, 정수는 값을 꺼낸 뒤 정수로 형변환을 하면 된다.


  • 이제 JSON 구조체를 정의한다.
  • 간단하게 구현하기 위해 토큰은 배열로 만들었고, 개수를 20개로 제한했다.


#define TOKEN_COUNT 20 // 토큰의 최대 개수

// JSON 구조체
typedef struct _JSON
{
    TOKEN tokens[TOKEN_COUNT]; // 토큰 배열
} JSON;


  • 지금까지 정의한 구조체로 JSON의 키-값에서 문자열 값과 숫자값 그리고 문자열 배열을 처리해 보자.


4. JSON 파일 읽기

  • 먼저 다음과 같이 파일의 내용을 읽어서 문자열 포인터를 반환하는 함수를 작성한다.


char *readFile(char *filename, int *readSize) // 파일을 읽어서 내용을 반환하는 함수
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL)
        return NULL;

    int size;
    char *buffer;

    // 파일 크기 구하기
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    // 파일 크기 + NULL 공간만큼 메모리를 할당하고 0으로 초기화
    buffer = malloc(size + 1);
    memset(buffer, 0, size + 1);

    // 파일 내용 읽기
    if (fread(buffer, size, 1, fp) < 1)
    {
        *readSize = 0;
        free(buffer);
        fclose(fp);

        return NULL;
    }

    // 파일 크기를 넘겨줌
    *readSize = size;

    fclose(fp); // 파일 포인터 닫기

    return buffer;
}


  • 파일을 연 뒤 파일의 크기를 구한다.
  • 그리고 파일 크기 + NULL 공간만큼 메모리를 할당하고 0으로 초기화한다.
  • 파일 읽기에 성공했다면 readSize를 역참조하여 파일 크기를 넣어주고, buffer를 반환한다.
  • 이렇게 하면 readFile 함수를 호출하는 쪽에서는 문자열 포인터를 반환값으로 받고, 파일 크기는 매개변수를 통해 받으므로 결괏값을 두 개 받는 효과를 낼 수 있다.


5. JSON에서 문자열 파싱하기

  • 다음의 그림은 JSON에서 문자열을 파싱하는 과정이다.


001


  • 파싱할 JSON 문서는 다음과 같으며 .c 파일이 있는 폴더에 example.json으로 저장한다.
{
  "Title": "Inception",
  "Genre": "Sci-Fi",
  "Director": "Christopher Nolan"
}


  • example.json 파일은 {}에 문자열로 된 키-값으로 구성되어 있다.
  • 여기서 공통적인 특징은 다음과 같다.


1] 첫 문자는 {로 시작한다.

2] 문자열은 항상 로 시작하여 로 끝난다.


  • 이제 공통적인 특징들을 이용하여 문자열을 처리해 보자.
  • 다음과 같이 JSON 파싱 함수를 작성하는데, 이 함수는 JSON 문서의 문자열, 문서의 크기, JSON 구조체 포인터를 받는다.


void parseJSON(char *doc, int size, JSON *json) // JSON 파싱 함수
{
    int tokenIndex = 0; // 토큰 인덱스
    int pos = 0;        // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{') // 문서의 시작이 {인지 검사
        return;

    pos++; // 다음 문자로

    while (pos < size) // 문서 크기만큼 반복
    {
        switch (doc[pos]) // 문자의 종류에 따라 분기
        {
        case '"': // 문자가 "이면 문자열
        {
            // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
            char *begin = doc + pos + 1;

            // 문자열의 끝 위치를 구함. 다음 "의 위치
            char *end = strchr(begin, '"');
            if (end == NULL) // "가 없으면 잘못된 문법이므로
                break;       // 반복을 종료

            int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 토큰 배열에 문자열 저장
            // 토큰 종류는 문자열
            json->tokens[tokenIndex].type = TOKEN_STRING;
            // 문자열 길이 + NULL 공간만큼 메모리 할당
            json->tokens[tokenIndex].string = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(json->tokens[tokenIndex].string, 0, stringLength + 1);

            // 문서에서 문자열을 토큰에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(json->tokens[tokenIndex].string, begin, stringLength);

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + "(+ 1)
        }
        break;
        }

        pos++; // 다음 문자로
    }
}


  • parseJSON 함수는 매개변수로 JSON 문서의 문자열 포인터 doc과 문서(파일) 크기 size, JSON 구조체의 포인터 json을 받는다.


void parseJSON(char *doc, int size, JSON *json) // JSON 파싱 함수


  • 먼저 문서의 시작이 {인지 검사한다.
  • 만약 {로 시작하지 않으면 파싱을 중단한다.
  • 맨 앞에 공백 문자나 개행 문자가 있을 수도 있지만 여기서는 무조건 {로 시작한다고 가정한다.
  • 그리고 검사가 끝났으면 pos1 증가시켜서 다음 문자를 처리한다.


void parseJSON(char *doc, int size, JSON *json) // JSON 파싱 함수
{
    int tokenIndex = 0; // 토큰 인덱스
    int pos = 0;        // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{') // 문서의 시작이 {인지 검사
        return;

    pos++; // 다음 문자로


  • 다음은 JSON 문서에서 문서의 시작 위치를 찾고, 다음 문자를 처리하는 과정이다.


002


  • 이제 while 반복문으로 문서 크기만큼 반복하면서 switch로 문자의 종류에 따라 분기한다.


    while (pos < size) // 문서 크기만큼 반복
    {
        switch (doc[pos]) // 문자의 종류에 따라 분기
        {
            // 생략...
        }

        pos++; // 다음 문자로
    }


  • doc[pos]에 들어있는 문자가 이면 문자열이므로 “” 안의 문자열을 분리해낸다.
  • 여기서 docchar 타입 포인터이므로 포인터 연산을 통해 문서의 중간 지점으로 이동할 수 있다.
  • 먼저 맨 앞의 를 제외한 문자열의 시작 위치를 구한다.
  • 그리고 strchr 함수로 문자열의 끝 위치인 다음 의 위치를 구한다.
  • 만약 가 없으면 잘못된 문법이므로 반복을 종료한다.


        case '"': // 문자가 "이면 문자열
        {
            // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
            char *begin = doc + pos + 1;

            // 문자열의 끝 위치를 구함. 다음 "의 위치
            char *end = strchr(begin, '"');
            if (end == NULL) // "가 없으면 잘못된 문법이므로
                break;       // 반복을 종료


  • 다음은 JSON 문서에서 문자열의 시작 위치 begin과 끝 위치 end를 구하는 과정이다.


003


  • 끝 위치에서 시작 위치를 빼면 문자열의 실제 길이를 알 수 있다.
  • 이제 토큰 배열의 요소에 문자열을 저장한다.
  • 토큰 종류에는 TOKEN_STRING을 지정하여 토큰이 문자열이라는 것을 표시해 주고, 문자열 포인터에는 문자열 길이 stringLength + NULL 공간만큼 메모리를 할당하고 0으로 초기화한다.


            int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 토큰 배열에 문자열 저장
            // 토큰 종류는 문자열
            json->tokens[tokenIndex].type = TOKEN_STRING;
            // 문자열 길이 + NULL 공간만큼 메모리 할당
            json->tokens[tokenIndex].string = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(json->tokens[tokenIndex].string, 0, stringLength + 1);


  • 그다음에는 memcpy 함수로 문자열 시작 위치에서 문자열 길이만큼 복사하여 문자열을 토큰에 저장한다.
  • strcpy 함수는 NULL 직전까지 복사하기 때문에 원하는 만큼 문자열을 가져올 수 없다.
  • 따라서 memcpy 함수로 복사할 크기를 제한해야 한다.
  • 모든 처리가 끝났으면 tokenIndex1 증가시키고, pos에는 문자열 길이와 의 크기 1을 더해서 다음 문자열을 처리할 수 있도록 만든다.


            // 문서에서 문자열을 토큰에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(json->tokens[tokenIndex].string, begin, stringLength);

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + "(+ 1)
        }
        break;


  • 다음은 문서에서 문자열을 토큰에 저장하는 과정이다.


004


  • 토큰에 문자열을 malloc 함수로 동적 메모리를 할당했다.
  • 한 번 할당한 메모리는 반드시 해제를 해줘야 한다.
  • 다음과 같이 freeJSON 함수는 토큰 개수만큼 반복하면서 토큰 종류가 문자열이면 동적 메모리를 해제한다.


void freeJSON(JSON *json) // JSON 해제 함수
{
    for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
    {
        if (json->tokens[i].type == TOKEN_STRING) // 토큰 종류가 문자열이면
            free(json->tokens[i].string);         // 동적 메모리 해제
    }
}


  • 이제 main 함수에서 지금까지 만든 함수들을 사용하여 JSON 문서를 파싱해 보자.


int main()
{
    int size;                                      // 문서 크기
    char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
    if (doc == NULL)
        return -1;

    JSON json = { // JSON 구조체 변수 선언 및 초기화
        0,
    };

    parseJSON(doc, size, &json); // JSON 문서 파싱

    printf("Title: %s\n", json.tokens[1].string);
    printf("Genre: %s\n", json.tokens[3].string);
    printf("Director: %s\n", json.tokens[5].string);

    freeJSON(&json); // json에 할당된 동적 메모리 해제

    free(doc); // 문서 동적 메모리 해제

    return 0;
}


  • 먼저 파일에서 JSON 문서를 읽고, 문서 크기를 구한다.


int size;                                      // 문서 크기
char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함


  • JSON 구조체로 변수를 선언한 뒤 0으로 초기화한다.
  • 그리고 parseJSON 함수를 호출하여 JSON 문서를 파싱한다.


JSON json = { // JSON 구조체 변수 선언 및 초기화
    0,
};

parseJSON(doc, size, &json); // JSON 문서 파싱


  • json.tokens 배열에 키와 값들이 들어있다.
  • 배열에 인덱스로 접근하여 값을 출력한다.


printf("Title: %s\n", json.tokens[1].string);
printf("Genre: %s\n", json.tokens[3].string);
printf("Director: %s\n", json.tokens[5].string);


  • 모든 처리가 끝났으면 freeJSON 함수를 호출하여 json 안에 할당된 동적 메모리를 해제하고, doc 문서 동적 메모리도 해제한다.


freeJSON(&json); // json에 할당된 동적 메모리 해제

free(doc); // 문서 동적 메모리 해제


  • 전체 소스 코드는 다음과 같다.


#define _CRT_SECURE_NO_WARNINGS // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>              // 파일 처리 함수가 선언된 헤더 파일
#include <stdlib.h>             // malloc, free 함수가 선언된 헤더 파일
#include <stdbool.h>            // bool, true, false가 정의된 헤더 파일
#include <string.h>             // strchr, memset, memcpy 함수가 선언된 헤더 파일

// 토큰 종류 열거형
typedef enum _TOKEN_TYPE
{
    TOKEN_STRING, // 문자열 토큰
    TOKEN_NUMBER  // 숫자 토큰
} TOKEN_TYPE;

// 토큰 구조체
typedef struct _TOKEN
{
    TOKEN_TYPE type; // 토큰 종류
    union            // 두 종류 중 한 종류만 저장할 것이므로 공용체로 만듦
    {
        char *string;  // 문자열 포인터
        double number; // 실수형 숫자
    };
    bool isArray; // 현재 토큰이 배열인지 표시
} TOKEN;

#define TOKEN_COUNT 20 // 토큰의 최대 개수

// JSON 구조체
typedef struct _JSON
{
    TOKEN tokens[TOKEN_COUNT]; // 토큰 배열
} JSON;

char *readFile(char *filename, int *readSize) // 파일을 읽어서 내용을 반환하는 함수
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL)
        return NULL;

    int size;
    char *buffer;

    // 파일 크기 구하기
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    // 파일 크기 + NULL 공간만큼 메모리를 할당하고 0으로 초기화
    buffer = malloc(size + 1);
    memset(buffer, 0, size + 1);

    // 파일 내용 읽기
    if (fread(buffer, size, 1, fp) < 1)
    {
        *readSize = 0;
        free(buffer);
        fclose(fp);

        return NULL;
    }

    // 파일 크기를 넘겨줌
    *readSize = size;

    fclose(fp); // 파일 포인터 닫기

    return buffer;
}

void parseJSON(char *doc, int size, JSON *json) // JSON 파싱 함수
{
    int tokenIndex = 0; // 토큰 인덱스
    int pos = 0;        // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{') // 문서의 시작이 {인지 검사
        return;

    pos++; // 다음 문자로

    while (pos < size) // 문서 크기만큼 반복
    {
        switch (doc[pos]) // 문자의 종류에 따라 분기
        {
        case '"': // 문자가 "이면 문자열
        {
            // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
            char *begin = doc + pos + 1;

            // 문자열의 끝 위치를 구함. 다음 "의 위치
            char *end = strchr(begin, '"');
            if (end == NULL) // "가 없으면 잘못된 문법이므로
                break;       // 반복을 종료

            int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 토큰 배열에 문자열 저장
            // 토큰 종류는 문자열
            json->tokens[tokenIndex].type = TOKEN_STRING;
            // 문자열 길이 + NULL 공간만큼 메모리 할당
            json->tokens[tokenIndex].string = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(json->tokens[tokenIndex].string, 0, stringLength + 1);

            // 문서에서 문자열을 토큰에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(json->tokens[tokenIndex].string, begin, stringLength);

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + "(+ 1)
        }
        break;
        }

        pos++; // 다음 문자로
    }
}

void freeJSON(JSON *json) // JSON 해제 함수
{
    for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
    {
        if (json->tokens[i].type == TOKEN_STRING) // 토큰 종류가 문자열이면
            free(json->tokens[i].string);         // 동적 메모리 해제
    }
}

int main()
{
    int size;                                      // 문서 크기
    char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
    if (doc == NULL)
        return -1;

    JSON json = { // JSON 구조체 변수 선언 및 초기화
        0,
    };

    parseJSON(doc, size, &json); // JSON 문서 파싱

    printf("Title: %s\n", json.tokens[1].string);
    printf("Genre: %s\n", json.tokens[3].string);
    printf("Director: %s\n", json.tokens[5].string);

    freeJSON(&json); // json에 할당된 동적 메모리 해제

    free(doc); // 문서 동적 메모리 해제

    return 0;
}

// Title: Inception
// Genre: Sci-Fi
// Director: Christopher Nolan


6. JSON에서 문자열 배열 파싱하기

  • 이번에는 문자열 배열을 파싱해 보자.
  • 다음의 그림은 JSON에서 문자열 배열을 파싱하는 과정이다.


005


  • 다음과 같이 example.json 파일에 출연 배우 명단을 Actors 키로 추가해 보자.


{
  "Title": "Inception",
  "Genre": "Sci-Fi",
  "Director": "Christopher Nolan",
  "Actors": [
    "Leonardo DiCaprio",
    "Joseph Gordon-Levitt",
    "Ellen Page",
    "Tom Hardy",
    "Ken Watanabe"
  ]
}


  • JSON 문서에서 문자열 배열은 [] 안에 로 묶은 문자열이 ,로 구분되어 들어있다.
  • 따라서 이 특징을 이용하여 문자열 배열을 처리해 보자.


  • 앞에서 만든 소스 코드의 parseJSON 함수에서 case ‘[’:를 추가한다.


void parseJSON(char *doc, int size, JSON *json) // JSON 파싱 함수
{
    int tokenIndex = 0; // 토큰 인덱스
    int pos = 0;        // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{') // 문서의 시작이 {인지 검사
        return;

    pos++; // 다음 문자로

    while (pos < size) // 문서 크기만큼 반복
    {
        switch (doc[pos]) // 문자의 종류에 따라 분기
        {
        case '"': // 문자가 "이면 문자열
        {
            // 생략...
        }
        break;
        case '[': // 문자가 [이면 배열
        {
            pos++; // 다음 문자로

            while (doc[pos] != ']') // 닫는 ]가 나오면 반복 종료
            {
                // 여기서는 문자열 배열만 처리
                if (doc[pos] == '"') // 문자가 "이면 문자열
                {
                    // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
                    char *begin = doc + pos + 1;

                    // 문자열의 끝 위치를 구함. 다음 "의 위치
                    char *end = strchr(begin, '"');
                    if (end == NULL) // "가 없으면 잘못된 문법이므로
                        break;       // 반복을 종료

                    int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치

                    // 토큰 배열에 문자열 저장
                    // 토큰 종류는 문자열
                    json->tokens[tokenIndex].type = TOKEN_STRING;
                    // 문자열 길이 + NULL 공간만큼 메모리 할당
                    json->tokens[tokenIndex].string = malloc(stringLength + 1);
                    // 현재 문자열은 배열의 요소
                    json->tokens[tokenIndex].isArray = true;
                    // 할당한 메모리를 0으로 초기화
                    memset(json->tokens[tokenIndex].string, 0, stringLength + 1);

                    // 문서에서 문자열을 토큰에 저장
                    // 문자열 시작 위치에서 문자열 길이만큼만 복사
                    memcpy(json->tokens[tokenIndex].string, begin, stringLength);

                    tokenIndex++; // 토큰 인덱스 증가

                    pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + "(+ 1)
                }

                pos++; // 다음 문자로
            }
        }
        break;
        }

        pos++; // 다음 문자로
    }
}


  • doc[pos]에 들어있는 문자가 [이면 배열이다.
  • 먼저 pos1 증가시켜서 [ 다음 문자를 처리하면서 ]가 나올 때까지 while 루프를 반복한다.


        case '[': // 문자가 [이면 배열
        {
            pos++; // 다음 문자로

            while (doc[pos] != ']') // 닫는 ]가 나오면 반복 종료
            {
                // 생략...

                pos++; // 다음 문자로
            }
        }


  • 즉, 다음의 그림과 같이 JSON 문서에서 배열의 시작 부분을 찾고, 다음 문자를 처리하게 된다.


006


  • 이제 이 나오면 문자열이다.
  • 여기서는 문자열 배열만 처리할 것이다.
  • 먼저 맨 앞의 를 제외한 문자열의 시작 위치를 구한다.
  • 그리고 strchr 함수로 문자열의 끝 위치인 다음 의 위치를 구한다.
  • 만약 가 없으면 잘못된 문법이므로 반복을 종료한다.


                // 여기서는 문자열 배열만 처리
                if (doc[pos] == '"') // 문자가 "이면 문자열
                {
                    // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
                    char *begin = doc + pos + 1;

                    // 문자열의 끝 위치를 구함. 다음 "의 위치
                    char *end = strchr(begin, '"');
                    if (end == NULL) // "가 없으면 잘못된 문법이므로
                        break;       // 반복을 종료


  • 다음은 문자열 배열에서 문자열의 시작 위치 begin과 끝 위치 end를 구하는 과정이다.


007


  • 문자열의 실제 길이를 구한 뒤 토큰 배열의 요소에 문자열을 저장한다.
  • 토큰 종류에는 TOKEN_STRING을 지정하여 토큰이 문자열이라는 것을 표시해 주고, 문자열 포인터에는 문자열 길이 + NULL 공간만큼 메모리를 할당하고 0으로 초기화한다.
  • 여기서 현재 문자열은 배열의 요소이므로 isArraytrue를 지정해 준다.


                    int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치

                    // 토큰 배열에 문자열 저장
                    // 토큰 종류는 문자열
                    json->tokens[tokenIndex].type = TOKEN_STRING;
                    // 문자열 길이 + NULL 공간만큼 메모리 할당
                    json->tokens[tokenIndex].string = malloc(stringLength + 1);
                    // 현재 문자열은 배열의 요소
                    json->tokens[tokenIndex].isArray = true;
                    // 할당한 메모리를 0으로 초기화
                    memset(json->tokens[tokenIndex].string, 0, stringLength + 1);


  • 이제 memcpy 함수로 문자열 시작 위치에서 문자열 길이만큼만 복사하여 문자열을 토큰에 저장한다.
  • 그리고 모든 처리가 끝나면 tokenIndex1 증가시키고, pos에는 문자열 길이와 의 크기 1을 더해서 다음 문자열을 처리할 수 있도록 만든다.


                    // 문서에서 문자열을 토큰에 저장
                    // 문자열 시작 위치에서 문자열 길이만큼만 복사
                    memcpy(json->tokens[tokenIndex].string, begin, stringLength);

                    tokenIndex++; // 토큰 인덱스 증가

                    pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + "(+ 1)
                }

                pos++; // 다음 문자로
            }


  • 다음은 문자열 배열의 문자열을 토큰에 저장하는 과정이다.


008


  • 이제 main 함수에서 JSON 문서의 Actors에 저장한 문자열 배열도 함께 출력할 수 있게 수정한다.


int main()
{
    int size;                                      // 문서 크기
    char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
    if (doc == NULL)
        return -1;

    JSON json = { // JSON 구조체 변수 선언 및 초기화
        0,
    };

    parseJSON(doc, size, &json); // JSON 문서 파싱

    printf("Title: %s\n", json.tokens[1].string);
    printf("Genre: %s\n", json.tokens[3].string);
    printf("Director: %s\n", json.tokens[5].string);

    printf("Actors:\n");
    printf("  %s\n", json.tokens[7].string);
    printf("  %s\n", json.tokens[8].string);
    printf("  %s\n", json.tokens[9].string);
    printf("  %s\n", json.tokens[10].string);
    printf("  %s\n", json.tokens[11].string);

    freeJSON(&json); // json에 할당된 동적 메모리 해제

    free(doc); // 문서 동적 메모리 해제

    return 0;
}

// Title: Inception
// Genre: Sci-Fi
// Director: Christopher Nolan
// Actors:
//   Leonardo DiCaprio
//   Joseph Gordon-Levitt
//   Ellen Page
//   Tom Hardy
//   Ken Watanabe


  • JSON 문서를 파싱했을 때 배열의 요소는 토큰에서 키(Actors) 문자열 뒤에 연달아서 위치한다.
  • 따라서 json.tokens에 인덱스를 7, 8, 9, 10, 11과 같이 지정하면 배열의 요소를 출력할 수 있다.


7. JSON에서 숫자 파싱하기

  • 지금까지 문자열을 파싱했으니 이번에는 숫자를 파싱해 보자.
  • 다음의 그림은 JSON에서 숫자를 파싱하는 과정이다.


009


  • 다음과 같이 example.json 파일에 출시 연도 Year, 상영 시간 Runtime, IMDB 평점 imdbRating 키와 값을 추가한다.


{
  "Title": "Inception",
  "Year": 2010,
  "Runtime": 148,
  "Genre": "Sci-Fi",
  "Director": "Christopher Nolan",
  "Actors": [
    "Leonardo DiCaprio",
    "Joseph Gordon-Levitt",
    "Ellen Page",
    "Tom Hardy",
    "Ken Watanabe"
  ],
  "imdbRating": 8.8
}


  • JSON에서 숫자 값은 로 묶지 않으며 숫자가 그대로 들어간다.
  • 따라서 이 특징을 이용하여 숫자를 처리해 보자.


  • 앞에서 만든 소스 코드의 parseJSON 함수에서 case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '-':를 추가한다.


void parseJSON(char *doc, int size, JSON *json) // JSON 파싱 함수
{
    int tokenIndex = 0; // 토큰 인덱스
    int pos = 0;        // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{') // 문서의 시작이 {인지 검사
        return;

    pos++; // 다음 문자로

    while (pos < size) // 문서 크기만큼 반복
    {
        switch (doc[pos]) // 문자의 종류에 따라 분기
        {
        case '"': // 문자가 "이면 문자열
        {
            // 생략...
        }
        break;
        case '[': // 문자가 [이면 배열
        {
            // 생략...
        }
        break;
        case '0': // 문자가 숫자이면
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '-': // -는 음수일 때
        {
            // 문자열의 시작 위치를 구함
            char *begin = doc + pos;
            char *end;
            char *buffer;

            // 문자열의 끝 위치를 구함. ,가 나오거나
            end = strchr(doc + pos, ',');
            if (end == NULL)
            {
                // } 가 나오면 문자열이 끝남
                end = strchr(doc + pos, '}');
                if (end == NULL) // }가 없으면 잘못된 문법이므로
                    break;       // 반복을 종료
            }

            int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 문자열 길이 + NULL 공간만큼 메모리 할당
            buffer = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(buffer, 0, stringLength + 1);

            // 문서에서 문자열을 버퍼에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(buffer, begin, stringLength);

            // 토큰 종류는 숫자
            json->tokens[tokenIndex].type = TOKEN_NUMBER;
            // 문자열을 숫자로 변환하여 토큰에 저장
            json->tokens[tokenIndex].number = atof(buffer);

            free(buffer); // 버퍼 해제

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + , 또는 }(+ 1)
        }
        break;
        }

        pos++; // 다음 문자로
    }
}


  • JSON 문서는 텍스트 문서이므로 안에 저장된 숫자는 사람이 보기에는 숫자이지만 실제로는 문자열이다.
  • 따라서 case ‘0’:과 같이 숫자를 문자로 처리해야 한다.
  • 숫자가 여러 자리라 하더라도 첫 번째 문자만 숫자이면 나머지 자리도 숫자로 처리하면 된다.
  • 그리고 숫자가 음수일 수도 있으므로 case ‘-’:와 같이 -도 함께 처리해 준다.


        case '0': // 문자가 숫자이면
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
        case '-': // -는 음수일 때
        {


  • 다음은 JSON 문서에서 숫자의 시작 부분을 찾는 모습이다.


010


  • 숫자는 앞에 가 없으므로 doc + pos가 시작 위치이다.
  • 마찬가지로 뒤에 가 없으므로 , 또는 }가 나오면 숫자의 끝으로 본다.
  • 만약 ,} 두 개 다 없으면 잘못된 문법이므로 반복을 종료한다.


            // 문자열의 시작 위치를 구함
            char *begin = doc + pos;
            char *end;
            char *buffer;

            // 문자열의 끝 위치를 구함. ,가 나오거나
            end = strchr(doc + pos, ',');
            if (end == NULL)
            {
                // } 가 나오면 문자열이 끝남
                end = strchr(doc + pos, '}');
                if (end == NULL) // }가 없으면 잘못된 문법이므로
                    break;       // 반복을 종료
            }


  • 다음은 JSON 문서에서 숫자의 시작 위치 begin과 끝 위치 end를 구하는 과정이다.


011


  • 문자열의 실제 길이를 구한 뒤 버퍼에 문자열의 길이 stringLength + NULL 공간만큼 메모리를 할당하고, 0으로 초기화한다.


            int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 문자열 길이 + NULL 공간만큼 메모리 할당
            buffer = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(buffer, 0, stringLength + 1);


  • 이제 문서의 문자열을 버퍼에 저장한다.
  • 단, 문자열을 그대로 사용할 수는 없으므로 atof 함수로 문자열(buffer)을 숫자로 변환하여 토큰에 저장한다.
  • 또한, 토큰 종류에는 TOKEN_NUMBER를 지정한다.
  • 모든 처리가 끝났으면 버퍼를 해제하고, tokenIndex1 증가시킨다.
  • 그리고 pos에는 문자열 길이와 또는 }의 크기 1을 더해서 다음 문자열을 처리할 수 있도록 만든다.


            // 문서에서 문자열을 버퍼에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(buffer, begin, stringLength);

            // 토큰 종류는 숫자
            json->tokens[tokenIndex].type = TOKEN_NUMBER;
            // 문자열을 숫자로 변환하여 토큰에 저장
            json->tokens[tokenIndex].number = atof(buffer);

            free(buffer); // 버퍼 해제

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + , 또는 }(+ 1)
        }
        break;


  • JSON 문서에서 숫자를 토큰에 저장하는 과정이다.


012


  • 이제 main 함수에서 숫자도 함께 출력해 보자.


int main()
{
    int size;                                      // 문서 크기
    char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
    if (doc == NULL)
        return -1;

    JSON json = { // JSON 구조체 변수 선언 및 초기화
        0,
    };

    parseJSON(doc, size, &json); // JSON 문서 파싱

    printf("Title: %s\n", json.tokens[1].string);
    printf("Year: %d\n", (int)json.tokens[3].number);
    printf("Runtime: %d\n", (int)json.tokens[5].number);
    printf("Genre: %s\n", json.tokens[7].string);
    printf("Director: %s\n", json.tokens[9].string);
    printf("Actors:\n");
    printf("  %s\n", json.tokens[11].string);
    printf("  %s\n", json.tokens[12].string);
    printf("  %s\n", json.tokens[13].string);
    printf("  %s\n", json.tokens[14].string);
    printf("  %s\n", json.tokens[15].string);
    printf("imdbRating: %f\n", json.tokens[17].number);

    freeJSON(&json); // json에 할당된 동적 메모리 해제

    free(doc); // 문서 동적 메모리 해제

    return 0;
}

// Title: Inception
// Year: 2010
// Runtime: 148
// Genre: Sci-Fi
// Director: Christopher Nolan
// Actors:
//   Leonardo DiCaprio
//   Joseph Gordon-Levitt
//   Ellen Page
//   Tom Hardy
//   Ken Watanabe
// imdbRating: 8.800000


  • json.tokens에서 숫자를 가져왔을 때 Year, Runtime(int)를 사용하여 정수로 변환했고, imdbRating은 실수를 그대로 출력했다.


8. 키로 값을 가져오는 함수 작성하기

  • 지금까지 토큰 배열에 인덱스를 지정하여 값을 가져왔다.
  • 이번에는 키를 지정하여 값을 가져오는 함수를 만들어보자.


  • 먼저 키에 해당하는 문자열을 가져오는 함수인데 동작 방법은 간단하다.
  • 토큰 개수만큼 반복하면서 토큰 종류가 문자열이면서 키와 일치하는지 검사한다.
  • 일치한다면 바로 뒤의 토큰(i + 1)이 문자열인지 확인한 뒤 문자열을 반환한다.
  • 즉, JSON은 키-값 형식으로 구성되어 있으므로 토큰에서 키 바로 뒤의 요소는 항상 값이다.


char *getString(JSON *json, char *key) // 키에 해당하는 문자열을 가져오는 함수
{
    for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
    {
        // 토큰 종류가 문자열이면서 토큰의 문자열이 키와 일치하면
        if (json->tokens[i].type == TOKEN_STRING &&
            strcmp(json->tokens[i].string, key) == 0)
        {
            // 바로 뒤의 토큰(i + 1)이 문자열이면
            if (json->tokens[i + 1].type == TOKEN_STRING)
                return json->tokens[i + 1].string; // 바로 뒤에 있는 토큰의 문자열 반환
        }
    }

    return NULL; // 키를 찾지 못했으면 NULL을 반환
}


  • 다음은 키에 해당하는 배열 중 인덱스를 지정하여 문자열을 가져오는 함수이다.
  • 토큰에서 키 바로 뒤(i + 1)부터 배열의 요소가 나열되는데 여기에 index를 더하면 해당 요소를 가져올 수 있다.
  • 단, 배열의 요소를 가져오기 전에 isArraytrue인지 확인해야 한다.
  • 그렇지 않으면 배열의 범위를 넘어서 다른 키 문자열을 가져오게 된다.


// 키에 해당하는 배열 중 인덱스를 지정하여 문자열을 가져오는 함수
char *getArrayString(JSON *json, char *key, int index)
{
    for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
    {
        // 토큰 종류가 문자열이면서 토큰의 문자열이 키와 일치한다면
        if (json->tokens[i].type == TOKEN_STRING &&
            strcmp(json->tokens[i].string, key) == 0)
        {
            // 바로 뒤의 토큰(i + 1)부터 배열의 요소
            // 인덱스를 지정한 토큰이 문자열이면서 배열이면
            if (json->tokens[i + 1 + index].type == TOKEN_STRING &&
                json->tokens[i + 1 + index].isArray == true)
                return json->tokens[i + 1 + index].string; // 해당 토큰의 문자열 반환
        }
    }

    return NULL; // 키를 찾지 못했으면 NULL을 반환
}


  • 배열의 요소를 가져오려면 요소의 개수를 알아내는 것이 좀 더 편리하다.
  • 다음은 키에 해당하는 문자열 배열의 요소 개수를 구하는 함수이다.
  • 요소의 개수는 키 바로 뒤의 토큰(i + 1)부터 isArraytrue인 개수를 세어서 반환하면 된다.


int getArrayCount(JSON *json, char *key) // 키에 해당하는 배열의 요소 개수를 구하는 함수
{
    for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
    {
        // 토큰 종류가 문자열이면서 토큰의 문자열이 키와 일치한다면
        if (json->tokens[i].type == TOKEN_STRING &&
            strcmp(json->tokens[i].string, key) == 0)
        {
            // 바로 뒤의 토큰(i + 1)부터 isArray가 true인 토큰의 개수를 세어서 반환
            int j = 0;
            while (json->tokens[i + 1 + j].isArray == true)
                j++;

            return j;
        }
    }

    return 0; // 키를 찾지 못했으면 0을 반환
}


  • 다음은 키에 해당하는 숫자를 가져오는 함수이다.
  • 키 바로 뒤의 토큰(i + 1)이 숫자이면 해당 값을 반환하면 된다.


double getNumber(JSON *json, char *key) // 키에 해당하는 숫자를 가져오는 함수
{
    for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
    {
        // 토큰 종류가 숫자이면서 토큰의 문자열이 키와 일치한다면
        if (json->tokens[i].type == TOKEN_STRING &&
            strcmp(json->tokens[i].string, key) == 0)
        {
            // 바로 뒤의 토큰(i + 1)이 숫자이면
            if (json->tokens[i + 1].type == TOKEN_NUMBER)
                return json->tokens[i + 1].number; // 바로 뒤에 있는 토큰의 숫자 반환
        }
    }

    return 0.0; // 키를 찾지 못했으면 0.0을 반환
}


  • 이제 main 함수에서 지금까지 만든 함수를 사용해서 값을 출력해 보자.


int main()
{
    int size;                                      // 문서 크기
    char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
    if (doc == NULL)
        return -1;

    JSON json = { // JSON 구조체 변수 선언 및 초기화
        0,
    };

    parseJSON(doc, size, &json); // JSON 문서 파싱

    printf("Title: %s\n", getString(&json, "Title"));          // Title의 값 출력
    printf("Year: %d\n", (int)getNumber(&json, "Year"));       // Year의 값 출력
    printf("Runtime: %d\n", (int)getNumber(&json, "Runtime")); // Runtime의 값 출력
    printf("Genre: %s\n", getString(&json, "Genre"));          // Genre의 값 출력
    printf("Director: %s\n", getString(&json, "Director"));    // Director의 값 출력

    printf("Actors:\n");
    int actors = getArrayCount(&json, "Actors");              // Actors 배열의 개수를 구함
    for (int i = 0; i < actors; i++)                          // 배열의 요소 개수만큼 반복
        printf("  %s\n", getArrayString(&json, "Actors", i)); // 인덱스를 지정하여 문자열을 가져옴

    printf("imdbRating: %f\n", getNumber(&json, "imdbRating")); // imdbRating의 값 출력

    freeJSON(&json); // json에 할당된 동적 메모리 해제

    free(doc); // 문서 동적 메모리 해제

    return 0;
}

// Title: Inception
// Year: 2010
// Runtime: 148
// Genre: Sci-Fi
// Director: Christopher Nolan
// Actors:
//   Leonardo DiCaprio
//   Joseph Gordon-Levitt
//   Ellen Page
//   Tom Hardy
//   Ken Watanabe
// imdbRating: 8.800000


  • 토큰에 인덱스를 직접 지정하지 않고도 키의 값을 가져와서 출력할 수 있다.
  • 마찬가지로 배열도 요소의 개수를 가져온 뒤 반복문으로 값을 출력할 수 있다.
  • 단, JSON은 대소문자를 구분하므로 Titletitle은 다른 키이다.
  • 그러므로 getString(&json, “Title”)과 같이 대소문자를 확실하게 지정해 줘야 한다.


9. JSON 파일 쓰기

  • 지금까지 JSON 파일의 내용을 읽고 분석하여 값을 출력했다.
  • 이번에는 반대로 프로그램에서 JSON 문서를 생성해 보자.


  • 다음의 소스 코드를 입력한 뒤 실행해 보자.
  • 단순히 JSON 형식에 맞춰서 출력하므로 코드가 간단하다.


#define _CRT_SECURE_NO_WARNINGS // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>              // 파일 처리 함수가 선언된 헤더 파일

int main()
{
    // JSON 문서에 저장할 데이터
    char *title = "Inception";
    int year = 2010;
    int runtime = 148;
    char *genre = "Sci-Fi";
    char *director = "Christopher Nolan";
    char actors[5][30] = {
        "Leonardo DiCaprio",
        "Joseph Gordon-Levitt",
        "Ellen Page",
        "Tom Hardy",
        "Ken Watanabe"};
    double imdbRating = 8.8;

    FILE *fp = fopen("example.json", "w"); // 쓰기 모드로 파일 열기

    // JSON 문법에 맞춰서 fprintf 함수로 값 출력
    fprintf(fp, "{\n");
    fprintf(fp, "  \"Title\": \"%s\",\n", title);
    fprintf(fp, "  \"Year\": %d,\n", year);
    fprintf(fp, "  \"Runtime\": %d,\n", runtime);
    fprintf(fp, "  \"Genre\": \"%s\",\n", genre);
    fprintf(fp, "  \"Director\": \"%s\",\n", director);
    fprintf(fp, "  \"Actors\": [\n");
    fprintf(fp, "    \"%s\", \n", actors[0]);
    fprintf(fp, "    \"%s\", \n", actors[1]);
    fprintf(fp, "    \"%s\", \n", actors[2]);
    fprintf(fp, "    \"%s\", \n", actors[3]);
    fprintf(fp, "    \"%s\" \n", actors[4]);
    fprintf(fp, "  ],\n");
    fprintf(fp, "  \"imdbRating\": %.1f\n", imdbRating);
    fprintf(fp, "}\n");

    fclose(fp); // 파일 닫기

    return 0;
}


  • 프로그램을 실행하면 .c 파일이 있는 폴더에 다음과 같은 내용으로 example.json 파일이 생성된다.


{
  "Title": "Inception",
  "Year": 2010,
  "Runtime": 148,
  "Genre": "Sci-Fi",
  "Director": "Christopher Nolan",
  "Actors": [
    "Leonardo DiCaprio",
    "Joseph Gordon-Levitt",
    "Ellen Page",
    "Tom Hardy",
    "Ken Watanabe"
  ],
  "imdbRating": 8.8
}


  • JSON 문서를 만드는 방법은 간단하다.
  • JSON 문법에 맞춰서 fprintf 함수로 파일에 출력만 해주면 된다.
  • 여기서 주의할 점은 “” 안에 를 사용하려면 \”와 같이 앞에 \를 붙여줘야 한다.
  • 지금까지 작성한 JSON 파서는 다음과 같은 한계를 가지고 있다.


1] 고정된 토큰 개수

2] 불 값(truefalse) 파싱이 구현되지 않음

3] 숫자, 불 배열 파싱이 구현되지 않음

4] 처음부터 배열로 시작하는 문서를 지원하지 않음

5] 객체 안에 객체 또는 배열 들어가는 문법을 지원하지 않음

6] 배열 안에 객체 또는 배열이 들어가는 문법을 지원하지 않음

7] 키의 값을 가져올 때 매번 모든 토큰을 검사하는 비효율적인 구조


  • 구문 분석(파서) 분야는 생각보다 복잡하고 난이도가 높은 분야이다.
  • 현재는 C로 구현된 JSON 파서가 많이 나와있다.
  • 이런 상태에서 JSON을 완벽하게 처리하는 파서를 다시 구현하는 일은 크게 의미가 없다.

References