CodeGym /행동 /JAVA 25 SELF /동적 구조 다루기: Map, List, JsonNode

동적 구조 다루기: Map, List, JsonNode

JAVA 25 SELF
레벨 46 , 레슨 3
사용 가능

1. Map과 List로 JSON 읽기

보통 JSON을 다룰 때는 그 구조를 미리 알고 있어 Java 클래스로 표현할 수 있습니다. 하지만 실제로는 예측하기 어렵습니다: 필드가 생겼다가 사라지고, 중첩이 바뀌기도 합니다. 이런 경우에는 범용 자료구조인 Map, List 또는 JSON 트리를 사용하는 편이 훨씬 편리합니다.

변형마다 Java 모델을 만드는 것은 오래 걸리고 취약합니다. 범용 컬렉션과 트리는 고정된 스키마에 묶이지 않고 필요한 부분만 유연하게 추출할 수 있게 해줍니다.

JSON 객체를 Map으로 역직렬화

JSON 예시:

{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "active": true
}

Map으로 역직렬화:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

public class Main {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":123,\"name\":\"Alice\",\"email\":\"alice@example.com\",\"active\":true}";

        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> data = mapper.readValue(json, Map.class);

        System.out.println(data);
        // 출력: {id=123, name=Alice, email=alice@example.com, active=true}
    }
}

이제 각 필드를 사전의 항목처럼 접근할 수 있습니다:

System.out.println(data.get("name")); // Alice

객체 배열을 List로 역직렬화

JSON 배열:

[
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]

역직렬화:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;

public class UsersReadExample {
    public static void main(String[] args) throws Exception {
        String json = "[{\"id\":1,\"name\":\"Alice\"},{\"id\":2,\"name\":\"Bob\"}]";

        ObjectMapper mapper = new ObjectMapper();
        List<Map<String, Object>> users = mapper.readValue(json, List.class);

        for (Map<String, Object> user : users) {
            System.out.println(user.get("name"));
        }
        // 출력:
        // Alice
        // Bob
    }
}

중요한 포인트: 범용 구조로 읽을 때 모든 중첩 객체는 Map으로, 배열은 List로 변환됩니다. 복잡한 구조에서는 형변환과 타입 확인을 신중히 해야 합니다:

Object items = data.get("items");
if (items instanceof List) {
    List<?> itemList = (List<?>) items;
    // ...
}

2. JsonNode: Jackson의 JSON 트리

Map/List로 작업하는 것은 편리하지만 안전하지는 않습니다: 타입을 잘못 다루거나 중첩을 놓칠 수 있습니다. Jackson은 더 강력한 도구 — 클래스 JsonNode — 를 제공합니다. 이는 각 노드가 객체, 배열, 값 또는 null일 수 있는 범용 트리입니다.

JsonNode 가져오기

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

public class JsonNodeStart {
    public static void main(String[] args) throws Exception {
        String json = "{\"id\":123,\"name\":\"Alice\",\"tags\":[\"java\",\"json\"]}";
        ObjectMapper mapper = new ObjectMapper();

        JsonNode root = mapper.readTree(json);
        // root는 JSON 트리의 루트입니다
    }
}

필드 접근

int id = root.get("id").asInt();         // 123
String name = root.get("name").asText();  // Alice
JsonNode tags = root.get("tags");         // 배열

System.out.println("이름: " + name);

배열 순회

for (JsonNode tag : tags) {
    System.out.println(tag.asText());
}
// 출력:
// java
// json

중첩 객체

복잡한 JSON 예시:

{
  "user": {
    "id": 1,
    "profile": {
      "nickname": "java_guru",
      "age": 25
    }
  }
}

중첩 값 추출:

JsonNode profile = root.get("user").get("profile");
String nickname = profile.get("nickname").asText();
System.out.println(nickname); // java_guru

안전한 접근: get vs path

- get("키") — 키가 없으면 null을 반환합니다. null에 대해 asText() 같은 메서드를 호출하면 NullPointerException이 발생합니다.
- path("키") — 키가 없으면 ‘빈’ 노드를 반환하며, asText()"", asInt()0을 돌려줍니다.

String phone = root.path("phone").asText(); // 필드가 없으면 ""

노드 타입 확인

if (root.has("tags") && root.get("tags").isArray()) {
    for (JsonNode tag : root.get("tags")) {
        // ...
    }
}

JsonNode 수정

JsonNode는 불변입니다. 트리를 생성/수정하려면 ObjectNode/ArrayNode를 사용하세요.

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;

ObjectMapper mapper = new ObjectMapper();

// 새 객체 생성
ObjectNode obj = mapper.createObjectNode();
obj.put("id", 10);
obj.put("name", "Bob");

// 배열 추가
ArrayNode arr = mapper.createArrayNode();
arr.add("Java").add("JSON");
obj.set("tags", arr);

System.out.println(obj.toPrettyString());
/*
{
  "id" : 10,
  "name" : "Bob",
  "tags" : [ "Java", "JSON" ]
}
*/

3. Gson 사용: JsonElement, JsonObject, JsonArray

Jackson만 있는 것은 아닙니다. Gson도 동적 JSON을 위한 편리한 API인 JsonElement, JsonObject, JsonArray를 제공합니다.

JsonElement로 파싱

import com.google.gson.JsonElement;
import com.google.gson.JsonParser;

String json = "{\"id\":123,\"name\":\"Alice\",\"tags\":[\"java\",\"json\"]}";
JsonElement root = JsonParser.parseString(json);

필드 접근

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

JsonObject obj = root.getAsJsonObject();
int id = obj.get("id").getAsInt();
String name = obj.get("name").getAsString();
JsonArray tags = obj.getAsJsonArray("tags");

for (JsonElement tag : tags) {
    System.out.println(tag.getAsString());
}

중첩과 안전성

if (obj.has("email")) {
    String email = obj.get("email").getAsString();
}

배열 다루기

String arrJson = "[{\"id\":1},{\"id\":2}]";
JsonArray arr = JsonParser.parseString(arrJson).getAsJsonArray();

for (JsonElement el : arr) {
    JsonObject item = el.getAsJsonObject();
    System.out.println(item.get("id").getAsInt());
}

수정

Gson의 객체는 가변이므로 필드를 추가/삭제할 수 있습니다:

import com.google.gson.JsonObject;

JsonObject newObj = new JsonObject();
newObj.addProperty("id", 42);
newObj.add("tags", tags);
System.out.println(newObj.toString());

4. 실습: 구조를 모르는 JSON에서 데이터 추출

과제: 구조가 바뀔 수 있는 JSON 구성(config)이 있다고 가정합시다:

{
  "service": "mail",
  "enabled": true,
  "params": {
    "host": "smtp.example.com",
    "port": 587
  }
}

Jackson으로 hostport 추출:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

ObjectMapper mapper = new ObjectMapper();
JsonNode root = mapper.readTree(json);
JsonNode params = root.path("params");
String host = params.path("host").asText();
int port = params.path("port").asInt();

System.out.println(host + ":" + port); // smtp.example.com:587

Gson으로도 동일하게:

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

JsonObject rootObj = JsonParser.parseString(json).getAsJsonObject();
JsonObject params = rootObj.getAsJsonObject("params");
String host = params.get("host").getAsString();
int port = params.get("port").getAsInt();

System.out.println(host + ":" + port);

5. 흔한 실수와 주의점

오류 №1: 잘못된 형변환. 문자열을 예상했지만 필드에 숫자나 null이 들어 있으면 asText() 또는 getAsString() 호출이 예상치 못한 결과나 예외를 일으킬 수 있습니다. 예를 들어 필드가 없을 때 root.get("foo").asText()NullPointerException을 유발합니다.

오류 №2: null 검사 누락. 특히 중첩 접근: root.get("params").get("host"). params가 없으면 NPE가 발생합니다. Jackson에서는 path()를 사용하고, Gson에서는 has로 존재 여부와 isJsonObject()/isJsonArray()로 타입을 확인하세요.

오류 №3: 노드 타입 혼동. 필드가 배열인데 객체처럼 접근하면 오류가 발생합니다. 타입을 확인하세요: Jackson — isArray()/isObject(), Gson — isJsonArray()/isJsonObject().

오류 №4: Map/List 작업 시 타입 정보 손실. Map<String, Object>로 역직렬화하면 중첩 구조가 Map/List의 연쇄로 바뀌어 탐색이 복잡해지고, 잦은 형변환과 instanceof 검사로 이어집니다.

오류 №5: 트리의 (불)변성을 잘못 이해함. Jackson에서 JsonNode는 불변이며, 변경하려면 ObjectNodeArrayNode를 사용해야 합니다. Gson의 객체는 가변적입니다. 변경한 노드는 현재 트리에서 그 노드를 참조하는 모든 곳에 영향을 준다는 점을 기억하세요.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION