JSON으로 작업하기: C/C++ vs. Java

동그란개발자·2024년 11월 9일

배경

ㅇㅖ? C로 HTTP API 코드를 작성한다고요? 🤷🏻‍♀️

그동안 Java/Spring으로 편하게 HTTP API 코드를 작성하던 나..

C/C++ base의 JSON 객체를 주고 받는 HTTP API 코드를 처음 접하고 든 충격을 전하고 싶어 노트북을 펼치게 되었다.

결론부터 말하자면, Java로 이러한 통신 코드를 작성하는 게 훨씬 단순하다는 점이고, 이 단순함은 JSON 객체를 다루는 편리함에서 와서 C/C++와 Java로 JSON 객체를 다룰 때 차이를 중점적으로 다루고자 한다.

[목표] JSON 객체 다룰 때 C/C++ vs. Java 주요 차이점 훑어보기

크게 두가지 측면에서 차이점을 비교해보자

  1. 메모리 관리
  2. 직렬화/역직렬화

✒️ Java의 Jackson 라이브러리, C의 Jansson 라이브러리 기준으로, 구체적인 사용법은 다루지 않는다

메모리 관리

C와 Java의 차이점을 하나 뽑자면, 동적 메모리를 다루는 방식에서의 차이가 가장 크다.

C/C++은 개발자가 직접 메모리를 관리하는 반면, Java는 JVM이 알아서 메모리 관리를 하므로 개발자가 신경쓰지 않아도 된다.

int* ptr = new int; // Allocates memory on the heap
*ptr = 10;

// Must manually deallocate to prevent memory leaks
delete ptr;

C에서 JSON 데이터를 다룰 때 역시 memory leak이 나지 않도록, 메모리 할당/해제를 직접적으로 해준다는 귀찮음이 따라온다.

Jansson 라이브러리의 기본 Json 구조는 json_t로, 데이터 타입과 reference count를 들고 있는 구조체다. 포인터를 써서 접근 및 변경하고, 사용 후에는 더 이상 사용하지 않는 json 객체를 반드시 명시적으로 free 하는 과정이 필요하다.(json_decref(): reference count--, 0이 될 시 memory free)

json_t *json = json_object();
// Use json...
json_decref(json);

Jansson 라이브러리 문서에는 서로가 서로의 레퍼런스를 잡아 memory free 할 수 없는 "Circular reference"를 주의하라고 명시했다. 예를 들어, 아래와 같은 상황이다:

json_t *obj1 = json_object();
json_t *obj2 = json_object();
json_object_set(obj1, "ref", obj2);
json_object_set(obj2, "ref", obj1);

이런 경우 결과적으로 서로가 서로의 레퍼런스를 잡고 있는 구조를 가지게 되어, decref 하더라도 free 될 수 없는 상황이 발생한다.

obj1 = {"ref": {"ref": obj1}}
obj2 = {"ref": {"ref": obj2}}

이렇듯 C/C++은 JSON 객체를 다룰 때, 메모리를 직접 다루기 때문에 오는 번거로움을 그대로 가져간다.

직렬화/역직렬화

직렬화/역직렬화란?

  • 직렬화(Serialization): 객체 → Byte Stream
  • 역직렬화(Deserialization): Byte Stream → 객체

객체지향에서 오는 장점 => 직렬화/역직렬화의 용이함

C는 절차지향적인 언어인 반면, Java는 객체지향적인 언어다. C++은 절차지향 base인 C에, 클래스를 도입함으로써 객체지향적인 특성을 가져왔다.

Java의 객체지향적인 성격이 JSON의 구조와 잘 어울린다. 클래스의 구조와 JSON의 구조가 상응이 잘되기 때문에, 클래스↔JSON 객체의 변환이 용이하다.

JSON 객체 다루기

Java의 Jackson 라이브러리가 제공하는 ObjectMapper를 통해 JSON ↔ Java 객체를 편리하게 변환할 수 있다.

import com.fasterxml.jackson.databind.ObjectMapper;

public class Person {
    private String name;
    private int age;

    // Getters and setters
    // ...

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        // Serialization
        Person person = new Person();
        person.setName("Yooyoung");
        person.setAge(28);
        String json = mapper.writeValueAsString(person);
        System.out.println("Serialized JSON: " + json);

        // Deserialization
        Person deserializedPerson = mapper.readValue(json, Person.class);
        System.out.println("Deserialized name: " + deserializedPerson.getName());
        System.out.println("Deserialized age: " + deserializedPerson.getAge());
    }
}

같은 동작을 하는 코드를 C로 작성해보자.

명시적으로 struct와 포인터를 활용하여 JSON을 다뤄야 하는 불편함이 있다.

#include <stdio.h>
#include <string.h>
#include <jansson.h>

typedef struct {
    char name[50];
    int age;
} Person;

// Serialization function
char* serialize_person(Person* person) {
    json_t *root = json_object();
    json_object_set_new(root, "name", json_string(person->name));
    json_object_set_new(root, "age", json_integer(person->age));

    char *json_string = json_dumps(root, JSON_COMPACT);
    json_decref(root);
    return json_string;
}

// Deserialization function
void deserialize_person(const char* json_string, Person* person) {
    json_error_t error;
    json_t *root = json_loads(json_string, 0, &error);

    if (!root) {
        fprintf(stderr, "JSON parsing error: %s\n", error.text);
        return;
    }

    json_t *name = json_object_get(root, "name");
    json_t *age = json_object_get(root, "age");

    if (json_is_string(name)) {
        strncpy(person->name, json_string_value(name), sizeof(person->name) - 1);
        person->name[sizeof(person->name) - 1] = '\0';
    }

    if (json_is_integer(age)) {
        person->age = json_integer_value(age);
    }

    json_decref(root);
}

int main() {
    // Serialization
    Person person = {"Yooyoung Lee", 28};
    char *json = serialize_person(&person);
    printf("Serialized JSON: %s\n", json);

    // Deserialization
    Person deserialized_person;
    deserialize_person(json, &deserialized_person);
    printf("Deserialized name: %s\n", deserialized_person.name);
    printf("Deserialized age: %d\n", deserialized_person.age);

    // Clean up
    free(json);

    return 0;
}

<주요 차이점>

Java의 객체지향적인 성격으로 직관적으로 JSON와 Java Object 간 매핑이 용이하기 때문에 사용성 측면에서 편리하다.

반면 C/C++은 일일이 key, value를 명시하는 과정이 필요하며, 이로 인해 보일러플레이트 코드가 더 요구된다.

타입 체킹 및 에러 핸들링
C는 강 타입(strong typing)인 반면, JSON은 약 타입(loose typing)

✔️ 강 타입(strong typing)과 약 타입(loose typing)

C++: 구체적으로 명시된 타입

int age = 30;
std::string name = "John";

Json: 타입 명시 X

{
  "age": 30,
  "name": "John",
  "isStudent": true,
  "grades": [95, 87, 92]
}

이러면 C++에서 deserialize할 때(JSON -> C++), 해당 값에 적절한 타입을 결정해야 한다.

이로 인해 C++ JSON 라이브러리는 통상 타입 체킹 및 변환 함수, 타입 불일치에 대한 에러 핸들링 로직 등을 포함한다.

Java 역시 strong-typed 언어인데.. Java는 이 문제를 어떻게 해결한 것일까?

핵심 원리는 Java에 내장된 리플렉션이다.

리플렉션은 런타임에 동적으로 클래스의 정보를 알아내고 실행할 수 있는 것을 말한다.

  • getter/setter 명시 없이도 필드 접근 가능
  • 리플렉션을 이용해 타입 정보 쉽게 구할 수 있음

덕분에 구조체를 정의하여 일일이 매핑해야 하는 C/C++에 비해, Java는 그 수고로움이 덜어진다.

References

https://jansson.readthedocs.io/en/latest/apiref.html

https://blogs.oracle.com/javamagazine/post/java-json-serialization-jackson

https://stackoverflow.com/questions/17549906/c-json-serialization/34165367

0개의 댓글