JSON 파서: JSON 문자열에서 데이터 파싱 및 추출
· 12분 읽기
목차
JSON 파싱 이해하기
JSON 파서는 JSON(JavaScript Object Notation) 데이터를 해석하여 일반 텍스트 문자열을 프로그래밍 언어에서 조작할 수 있는 구조화된 데이터 형식으로 변환하는 특수 도구입니다. 이러한 변환은 JSON이 클라이언트와 서버 간 데이터 교환의 사실상 표준이 되면서 현대 웹 개발의 기본이 되었습니다.
JSON의 인기는 단순성과 사람이 읽기 쉬운 특성에서 비롯됩니다. 장황한 여는 태그와 닫는 태그가 필요한 XML과 달리, JSON은 중괄호, 대괄호, 키-값 쌍을 사용하는 깔끔한 구문을 사용합니다. Google, Amazon, Facebook, Twitter와 같은 주요 기술 기업들은 API에 JSON을 사용하며 매일 수십억 건의 JSON 요청을 처리합니다.
REST API에서 데이터를 가져오거나, 양식을 제출하거나, 구성 파일을 로드할 때 JSON을 사용하고 있을 가능성이 높습니다. 파서는 번역기 역할을 하여 직렬화된 문자열 형식을 코드에서 직접 액세스하고 수정할 수 있는 객체, 배열, 숫자, 불리언과 같은 네이티브 데이터 구조로 변환합니다.
프로 팁: 프로덕션에서 JSON을 파싱하기 전에 항상 JSON 포매터 및 검증기를 사용하여 먼저 검증하여 구문 오류를 조기에 발견하고 런타임 예외를 방지하세요.
JSON 파싱이 중요한 이유
JSON 파싱을 이해하는 것은 여러 가지 이유로 중요합니다:
- API 통합: 거의 모든 현대 API는 날씨 서비스부터 결제 게이트웨이까지 JSON 형식으로 데이터를 반환합니다
- 구성 관리: 많은 애플리케이션이 설정과 구성을 JSON 파일로 저장합니다
- 데이터 저장: MongoDB와 같은 NoSQL 데이터베이스는 JSON과 유사한 형식(BSON)으로 문서를 저장합니다
- 실시간 통신: WebSocket 연결과 서버 전송 이벤트는 종종 JSON 페이로드를 전송합니다
- 마이크로서비스 아키텍처: 서비스들은 HTTP를 통한 JSON을 사용하여 서로 통신합니다
JSON 파서 작동 방식
JSON 파서는 문자열을 토큰으로 분해하고, 구조를 검증하고, 해당 데이터 객체를 구성하는 다단계 프로세스를 통해 작동합니다. 이 프로세스를 이해하면 더 효율적인 코드를 작성하고 파싱 문제를 효과적으로 디버그할 수 있습니다.
파싱 파이프라인
일반적인 JSON 파싱 워크플로는 네 가지 주요 단계로 구성됩니다:
- 어휘 분석(토큰화): 파서는 입력 문자열을 문자 단위로 스캔하여 중괄호, 대괄호, 콜론, 쉼표, 문자열, 숫자, 키워드(true, false, null)와 같은 토큰을 식별합니다
- 구문 분석: 토큰이 JSON 문법 규칙에 따라 적절한 구조를 갖추고 있는지 확인합니다. 파서는 중괄호가 일치하는지, 쉼표가 요소를 올바르게 구분하는지, 키가 항상 문자열인지 확인합니다
- 의미 분석: 파서는 JSON 구조가 논리적으로 타당한지 검증하며, 중복 키와 적절한 중첩을 확인합니다
- 객체 구성: 마지막으로 파서는 프로그래밍 언어의 네이티브 데이터 구조를 구축하여 JSON 객체를 딕셔너리/객체에, JSON 배열을 리스트/배열에 매핑합니다
기본 파싱 예제
다음은 JSON 파싱이 문자열을 사용 가능한 데이터로 변환하는 방법을 보여주는 간단한 예제입니다:
// API에서 받은 JSON 문자열
const jsonString = '{"name":"Alice","age":30,"skills":["JavaScript","Python","Go"],"isDeveloper":true}';
// 문자열을 JavaScript 객체로 파싱
const userData = JSON.parse(jsonString);
// 이제 데이터에 직접 액세스할 수 있습니다
console.log(userData.name); // 출력: Alice
console.log(userData.skills[0]); // 출력: JavaScript
console.log(userData.isDeveloper); // 출력: true
파서는 평면 문자열을 점 표기법이나 대괄호 표기법을 사용하여 속성에 액세스할 수 있는 구조화된 객체로 변환합니다. 이를 통해 데이터 조작이 간단하고 직관적으로 이루어집니다.
JSON 데이터 타입 이해하기
JSON은 파서가 인식하고 변환해야 하는 6가지 기본 데이터 타입을 지원합니다:
| JSON 타입 | 설명 | 예제 | JavaScript 동등물 |
|---|---|---|---|
| String | 큰따옴표로 묶인 텍스트 | "hello" |
String |
| Number | 정수 또는 부동소수점 | 42, 3.14 |
Number |
| Boolean | 참 또는 거짓 값 | true, false |
Boolean |
| Null | 값의 부재를 나타냄 | null |
null |
| Object | 키-값 쌍의 컬렉션 | {"key":"value"} |
Object |
| Array | 정렬된 값의 목록 | [1,2,3] |
Array |
수동 파싱 vs. 라이브러리 사용
JSON을 다룰 때 두 가지 주요 접근 방식이 있습니다: 처음부터 자체 파서를 작성하거나 기존 라이브러리를 사용하는 것입니다. 각 접근 방식에는 특정 사용 사례에 따라 달라지는 뚜렷한 장점과 절충점이 있습니다.
내장 라이브러리 사용(권장)
대부분의 현대 프로그래밍 언어에는 네이티브 JSON 파싱 기능이 포함되어 있습니다. 이러한 내장 파서는 실전에서 검증되고 최적화되어 있으며 자체 구축 시 고려하지 못할 수 있는 엣지 케이스를 처리합니다.
라이브러리 기반 파싱의 장점:
- 수백만 건의 사용 사례에서 철저히 테스트됨
- 네이티브 코드 구현으로 성능 최적화
- 복잡한 엣지 케이스와 잘못된 데이터를 우아하게 처리
- 보안 취약점을 해결하기 위해 정기적으로 업데이트됨
- 디버깅을 위한 유용한 오류 메시지 제공
- 대용량 JSON 파일 스트리밍 지원
라이브러리를 사용해야 하는 경우:
- 신뢰성이 중요한 프로덕션 애플리케이션
- 신뢰할 수 없거나 외부 데이터 소스 작업
- 메모리 효율성이 필요한 대용량 JSON 파일 처리
- 개발 속도가 중요한 촉박한 일정의 프로젝트
수동 파싱 구현
JSON 파서를 수동으로 구축하는 것은 파싱 알고리즘, 상태 머신, 언어 설계에 대한 이해를 깊게 하는 훌륭한 학습 연습입니다. 그러나 프로덕션 사용에는 거의 적합하지 않습니다.
수동 파싱이 의미 있는 경우:
- 파싱 기초를 이해하기 위한 교육 목적
- 라이브러리 지원이 없는 극도로 제한된 환경
- 알려진 구조를 가진 JSON의 엄격한 하위 집합 파싱
- 특정 패턴에 최적화할 수 있는 성능이 중요한 시나리오
다음은 기본 객체에 대한 수동 JSON 파싱의 단순화된 예제입니다:
function simpleJSONParse(jsonString) {
let index = 0;
function parseValue() {
skipWhitespace();
const char = jsonString[index];
if (char === '{') return parseObject();
if (char === '[') return parseArray();
if (char === '"') return parseString();
if (char === 't' || char === 'f') return parseBoolean();
if (char === 'n') return parseNull();
if (char === '-' || (char >= '0' && char <= '9')) return parseNumber();
throw new Error(`Unexpected character: ${char}`);
}
function parseObject() {
const obj = {};
index++; // skip opening brace
skipWhitespace();
while (jsonString[index] !== '}') {
const key = parseString();
skipWhitespace();
index++; // skip colon
const value = parseValue();
obj[key] = value;
skipWhitespace();
if (jsonString[index] === ',') index++;
skipWhitespace();
}
index++; // skip closing brace
return obj;
}
// Additional parsing functions would go here...
return parseValue();
}
빠른 팁: 학습을 위해 수동 파서를 구축하는 경우, json.org/JSON_checker의 공식 JSON 테스트 스위트에 대해 테스트하여 모든 유효하고 유효하지 않은 케이스를 올바르게 처리하는지 확인하세요.
다양한 프로그래밍 언어에서 JSON 파싱
모든 주요 프로그래밍 언어는 JSON 파싱 기능을 제공하지만 구문과 접근 방식은 다양합니다. 이러한 차이점을 이해하면 다양한 기술 스택에서 효과적으로 작업할 수 있습니다.
JavaScript/Node.js
JavaScript는 전역 JSON 객체를 통해 언어에 직접 내장된 네이티브 JSON 지원을 제공합니다:
// JSON 문자열을 객체로 파싱
const data = JSON.parse('{"name":"Bob","age":25}');
// 객체를 JSON 문자열로 변환
const jsonString = JSON.stringify(data);
// 들여쓰기로 예쁘게 출력
const formatted = JSON.stringify(data, null, 2);
Python
Python의 json 모듈은 직관적인 메서드 이름으로 포괄적인 JSON 처리를 제공합니다:
import json
# JSON 문자열 파싱
json_string = '{"name":"Bob","age":25}'
data = json.loads(json_string)
# 파일에서 JSON 파싱
with open('data.json', 'r') as file:
data = json.load(file)
# JSON 문자열로 변환
json_output = json.dumps(data, indent=2)
Java
Java는 JSON 파싱을 위해 Jackson이나 Gson과 같은 외부 라이브러리가 필요합니다:
// Jackson 사용
ObjectMapper mapper = new ObjectMapper();
String jsonString = "{\"name\":\"Bob\",\"age\":25}";
User user = mapper.readValue(jsonString, User.class);
// Gson 사용
Gson gson = new Gson();
User user = gson.fromJson(jsonString, User.class);
Go
Go의 encoding/json 패키지는 매핑을 위해 구조체 태그를 사용합니다:
import "encoding/json"
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
// JSON 파싱
var user User
json.Unmarshal([]byte(jsonString), &user)
// JSON 생성
jsonBytes, _ := json.Marshal(user)
언어 비교 표
| 언어 | 파싱 메서드 | 문자열화 메서드 | 라이브러리 필요 | 타입 안전성 |
|---|---|---|---|---|
| JavaScript | JSON.parse() |
JSON.stringify() |
아니오 (내장) | 동적 |
| Python | json.loads() |
json.dumps() |
아니오 (표준 라이브러리) | 동적 |
| Java | readValue() |
writeValue() |
예 (Jackson/Gson) | 정적 |
| Go | Unmarshal() |
Marshal() |
아니오 (표준 라이브러리) | 정적 |
| C# | JsonSerializer.Deserialize() |
JsonSerializer.Serialize() |
아니오 (.NET Core 3.0+) | 정적 |
고급 JSON 파싱 기법
기본 파싱 외에도 깊게 중첩된 데이터, 대용량 파일, 동적 스키마와 같은 복잡한 시나리오를 처리하는 데 도움이 되는 여러 고급 기법이 있습니다.
스트리밍 JSON 파싱
대용량 JSON 파일(수백 메가바이트 또는 기가바이트)을 다룰 때 전체 파일을 메모리에 로드하는 것은 실용적이지 않습니다. 스트리밍 파서는 JSON을 점진적으로 처리하여 한 번에 청크를 읽습니다.
// Node.js 스트리밍 예제
const fs = require('fs');
const JSONStream = require('JSONStream');
fs.createReadStream('large-file.json')
.pipe(JSONStream.parse('items.*'))
.on('data', (item) => {
// 각 항목을 개별적으로 처리
console.log(item);
});
스트리밍은 특히 다음과 같은 경우에 유용합니다:
- 수천 개의 JSON 항목이 있는 로그 파일 처리
- 대용량 데이터셋을 데이터베이스로 가져오기
- API에서 실시간 데이터 처리
- 임베디드 시스템과 같은 메모리 제약 환경
복잡한 쿼리를 위한 JSONPath
JSONPath는 JSON 구조를 쿼리하기 위한 XPath와 유사한 구문을 제공하여 복잡하게 중첩된 객체에서 특정 데이터를 쉽게 추출할 수 있습니다:
const jp = require('jsonpath');
const data = {
store: {
books: [
{ title: "Book 1", price: 10 },
{ title: "Book 2", price: 15 },
{ title: "Book 3", price: 20 }
]
}
};
// 가격이 18 미만인 모든 책 찾기
const affordableBooks = jp.query(data, '$.store.books[?(@.price < 18)]');
// 결과: [{ title: "Book 1", price: 10 }, { title: "Book 2", price: 15 }]
스키마 검증
JSON Schema를 사용하면 JSON 데이터의 예상 구조를 정의하고 들어오는 페이로드를 검증할 수 있습니다:
const Ajv = require('ajv');
const ajv = new Ajv();
const schema = {
type: "object",
properties: {
name: { type: "string" },
age: { type: "number", minimum: 0 }
},
required: ["name", "age"]
};
const validate = ajv.compile(schema);
const valid = validate({ name: "Alice", age: 30 });
if (!valid) {
console.log(validate.errors);
}