PHpullh

LANGUAGE COMPARISON

JSON를 언어별로 비교하기

같은 개념을 실제 예제로 나란히 확인하고, 각 언어의 개별 설명으로 이어갈 수 있습니다.

JSON 처리는 타입 시스템과의 협상입니다

JSON 자체는 단순합니다. 문자열, 숫자, 불리언, null, 배열, 객체가 전부입니다. 복잡해지는 이유는 이 여섯 가지를 각 언어의 타입 체계에 끼워 맞추는 과정에 있습니다. 그래서 아래 열 개 언어의 코드는 크게 세 방식으로 갈립니다.

구조를 미리 선언하는 방식이 첫째입니다. Go는 구조체에 태그를 달아 필드 이름을 매핑하고, Java는 Jackson이 리플렉션으로 클래스에 채워 넣으며, C#은 System.Text.Json이 같은 일을 합니다. Rust의 serde와 Kotlin의 kotlinx.serialization은 여기서 한 걸음 더 나가 컴파일 시점에 변환 코드를 생성합니다. 리플렉션 비용이 없고 잘못된 매핑을 컴파일 단계에서 잡을 수 있다는 장점이 있는 대신 빌드 설정이 늘어납니다.

구조 없이 다루는 방식이 둘째입니다. Python의 json.loads는 딕셔너리를, JavaScript의 JSON.parse는 객체를, PHP의 json_decode는 객체나 연관 배열을 돌려줍니다. 스키마를 몰라도 바로 시작할 수 있어 탐색과 스크립트에 좋지만, 필드 이름 오타가 런타임까지 살아남습니다. 셋째로 C++처럼 표준에 JSON이 없어 라이브러리 선택 자체가 첫 결정인 경우도 있습니다.

Kotlin

kotlinx.serialization JSON

Kotlin 공식 직렬화 라이브러리로 data class를 JSON으로 변환합니다.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class User(
    val id: Int,
    val name: String,
    @SerialName("email_address")
    val email: String,
    val active: Boolean = true
)

fun main() {
    val json = Json {
        prettyPrint = true
        ignoreUnknownKeys = true
        encodeDefaults = false
    }

    val user = User(1, "Alice", "alice@test.com")

    // 직렬화
    val jsonStr = json.encodeToString(user)
    println(jsonStr)

    // 역직렬화
    val decoded = json.decodeFromString<User>(jsonStr)
    println(decoded.name)

    // 리스트 직렬화
    val users = listOf(user, user.copy(id = 2, name = "Bob"))
    println(json.encodeToString(users))
}

Python

JSON/YAML 파싱

json 모듈로 JSON 데이터를 파싱하고 직렬화합니다. 커스텀 인코더/디코더도 다룹니다.

import json
from datetime import datetime, date
from dataclasses import dataclass, asdict

# 기본 직렬화/역직렬화
data = {"name": "Alice", "scores": [90, 85, 92], "active": True}
json_str = json.dumps(data, ensure_ascii=False, indent=2)
print(json_str)

parsed = json.loads(json_str)
print(parsed["name"])

# 커스텀 인코더
class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime, date)):
            return obj.isoformat()
        if isinstance(obj, set):
            return list(obj)
        return super().default(obj)

data = {
    "created": datetime.now(),
    "tags": {"python", "json"},
}
print(json.dumps(data, cls=CustomEncoder, ensure_ascii=False))

# dataclass → JSON
@dataclass
class User:
    name: str
    age: int
    email: str

user = User("Alice", 30, "alice@test.com")
print(json.dumps(asdict(user), ensure_ascii=False))

# JSON 파일 읽기/쓰기
# with open("data.json", "w", encoding="utf-8") as f:
#     json.dump(data, f, ensure_ascii=False, indent=2)
# with open("data.json", "r", encoding="utf-8") as f:
#     loaded = json.load(f)

Go

encoding/json — 직렬화

Go의 JSON 인코딩/디코딩. 구조체 태그로 필드명을 제어합니다.

package main

import (
	"encoding/json"
	"fmt"
)

type Address struct {
	City    string `json:"city"`
	Country string `json:"country"`
}

type User struct {
	ID        int       `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email,omitempty"`  // 빈 값 생략
	Password  string    `json:"-"`                // JSON 제외
	Address   Address   `json:"address"`
	Tags      []string  `json:"tags,omitempty"`
}

func main() {
	// 인코딩 (Marshal)
	user := User{
		ID:    1,
		Name:  "Alice",
		Email: "alice@test.com",
		Address: Address{"Seoul", "Korea"},
		Tags:  []string{"admin", "dev"},
	}

	data, err := json.Marshal(user)
	if err != nil { panic(err) }
	fmt.Println(string(data))

	// 들여쓰기 있는 JSON
	pretty, _ := json.MarshalIndent(user, "", "  ")
	fmt.Println(string(pretty))

	// 디코딩 (Unmarshal)
	jsonStr := `{"id":2,"name":"Bob","address":{"city":"Busan","country":"Korea"}}`
	var decoded User
	if err := json.Unmarshal([]byte(jsonStr), &decoded); err != nil {
		panic(err)
	}
	fmt.Printf("이름: %s, 도시: %s
", decoded.Name, decoded.Address.City)

	// 동적 JSON — map 사용
	var dynamic map[string]interface{}
	json.Unmarshal([]byte(jsonStr), &dynamic)
	fmt.Println("name:", dynamic["name"])

	// json.Decoder — 스트림 디코딩
	// decoder := json.NewDecoder(r)
	// decoder.Decode(&obj)
}

Java

Jackson JSON 처리

Java 생태계의 표준 JSON 라이브러리 Jackson으로 직렬화/역직렬화를 마스터합니다.

import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.node.*;
import com.fasterxml.jackson.annotation.*;

// Jackson 어노테이션
@JsonIgnoreProperties(ignoreUnknown = true)
record User(
    @JsonProperty("user_id") int id,
    @JsonProperty("full_name") String name,
    @JsonIgnore String password,
    @JsonInclude(JsonInclude.Include.NON_NULL) String email
) {}

// 상속 처리
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat")
})
abstract class Animal { public abstract String sound(); }
class Dog extends Animal { public String sound() { return "멍멍"; } }
class Cat extends Animal { public String sound() { return "야옹"; } }

public class JacksonDemo {
    public static void main(String[] args) throws Exception {
        var mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);

        // 직렬화
        var user = new User(1, "Alice", "secret", "alice@test.com");
        String json = mapper.writeValueAsString(user);
        System.out.println(json);

        // 역직렬화
        String input = """
            {"user_id": 2, "full_name": "Bob", "unknown_field": "ignored"}
            """;
        User decoded = mapper.readValue(input, User.class);
        System.out.println(decoded.name()); // Bob

        // 컬렉션
        String listJson = "[1, 2, 3]";
        var list = mapper.readValue(listJson,
            mapper.getTypeFactory().constructCollectionType(
                java.util.List.class, Integer.class));

        // JsonNode — 동적 JSON
        JsonNode root = mapper.readTree(json);
        System.out.println(root.get("full_name").asText());
        System.out.println(root.has("password")); // false

        // ObjectNode 빌드
        ObjectNode node = mapper.createObjectNode();
        node.put("name", "Java");
        node.put("version", 21);
        node.putArray("features").add("Records").add("Sealed");
        System.out.println(mapper.writeValueAsString(node));
    }
}

PHP

JSON 파일 다루기

배열을 JSON 파일로 저장했다가 다시 읽어오는 기본 예제입니다.

<?php
$path = sys_get_temp_dir() . '/config.json';
file_put_contents($path, json_encode(['cache' => true]));
$config = json_decode(file_get_contents($path), true);

var_dump($config);

JavaScript

JSON 저장

process.env.NODE_ENV와 process.argv.slice(2)를 객체로 묶어 한 번에 출력하며, 환경 변수가 없으면 development로 대체합니다. 실행 환경과 전달된 인자를 동시에 확인해야 하는 CLI 진입점에서 쓰는 형태입니다. ??는 undefined와 null만 걸러내므로 빈 문자열로 설정된 변수는 그대로 통과합니다.

// JSON 저장
console.log({ env: process.env.NODE_ENV ?? "development", args: process.argv.slice(2) });

TypeScript

JSON 저장

process.env.NODE_ENV와 process.argv.slice(2)를 객체로 묶어 한 번에 출력하며, 환경 변수가 없으면 development로 대체합니다. 실행 환경과 전달된 인자를 동시에 확인해야 하는 CLI 진입점에서 쓰는 형태입니다. ??는 undefined와 null만 걸러내므로 빈 문자열로 설정된 변수는 그대로 통과합니다.

// JSON 저장
console.log({ env: process.env.NODE_ENV ?? "development", args: process.argv.slice(2) });

C#

JSON 저장

process.env.NODE_ENV와 process.argv.slice(2)를 객체로 묶어 한 번에 출력하며, 환경 변수가 없으면 development로 대체합니다. 실행 환경과 전달된 인자를 동시에 확인해야 하는 CLI 진입점에서 쓰는 형태입니다. ??는 undefined와 null만 걸러내므로 빈 문자열로 설정된 변수는 그대로 통과합니다.

// JSON 저장
using System;
using System.IO;

Console.WriteLine(Path.Combine("logs", "2026", "app-4.log"));

C++

JSON 저장

process.env.NODE_ENV와 process.argv.slice(2)를 객체로 묶어 한 번에 출력하며, 환경 변수가 없으면 development로 대체합니다. 실행 환경과 전달된 인자를 동시에 확인해야 하는 CLI 진입점에서 쓰는 형태입니다. ??는 undefined와 null만 걸러내므로 빈 문자열로 설정된 변수는 그대로 통과합니다.

// JSON 저장
#include <filesystem>
#include <iostream>

int main() {
    std::cout << std::filesystem::path("logs") / "2026" / "app-4.log" << "\n";
}

Rust

JSON 저장

process.env.NODE_ENV와 process.argv.slice(2)를 객체로 묶어 한 번에 출력하며, 환경 변수가 없으면 development로 대체합니다. 실행 환경과 전달된 인자를 동시에 확인해야 하는 CLI 진입점에서 쓰는 형태입니다. ??는 undefined와 null만 걸러내므로 빈 문자열로 설정된 변수는 그대로 통과합니다.

// JSON 저장
use std::path::PathBuf;
fn main() {
    let path = PathBuf::from("logs").join("2026").join("app-4.log");
    println!("{}", path.display());
}

숫자와 null에서 대부분의 사고가 납니다

JSON 명세에는 정수와 실수의 구분이 없습니다. 그래서 언어마다 숫자를 어떻게 받을지 스스로 정하는데, 여기서 데이터가 조용히 망가집니다. JavaScript의 기본 숫자 타입은 배정밀도 부동소수점이라 안전하게 표현할 수 있는 정수 범위에 한계가 있습니다. 백엔드에서 64비트 정수 ID를 그대로 내보내면 프런트엔드에서 끝자리가 달라진 값을 받게 됩니다. 서버 로그에는 아무 오류도 남지 않습니다. 큰 정수 ID는 문자열로 보내는 것이 이 문제를 피하는 가장 확실한 방법입니다. Python은 임의 정밀도 정수를 갖지만 실수는 여전히 부동소수점이므로 금액 계산에는 별도 십진 타입이 필요합니다.

null도 만만치 않습니다. "필드가 없음", "필드가 있고 값이 null", "필드가 있고 기본값"은 서로 다른 세 상태인데, 많은 역직렬화 도구가 이를 하나로 뭉갭니다. Go의 구조체는 JSON에 없던 필드를 제로값으로 채우므로 0이 온 것인지 아예 안 온 것인지 구분되지 않습니다. 포인터 타입이나 별도 플래그를 써야 구분됩니다. 부분 수정 API에서 "이 필드를 건드리지 말라"와 "이 필드를 비우라"를 구분해야 할 때 반드시 걸리는 문제입니다. Rust의 Option과 Kotlin의 널 허용 타입은 이 구분을 타입에 담을 수 있는 쪽입니다.

이름 매핑과 알 수 없는 필드

  • Go의 대소문자 규칙 — 패키지 외부에 공개되지 않은 소문자 필드는 직렬화 대상에서 아예 빠집니다. 구조체 태그 없이 쓰면 필드 이름이 대문자로 시작하는 형태로 나가므로, snake_case를 쓰는 API와 맞추려면 태그를 반드시 붙여야 합니다.
  • PHP의 두 얼굴 — json_decode는 기본적으로 객체를 돌려주고, 두 번째 인자를 참으로 주면 연관 배열을 돌려줍니다. 두 형태는 접근 문법이 달라서, 이 인자를 확인하지 않고 쓴 코드가 다른 함수로 옮겨지면 바로 깨집니다.
  • 모르는 필드 정책 — 서버가 새 필드를 추가했을 때 클라이언트가 예외를 던질 것인지 무시할 것인지는 라이브러리 설정입니다. 무시가 기본인 쪽이 호환성에 유리하지만, 오타 난 필드도 함께 무시되어 값이 조용히 비는 대가가 따릅니다. 어느 쪽이든 의식적으로 고르는 편이 낫습니다.
  • 날짜 — JSON에는 날짜 타입이 없습니다. ISO 8601 문자열, 초 단위 정수, 밀리초 단위 정수가 모두 쓰이고 표준은 없습니다. 시간대 정보가 빠진 문자열을 주고받으면 서버와 클라이언트가 서로 다른 시각으로 해석합니다. API 문서에 형식을 못 박아 두어야 합니다.

신뢰할 수 없는 입력을 파싱할 때는 크기와 깊이를 제한해야 합니다. 깊게 중첩된 JSON은 재귀 파서의 스택을 소진시킬 수 있고, 거대한 본문은 메모리를 먹습니다. 요청 본문 크기 제한을 서버 앞단에 두고, 스트리밍 파서를 지원하는 라이브러리라면 전체를 메모리에 올리지 않는 방식을 검토하는 것이 좋습니다. 또한 역직렬화가 임의의 타입을 생성하도록 허용하는 설정은 원격 코드 실행으로 이어진 전례가 있으므로, 다형 역직렬화를 켤 때는 허용 타입을 명시적으로 좁혀야 합니다.

정리하면 JSON 코드의 품질은 파싱 한 줄이 아니라 경계에서 결정됩니다. 외부에서 들어온 데이터는 내부 타입으로 한 번 변환하면서 검증하고, 그 뒤로는 검증된 타입만 흘려보내는 구조가 어느 언어에서든 가장 오래갑니다. 언어별 직렬화 상세는 Go encoding/json, Java Jackson, kotlinx.serialization, Python 파일과 데이터 문서에서 이어집니다.

JSON이 맞지 않는 경우

JSON은 사람이 읽을 수 있고 어디서나 파싱된다는 점 때문에 기본 선택이 되었지만, 모든 자리에 맞지는 않습니다. 대량의 수치 데이터를 주고받을 때는 텍스트 표현이 크기와 파싱 비용을 모두 키웁니다. 스키마를 강제해야 하는 서비스 간 통신에서는 별도의 정의 언어를 쓰는 이진 포맷이 호환성 관리에 유리합니다. 반대로 사람이 손으로 편집하는 설정 파일이라면 주석을 쓸 수 없다는 점이 불편해서, 주석과 후행 쉼표를 허용하는 다른 형식이 선호되기도 합니다.

선택 기준은 단순합니다. 읽는 주체가 사람인가 기계인가, 스키마가 자주 바뀌는가, 그리고 데이터 크기가 문제가 되는 규모인가입니다. 세 질문의 답이 모두 JSON 쪽이 아니라면 다른 형식을 검토할 시점입니다.