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

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

먼저 키에 해당하는 문자열을 가져오는 함수인데 동작 방법은 간단합니다. 먼저 토큰 개수만큼 반복하면서 토큰 종류가 문자열이면서 키와 일치하는지 검사합니다. 일치한다면 바로 뒤의 토큰(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을 반환
}

배열의 요소를 가져오려면 요소의 개수를 알아내는 것이 좀 더 편리합니다. 다음은 키에 해당하는 문자열 배열의 요소 개수를 구하는 함수입니다. 요소의 개수는 키 바로 뒤의 토큰( + 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;     // 문서 크기

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

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

    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;
}

토큰에 인덱스를 직접 지정하지 않고도 키의 값을 가져와서 출력할 수 있습니다. 마찬가지로 배열도 요소의 개수를 가져온 뒤 반복문으로 값을 출력할 수 있습니다.

단, JSON은 대소문자를 구분하므로 Titletitle은 다른 키입니다. 그러므로 getString(&json, "Title")과 같이 대소문자를 확실하게 지정해주어야 합니다.

다음은 전체 소스 코드입니다(파일은 GitHub 저장소의 Unit 83/83.6/json/json 폴더에 들어있습니다).

json.c

#define _CRT_SECURE_NO_WARNINGS    // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>     // 파일 처리 함수가 선언된 헤더 파일
#include <stdlib.h>    // malloc, free, atof 함수가 선언된 헤더 파일
#include <stdbool.h>   // bool, true, false가 정의된 헤더 파일
#include <string.h>    // strchr, strcmp, 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;
        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;
        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++; // 다음 문자로
    }
}

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);            // 동적 메모리 해제
    }
}

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을 반환
}

// 키에 해당하는 배열 중 인덱스를 지정하여 문자열을 가져오는 함수
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을 반환
}

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을 반환
}

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을 반환
}

int main()
{
    int size;    // 문서 크기

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

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

    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;
}

Visual Studio에서 Ctrl+F5 키를 눌러서 프로그램을 실행하면 다음과 같이 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.800000

지금까지 JSON 파일을 읽는 방법을 배웠는데 내용이 좀 길고 복잡했습니다. 여기서는 인덱스와 포인터 연산, 문자열 함수를 활용하여 문자열을 분석하는 것이 가장 중요한 부분입니다. 이해가 잘 되지 않는다면 'Unit 39 문자열 사용하기', 'Unit 44 문자열 검색하기', 'Unit 59 포인터 연산 사용하기'를 복습하는 것이 좋습니다.