무엇을 해야할지 모를 때는 공부를 하면 된다는 말이 두고두고 기억에 남는다.

굳이 C 언어를 해야할 필요는 없다. 하지만 프로그래밍 언어를 아는게 C 언어 정도라서 결국 C 언어로 하게 되었다.

처음에는 google photo 에서 데이터를 내려받았을 때 사진 메타 정보를 json 파일로 주는게 짜증 나서 이것을 입히는 프로그램을 짜려고 했다. 하지만 프로그래밍이라는 것이 그렇게 녹녹하지는 않았다. 그래서 계획을 줄이고 줄이다가 cJSON 라이브러리를 사용해서 json 을 파싱해보자는 결론을 내게 되었다.

cJSON 라이브러리를 (https://github.com/DaveGamble/cJSON) 어떻게 사용할까 하다가 프로젝트 폴더 안에 포함시키기로 했다. 그리고 make 파일을 만들어보는 것까지 해보고 싶다. 보통 C나 C++ 프로젝트들은 make 같은 빌드도구를 이용해서 바이너리 파일로 만들고 사용한다. 하지만 지금까지 내가 짠 프로그램은 간단하게 터미널 출력을 하는 프로그램들이라서 그런 도구들이나 IDE의 도움 없이 사용해본 적이 없다. 그래서 Makefile 또한 작성을 해보고 싶어 해당 프로젝트에서는 Makefile 작성 또한 해보기로 하였다.

{"web-app": {
  "servlet": [
    {
      "servlet-name": "cofaxCDS",
      "servlet-class": "org.cofax.cds.CDSServlet",
      "init-param": {
        "configGlossary:installationAt": "Philadelphia, PA",
        "configGlossary:adminEmail": "[email protected]",
        "configGlossary:poweredBy": "Cofax",
        "configGlossary:poweredByIcon": "/images/cofax.gif",
        "configGlossary:staticPath": "/content/static",
        "templateProcessorClass": "org.cofax.WysiwygTemplate",
        "templateLoaderClass": "org.cofax.FilesTemplateLoader",
        "templatePath": "templates",
        "templateOverridePath": "",
        "defaultListTemplate": "listTemplate.htm",
        "defaultFileTemplate": "articleTemplate.htm",
        "useJSP": false,
        "jspListTemplate": "listTemplate.jsp",
        "jspFileTemplate": "articleTemplate.jsp",
        "cachePackageTagsTrack": 200,
        "cachePackageTagsStore": 200,
        "cachePackageTagsRefresh": 60,
        "cacheTemplatesTrack": 100,
        "cacheTemplatesStore": 50,
        "cacheTemplatesRefresh": 15,
        "cachePagesTrack": 200,
        "cachePagesStore": 100,
        "cachePagesRefresh": 10,
        "cachePagesDirtyRead": 10,
        "searchEngineListTemplate": "forSearchEnginesList.htm",
        "searchEngineFileTemplate": "forSearchEngines.htm",
        "searchEngineRobotsDb": "WEB-INF/robots.db",
        "useDataStore": true,
        "dataStoreClass": "org.cofax.SqlDataStore",
        "redirectionClass": "org.cofax.SqlRedirection",
        "dataStoreName": "cofax",
        "dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver",
        "dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon",
        "dataStoreUser": "sa",
        "dataStorePassword": "dataStoreTestQuery",
        "dataStoreTestQuery": "SET NOCOUNT ON;select test='test';",
        "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log",
        "dataStoreInitConns": 10,
        "dataStoreMaxConns": 100,
        "dataStoreConnUsageLimit": 100,
        "dataStoreLogLevel": "debug",
        "maxUrlLength": 500}},
    {
      "servlet-name": "cofaxEmail",
      "servlet-class": "org.cofax.cds.EmailServlet",
      "init-param": {
      "mailHost": "mail1",
      "mailHostOverride": "mail2"}},
    {
      "servlet-name": "cofaxAdmin",
      "servlet-class": "org.cofax.cds.AdminServlet"},

    {
      "servlet-name": "fileServlet",
      "servlet-class": "org.cofax.cds.FileServlet"},
    {
      "servlet-name": "cofaxTools",
      "servlet-class": "org.cofax.cms.CofaxToolsServlet",
      "init-param": {
        "templatePath": "toolstemplates/",
        "log": 1,
        "logLocation": "/usr/local/tomcat/logs/CofaxTools.log",
        "logMaxSize": "",
        "dataLog": 1,
        "dataLogLocation": "/usr/local/tomcat/logs/dataLog.log",
        "dataLogMaxSize": "",
        "removePageCache": "/content/admin/remove?cache=pages&id=",
        "removeTemplateCache": "/content/admin/remove?cache=templates&id=",
        "fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder",
        "lookInContext": 1,
        "adminGroupID": 4,
        "betaServer": true}}],
  "servlet-mapping": {
    "cofaxCDS": "/",
    "cofaxEmail": "/cofaxutil/aemail/*",
    "cofaxAdmin": "/admin/*",
    "fileServlet": "/static/*",
    "cofaxTools": "/tools/*"},

  "taglib": {
    "taglib-uri": "cofax.tld",
    "taglib-location": "/WEB-INF/tlds/cofax.tld"}}}

https://json.org 에서 예제가 될만한 json 파일 찾다가 해당 예제를 사용해보기로 했다. 적당히 계층이 잘 나누어져 있는 예제로 보였기 때문이다.


#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"
#include <string.h>

#define BUFFER_SIZE 1024
void json_parse(char *json_string)
{
    cJSON *cursor = NULL;
    cJSON *servlet = NULL;
    cJSON *cacheTemplatesTrack = NULL;
    cJSON *root = cJSON_Parse(json_string);
    int i;
    int array_size;
    if (root == NULL) {
        printf("JSON PARSE FAIL");
        exit(1);
    }

    cursor = cJSON_GetObjectItem(root, "web-app");
    if (cursor == NULL) {
        printf("cursor is NULL");
        exit(2);
    }
    servlet = cJSON_GetObjectItem(cursor, "servlet");
    if (servlet == NULL) {
        printf("servlet is NULL");
        exit(3);
    }
    array_size = cJSON_GetArraySize(servlet);
    for (i = 0; i < array_size; i++) {
        cursor = cJSON_GetArrayItem(servlet, i);
        if(cursor == NULL) {
            printf("cursor is NULL");
            exit(4);
        }
        cursor = cJSON_GetObjectItem(cursor, "init-param");
        if (cursor == NULL) {
            printf("cursor is NULL");
            exit(5);
        }
        if (cursor != NULL) {
            cacheTemplatesTrack = cJSON_GetObjectItem(cursor, "cacheTemplatesTrack");
            if (cacheTemplatesTrack != NULL) {
                char *output = cJSON_Print(cacheTemplatesTrack);
                printf("cacheTemplatesTrack : %s\n", output);
                free(output);
                exit(0);
            }
        }
    }
    cJSON_Delete(root);
    cJSON_Delete(cursor);
    cJSON_Delete(servlet);
}
int main()
{
    FILE *fp = fopen("example.json", "r");
    if (fp == NULL) {
        printf("READ JSON FILE ERROR");
        return EXIT_FAILURE;
    }

    char *json_string = NULL;
    size_t json_size = 0;

    char buffer[BUFFER_SIZE];
    while (fgets(buffer, BUFFER_SIZE, fp)) {

        //printf("%s", buffer);
        json_size += (BUFFER_SIZE * sizeof(char));

        if (json_string == NULL) {
            json_string = malloc(json_size);
            *json_string = 0;

            if (json_string == NULL) {

                puts("json_string NOT ALLOCATED");
                return EXIT_FAILURE;
            }
        }
        else {
            char *realloc_json_string = realloc(json_string, json_size);
            if (realloc_json_string == NULL) {
                puts("realloc_json_string NOT ALLOCATED");
                free(json_string);
                return EXIT_FAILURE;
            }
            json_string = realloc_json_string;
        }
        strncat(json_string, buffer, BUFFER_SIZE);
    }
    fclose(fp);
    printf("%s", json_string);
    printf("------------\n");

    json_parse(json_string);
    return 0;
}

main 함수

일단 main 함수에서는 json 파일을 열고 string 으로 변환한 뒤 출력하는 로직을 수행한다. 기본적으로 cJSON 은 string 으로 된 json 파일을 파싱하기 때문에 string 으로 만들어주어야한다.

기본적으로는 파일 포인터를 선언하고 파일을 연다. 그 이후엔 fgets 함수를 통해 버퍼만큼 파일에서 문자열을 읽고 이를 기존 문자열 뒤에 계속 붙일 것이다.

json_size는 말 그대로 json string 의 크기를 나타낼 것이다. 일단 0부터 시작한다.

while 문 조건에 있는 “fgets(buffer, BUFFER_SIZE, fp)” 을 수행할 때마다 파일을 조금씩 읽는다. fgets 함수는 스트림에서 문자열을 받는 함수이다. 현재 fgets 함수 파라미터는 buffer, BUFFER_SIZE, fp 이렇게 3개가 있다. 파일 포인터인 fp 로부터 BUFFER_SIZE 만큼 문자를 읽고 이것을 buffer 문자열에 저장하는 형태이다. 혹은 개행 문자를 만나도 역시 읽는 작업을 멈춘다. 최초에 읽은 문자열은 buffer 로 저장되고 이 크기만큼 json_size에 해당 크기 만큼 할당한다. 최초로 문자열을 읽으면 json_string은 NULL 이므로 if 분기로 들어간다. 그럼 최초의 json 크기만큼 일단 메모리를 할당 하고 이것을 json_string에 합친다. 그럼 json_string은 NULL에서 최초로 입력 받은 문자열을 저장한다.

그리고 다시 while 에 있는 fgets는 기존에 중단되었던 부분에서 파일을 읽고 내용이 있으면 true를 반환한다. 반환하면 json_size에 기존에 있던 양에서 BUFFER_SIZE를 추가한다. 그리고 else 분기로 들어간다. else 분기에서 realloc 을 통해 json_string 문자열을 json_size 만큼 재할당한다. 재할당한 후 json_string 에 대한 데이터를 복사한다. realloc_json_string이 json_string 과 포인터 주소가 같지 않을 수 있기 때문에 realloc_json_string 주소를 json_string에 대입함으로서 그 주소를 일치 시킨다. 그렇게 길이가 늘어난 json_string 에 strncat 을 이용해 뒤에 문자열을 덧붙인다. 이것을 파일 포인터가 모든 문자열을 읽을 때까지 수행한다.

그리고 이것을 다 수행하면 json_string 에는 우리가 읽고 싶은 json 문자열이 전부 들어간다. fclose 를 통해 파일을 닫고 json_parse 함수로 넘겨준다.

json_parse 함수

우리는 json 파일에서 “cacheTemplatesTrack” 에 대한 데이터를 찾을 것이다. 이 항목은 webapps - servlet - init-param - cacheTemplatesTrack 으로 계층화가 되어 있으며 값은 100 이다.

우선 cJSON 타입으로 변수들을 선언한다. cursor는 현재 계층을 나타낼 것이다. servlet 은 servlet 계층에 대한 값을 나타낼 것이고 cacheTemplatesTrack 는 cacheTemplatesTrack 의 값을 나타낼 것이다. root 는 첫 번째 계층을 나타낼 것이다. 그래서 일단 cJSON_Parse 함수에 json_string 을 넘겨서 전체에 대한 파싱을 진행한다. 그리고 cJSON_GetObjectItem 를 통해 webapps 에 대한 데이터를 cursor 에 넣는다. 그리고 다시 cJSON_GetObjectItem 을 통해 servlet 에 대한 데이터를 servlet 에 넣는다. servlet은 여러개의 데이터들의 배열이다. 그래서 일단 array_size를 구한다. 그리고 for 를 통해 init-parm 을 찾는다. cursor 를 통해 init-parm 의 값을 찾으면 이것을 output 을 이용해 콘솔 출력한다. 그리고 exit 를 통해 프로그램을 종료한다.



#CC=gcc
CFLAGS=-Wall -O2

example_json_parse: json_parse.o cJSON.o
	$(CC) $(CFLAGS) -o example_json_parse json_parse.o cJSON.o

json_parse.o: json_parse.c
	$(CC) $(CFLAGS) -c json_parse.c

cJSON.o: cJSON.c cJSON.h
	$(CC) $(CFLAGS) -c cJSON.c cJSON.h

# 정리 작업
clean:
	rm -f *.o example_json_parse

해당 프로그램은 cJSON.c 와 cJSON.h 를 참조하기 때문에 컴파일을 할 때 “gcc -O2 cjson.c json_parse.c -o example_json_parse” 이렇게 컴파일을 해서 바이너리 파일을 생성해야한다. 하지만 이게 파일이 여러 개일 때는 굉장히 복잡하고 귀찮게 느껴진다. 그래서 C언어와 C++ 언어에서는 Makefile 을 통해 빌드 하는 스크립트를 짤 수 있다. CMake 라는 빌드 툴이 있는데 이 빌드 툴을 이용하면 configure 바이너리 파일에서 미리 빌드 변수를 설정하고 make && make install 을 통해 설치할 수 있는데 현재 내가 짠 프로그램은 간단한 프로그램이라서 단순하게 Makefile 을 작성하였다.

CC 는 컴파일러를 지정할 수 있다. 내 리눅스 머신에선 gcc 와 clang 이 모두 깔려 있는데 보통 기본 컴파일러는 gcc로 굳이 지정하지 않아도 리눅스머신에서는 컴파일이 된다. CFLAGS는 컴파일 명령어로 Wall 옵션은 모호성에 대해 경고 메시지를 출력한다. -O2 는 최적화 옵션으로 통상적으로 O2 로 최적화를 한다.

O1 은 실행파일을 가능한 작게 하면서 컴파일 시간이 오래 걸리지 않는 선에서 최적화를 한다. O2 는 가능한 빠르게 실행 되도록 수행하는데 코드 크기가 너무 커지지 않는 선에서 수행하낟. Os는 O2에서 제공하는 최적화 기능 + 코드 키기를 증가시키는 최적화 옵션은 제외한다. O3는 코드 크기는 전혀 신경쓰지 않고 오직 빠른 코드를 만들어 내기 위해 최적화를 한다. 하지만 반드시 이것이 O2를 이용해 만든 코드 보다 빠르다는 보장은 없다. 왜냐하면 CPU 캐시를 신경쓰지 않고, 만들어 캐싱이 제대로 안 될 수 있으며 코드를 임의대로 수정하여 의도된 동작을 못할 수도 있다.

Makefile 이 있는 위치에서 make 를 하면 example_json_parse 부터 실행한다. 이는 json_parse.o cJSON.o 를 요구하며 해당 오브젝트 파일을 참조하여 example_json_parse 을 만든다. 그럼 json_parse.o 와 cJSON.o 는 어떻게 만드는가? 이것은 각각 그 밑에 있는 문장을 통해 어떻게 만드는지 나와 있고, 최종적으로는 json_parse.o, cJSON.o example_json_parse 가 추가적으로 만들어진다.

그리고 make clean 처럼 뒤에 명령어를 붙이면 clean: 뒤에 있는 커맨드들이 실행 된다. 해당 커맨드들은 오브젝트 파일들과 example_json_parse 를 삭제 하는 것을 확인할 수 있다. 만약 만든 example 파일을 install 한다고 하면 install: 를 추가하여 지정된 위치에 저장하는 등의 명령들을 수행할 수 있겠끔 타이핌하면 된다.

재밌게도 Makefile 에서 example_json_parse 를 제일 밑에 쓰면 수행이 되지 않는다. 위에서 아래로 의존성을 참고하게 된 이유는 최종 실행파일은 동일하나 의존성을 추가해야할 때 밑으로 추가할 수 있겠끔 처리한 것으로 보인다. 밑에 계속 추가하면 되니 그쪽이 좀 더 편하다. 또한 커맨드들은 TAB 을 사용해서 들여쓰기를 한 다음 작성해야한다. 처음에 vscode 를 이용해서 작성하였더니 TAB 을 했음에도 공백이 4개가 들어가서 해당 설정을 일시적으로 변경한 다음에 처리했다.

Makefile 작성에 대한 매뉴얼을 (https://www.gnu.org/software/make/manual/make.html) 참고하여 좀 더 자세히 알아볼 수 있다.


이렇게 나에게는 무척이나 거대한 C 언어 활용하는 프로젝트가 하나 끝났다. 앞으로는 종종 이렇게 프로그램을 작성한 내용을 리뷰해볼까 싶다. 이유는 잘 모르겠지만 PLC나 임베디드 쪽을 배워서 그런 건지, 네트워크, 하드웨어 같은 걸 하고 싶어서 그런건지 C 언어 같은 것들을 잘 쓰진 못해도 그냥 안정감 같은 게 든다. python 이나 그런 언어들이 더 쉽다고는 하는데 말이다. 오픈소스와 컴퓨터 과학계의 오랜 레거시이자 근본이라 그런 걸지도 모르겠다. 처음 시작하기 무척이나 어려운 언어지만 다행히 학교 다닐 때 C로 PWM 제어 같은 것까지 해보고, 친구들도 많이 도와주어서 무사히 작성과 실행을 완료하였다. 나에게는 참으로 고마운 인연과 우연들이다.