JSON
About JSON
JSON 데이터 처리는 웹개발에서 매우 익숙하고 흔하게 사용되는 문법이라 한번은 정리해둘 필요가 있을 것 같다.
그냥 공식처럼 사용해도 전혀 문제가 없지만 생각을 확장하면 사용 목적을 좀더 명확히 이해할 수 있다.
Why JSON
JSON 은 DB 안의 레코드나 텍스트 형식의 저장형태, 환경설정 파일로도 사용되지만 가장 흔하게 사용되는 분야는 역시
웹분야의 VO 형태일 것이다.
왜 지금처럼 JSON 이 널리 쓰이게 되었는지를 생각해보면 JSON 의 활용방법과 형태를 이해할 수 있다.
프론트개발에서 사용되는 JSON.stringify() , JSON.parse() 함수를 떠올려보자.
왜 하필 JSON 형태일까? 데이터 송신을 보내기 전 수행하는 직렬화와 관련이 있다.
직렬화 (Serialization)
전송을 시작하기 전애 데이터를 한곳에 모아 송신에 유리한 형태로 일렬로 쭈욱 늘어놓는
방식을 직렬화라고 한다.
하나의 객체가 가지고 있는 데이터들도 직렬화를 수행하지 전에는 메모리 이곳저곳에
산재되어 있기 때문에 이것들을 전송전에 한곳으로 모아
캐싱에 유리한 형태로 정리함과 동시에
메모리가 해제되면서 발생할 수 있는 데이터의 유실을 방지할 수 있다.
JSON 이외에도 통신에 사용되는 포멧은 XML, CSV. YAML 이나 Binary 방식등 여러가지가 있지만
데이터 통신을 수행하는데에 JSON 이 유행하게된 이유는
사람이 이해하기 쉬우면서 데이터도 작게 압축할 수 있기 때문이다.
<?xml version="1.0" encoding="UTF-8"?>
<!-- XML 형식 -->
<user>
<name>홍길동</name>
<age>30</age>
</user>
<!-- Array 사용-->
<users>
<user>
<name>홍길동</name>
<age>30</age>
</user>
<user>
<name>김철수</name>
<age>27</age>
</user>
</users>
// JSON 형식
{
"name": "홍길동",
"age": 30
}
// Array 사용
{
users: [
{
"name" : "홍길동",
"age" : "30",
},
{
"name" : "김철수",
"age" : "27"
}
]
}
XML 과 비교하면 열고 닫아야하는 태그도 필요없고 key-value 의 단순한 구조로
데이터의 표현이 가능하다.
전송할 데이터도 상대적으로 작아져 통신비용을 아낄 수 있다는 큰 장점이 있다.
‘충분히 효율적이면서 다루기 쉽다’
이 부분 때문에 일반적인 외부 REST API 나 스프링 프레임워크에서의 컨트롤러에서 JSON 이 흔하게 사용되고
비교적 제한된 영역에서의 보안이나 고성능이 요구되는 일부 내부 인터페이스 분야에서는 바이너리, gRPC, Protobuf
와 같은 시스템과의 결합도가 상대적으로 높아지는 다른 기술이 사용되는 경우도 있다.
JSON 이 자바스크립트 전용은 아니지만
Java 나 Python, C# 등 많은 언어가 JSON 데이터 처리를 지원하지만
이름에서도 알 수 있듯이 JSON (JavaScript Object Notation - 자바스크립트 객체 표기법) 은
Javascript 와의 궁합이 좋고 구조적으로 이 둘은 매우 유사하기 때문에 육안으로 식별하기에 편리하다.
프론트개발 솔루션을 사용하다보면 Javascript 객체와 JSON 을 혼동하기 쉬운데
JSON 은 단순한 String 타입으로 객체를 문서화 해놓은 설계도이고
이 설계도를 이용해 Javascript 등 각종 언어에서 사용하는 객체를 생성한다고 볼 수 있다.
JSON 객체라는 말은 그래서 조금 애메한 표현이라고 생각하는데 ( 문서에서는 JSON Object 라고 표현하기는 하지만)
객체의 형식을 표현해놓은 Notation 이라고 보는것이 정확하고 한마디로
JSON 객체와 Javascript 객체는 다르다
라고 정의하는 것이 옳다.
Jackson 라이브러리는 Javascript 객체를 직접 전달받는 것이 아니라, HTTP Body 로
들어온 JSON 데이터를 Java 객체로 역직렬화 해준다.
.json 파일과 변환 방식
.json 파일은 생성할 객체의 설계도를 저장하고 있는 파일로 실제 사용목적은 txt 파일과 다르지 않다.
실제로 Javascript 에서 그냥 txt 파일에 저장된 JSON 형태의 텍스트를 읽어들여 객체를 생성해도 무방하고
JSON 파일을 노트 형식으로 사용하는 경우도 빈번한다.
IDE 에서 .json 파일을 특별하게 취급하는 이유는 단지 확장자 자체가 메타정보의 역할을 하고 있기 떄문이지
다른 목적은 없다. (사실 애초에 모든 파일은 그저 숫자로 기록된 저장 매체이고 컴퓨터나
인간이 그 숫자를 해석하는 방식에 따라 실행파일이 되기도, 문서파일이 되기도, 영상, 오디오 파일이 되기도 한다)
만약 노트앱을 만든다고 가정하면 매일 매일의 다이어리를 2026.json 파일 안에 관리하면
추후 모든 작성내역을 객체로 읽어와서 연간 데이터를 취합해 통계를 작성하거나 저장소나
클라우드로 넘겨 DB에 적재하기에도 상당히 용이할 것이다. (대부분 그렇게 하고 있다. 이런식으로)
{
"2026" : {
"Jan" : {
"1" : {},
"2" : {},
"3" : {}
},
"Feb" : {
"1" : {},
"2" : {},
"3" : {}
}
}
}
이렇게 JSON 을 만들면 data["2026"]["Jan"]["1"] 형태로 접근할 수 있어 편리하다.
배열로 혹은 객체로
// 배열로 생성
[
{
"name": "홍길동",
"age": 30
},
{
"name": "김철수",
"age": 25
},
{
"name": "이영희",
"age": 28
}
]
const users = JSON.parse(jsonText);
console.log(users[0].name); // 홍길동
console.log(users[1].name); // 김철수
달력과 같이 연도/월/일이 고정된 식별자라면 {} 형태의 Object 로 시작하는것이 편리하고
동일한 종류의 데이터가 순서대로 반복되는 일반적인 형태의 vo 나 화면 그리드의 데이터는
주로 [] 배열로 작성하는 경우가 많다.
객체 안에 배열을 넣는 형태는 API 에서 전송할 데이터를 작성할 때 주로 사용된다.
// 객체로 생성
{
"users": [
{
"name": "홍길동",
"age": 30
},
{
"name": "김철수",
"age": 25
}
]
}
const userData = JSON.parse(jsonText);
console.log(userData.users[1].name); // 김철수
중첩된 형태라면
{
"user": {
"name": "홍길동",
"age": 30
},
"items": [
{
"productId": 100,
"quantity": 2
},
{
"productId": 200,
"quantity": 1
}
]
}
public class OrderVO {
private UserVO user;
private List<ItemVO> items;
// getter / setter
}
@PostMapping("/order")
public void order(@RequestBody OrderVO order) {
}
혹은 이런 방식으로 라이브 서버에서 .json 파일을 읽어와서 사용한다.
const response = await fetch("/data/products.json");
const userData = await response.json(); // Response 객체의 json() 함수.
console.log(userData.users[0].name); // 홍길동
Jackson 라이브러리
스프링 프레임워크에서 주로 사용하는 Jackson 라이브러리는 프론트와 백엔드 사이에서 데이터의
JSON <-> Java Vo 변환 작업을 담당한다.
위의 JSON 형식을 스프링 컨트롤러에서 수신하는 형태는 이렇다.
@PostMapping("/users")
public void users(@RequestBody List<UserVO> users) {
System.out.println(users.get(0).getName());
}
[프론트엔드]
JavaScript 객체
↓
JSON.stringify()
↓
JSON 문자열
↓
HTTP Request Body
↓
[Spring Controller]
↓
Jackson
↓
Java 객체 (VO / DTO / Map)
const user = {
name: "홍길동",
age: 30
};
fetch("/user", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(user)
});
// {"name":"홍길동","age":30}
user 는 자바스크립트 객체이고 HTTP 로 전송할 떄는 JSON.stringify(user)
처리를 통해 JSON 변환 후 텍스트 형태로 스프링 컨트롤러로 넘겨지고 이 과정에서
Jackson 이 개입되어 자바 VO 로 데이터가 변환된다.
자바에서 JSONObject.jar 라이브러리를 사용해 JSON 객체를 만들 수 있다.
import org.json.JSONObject;
import org.json.JSONArray;
JSONObject jsonList = new JSONObject();
JSONArray jsonArray = new JSONArray();
JSONObject user1 = new JSONObject();
user1.put("name", "홍길동");
user1.put("age", 10);
JSONObject user2 = new JSONObject();
user2.put("name", "김철수");
user2.put("age", 30);
JSONObject user3 = new JSONObject();
user3.put("name", "박새로이");
user3.put("age", 40);
jsonArray.put(user1);
jsonArray.put(user2);
jsonArray.put(user3);
jsonList.put("list", jsonArray);
System.out.println(jsonList.toString());
// {"list":[{"name":"홍길동","age":10},{"name":"김철수","age":30},{"name":"박새로이","age":40}]}
반대로 ObjectMapper 를 사용하면 JSON 형식의 스트링을 Java 객체로 만들 수 있다.
ObjectMapper 는 Jackson 라이브러리의 핵심 클래스.
Jackson
├── jackson-core
│ └── JSON의 토큰 처리
│
├── jackson-annotations
│ └── @JsonProperty 등의 어노테이션
│
└── jackson-databind
└── ObjectMapper
// Java -> JSON
ObjectMapper objectMapper= new ObjectMapper();
UserVo user1 = new UserVo("홍길동", 30);
String json1 = objectMapper.writeValueAsString(user1);
System.out.println(json1);
//{"age":30,"name":"홍길동"}
// JSON -> Java
String json2 = """
{
"name": "김철수",
"age": 29
}
""";
UserVo user2 = objectMapper.readValue(json2, UserVo.class);
System.out.println(user2.getName()); // 김철수
System.out.println(user2.getAge()); // 29
// List 변환
String jsonList = """
[
{
"name": "홍길동",
"age": 30
},
{
"name": "김철수",
"age": 20
}
]
""";
// UserVo.class 는 단일 타입이기 때무
// 제너릭 타입은 TypeReference 를 사용해 Jackson 에 타입정보를 전달.
// TypeRefence 는 abstract, 따라서 {} 로 익명클래스를 만들어 인스턴스화
List<UserVo> users = objectMapper.readValue(jsonList
, new TypeReference<List<UserVo>>(){});
for(UserVo vo : users) {
System.out.println(vo.toString());
}
// name : 홍길동, age : 30
// name : 김철수, age : 20
List<Map<String, Object>> map = objectMapper.readValue(jsonList, new TypeReference<List<Map<String, Object>>>(){});
for(int i=0; i<map.size(); i++) {
System.out.println(map.get(i).toString());
}
// {name=홍길동, age=30}
// {name=김철수, age=20}
// 혹은 객체 -> Map 으로 JSON을 거치지 않고도 컨버팅
UserVo user3 = new UserVo("김영희", 27);
Map<String, Object> tempMap = objectMapper.convertValue(user3, new TypeReference<Map<String, Object>>(){});
System.out.println(tempMap);
// {age=27, name=김영희}