[Spring] Jackson 으로 Json 조작하기

식빵·2021년 12월 23일
3

Spring Lab

목록 보기
5/35
post-thumbnail

개요

spring-boot-stater-web 의존성을 주입하면 Jackson 라이브러리가 자동으로 포함된다.

Spring mvc 는 내부적으로 @RequestBody, @ResponseBody, ResponseEntity<?> 같은
Http Body 와 관련된 JSON converting 을 할 때 주로 Jackson을 사용하게 된다.

Jackson은 꼭 Spring mvc 내부에서만 사용하기 위한 라이브러리가 아니다.
우리가 Controller 클래스에서도 사용할 수 있고, DB에 JSON을 넣기 위한 작업에도 사용된다.

지금부터 Jackson을 통한 JSON 조작, 제어하는 법을 알아보자.

참고: 사용하는 자바 버전은 11 입니다.




프로젝트 라이브러리 의존성

빌드툴은 gradle 이다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

ps: 최근 알게 된 건데, spring-boot-starter-web 보다는 jackson 만 쏙 뽑아서
쓰고 싶다면 아래와 같은 artifact 만 가져오면 됩니다.

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

<!-- LocalDateTime 을 지원하려면 아래 dependency 도 필요합니다 -->
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

만약에 Bean 으로 DI 받아서 테스트하고 싶다면 아래처럼 ObjectMapper 를 bean 객체로 생성하여 applicationContext 에 넣으면 끝입니다.

@Configuration
public class JsonConfig {
	@Bean
	public ObjectMapper objectMapper() {
		final ObjectMapper objectMapper = new ObjectMapper()
		.registerModule(new JavaTimeModule())
		.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
		.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE,false);
		return objectMapper;
	}
}



Jackson 핵심 클래스들

Jackson 라이브러리는 JSON을 표현하기 위해서 JsonNode 라는 추상 클래스를 제공한다.
하지만 추상 클래스에는 JSON 조작을 지원하는 메소드가 없다.
대신 추상 클래스를 상속한 자식 클래스의 메소드들을 통해서 이루어 진다.

아래 그림은 JsonNode 추상 클래스를 구현한 자식 클래스들이다.

이러한 자식 클래스의 인스턴스는 ObjectMapper 클래스를 통해서 편리하게 생성 가능하다.
그 방법에 대해서는 실습을 통해서 차차 알아갈 것이다.




실습에 필요한 기본 코드

아래 코드는 앞으로의 실습에 도움을 주는 것들이다.
그리고 앞으로의 @Test 메소드는 JacksonJsonTest 클래스 내에 작성되는 것들이다.

package hello.itemservice.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.databind.DeserializationFeature;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JacksonJsonTest {

    ObjectMapper mapper = new ObjectMapper()
		.registerModule(new JavaTimeModule())
		.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
	.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE,false);
    
    // java.time 관련 파싱을 위해서는 위처럼 registerModule 작업을 해줘야 한다.
    // 나도 아직 잘 모르고, 현재 글의 주제와는 조금 멀어서 자세한 설명X, 
    // 대신 맨 밑의 참고 링크를 보자.

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Address {
        private String city;
        private String road;
        private String zipcode;
    }

    public String getPrettyJsonString(JsonNode node) {

        try {
            // 이쁘게 print하기 위해 writerWithDefaultPrettyPrinter 추가
            // 이쁜 print 가 필요없다면 mapper.writeValueAsString(node); 사용해도 됨
            String prettyJson = mapper
            			.writerWithDefaultPrettyPrinter() 
            			.writeValueAsString(node);
                              
            log.info("pretty Print Result...\n{}",prettyJson);
            
            return prettyJson;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }

}

참고. ObjectMapper의 write 계열 메소드들

약간 삼천포로 빠지는 느낌이지만, 중요하고 계속 쓰이는 것이므로 짚고 넘어가겠다.

ObjectMapper 의 write* 계열의 메소드들은 Java 객체를 Json 데이터로 변형 후
해당 데이터를 File, OuputStream, Writer 등을 통해서 외부로 내보내거나
String이나 byte[]로 변환하는 기능을 제공한다.

한번 간단하게 ObjectMapper 의 write* 계열의 메소드를 사용해보자.

@Test
void writeValueTest() {
    try {
        HashMap<String, Object> resultMap = new HashMap<>();
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        resultMap.put("wow", "good");
        resultMap.put("name", "you");
        resultMap.put("list", list);

        // String 변환
        String jsonString = mapper.writerWithDefaultPrettyPrinter()// 이쁘게 프린트 하기
        			  .writeValueAsString(resultMap);
        log.info(jsonString);

        
        // File 객체를 통한 외부 저장
        mapper.writeValue(Paths.get("C:/upload/json/mapObjectToJson.json").toFile(),
        		  resultMap);

        // Stream을 통한 외부 저장
        try(OutputStream os = 
            Files.newOutputStream(Paths.get("C:/upload/json/mapObjectToJson2.json"))
        ) {
            mapper.writeValue(os, resultMap);
        }

        // Writer 를 통한 외부 저장
        try (BufferedWriter bw = Files.newBufferedWriter(
        	Paths.get("C:/upload/json/mapObjectToJson3.json"),
                StandardCharsets.UTF_8)
        ) {
            mapper.writeValue(bw, resultMap);
        }

    } catch (IOException e) {
        e.printStackTrace();
    }
}

콘솔 출력:


저장된 json 파일 내용:

HashMap 및 List 같은 일반적인 Java 객체가 Json으로 형태 변환이 일어나고
저장 혹은 출력되는 것을 확인했다.

이후 실습에서 생성한 JsonNode 계열의 인스턴스도 또한 ObjectMapper의 write* 계열의
메소드를 통해서 Json String으로 만들거나, 외부에 json 파일로 저장하는 작업을 할 것이다.




JsonNode 타입의 객체 생성 방법

내가 아는 방법 3가지만 작성해보겠다.
(다른 좋은 방법도 추천해주고 싶으면 댓글 부탁드립니다)


1. 문자열 기반 생성

@Test
void createJsonNodeWithText() throws JsonProcessingException {

    // JSON 객체 문자열
    String jsonObjectText =
            "{" +
            "  \"name\": \"dailyCode\"," +
            "  \"age\": 0," +
            "  \"address\": {" +
            "    \"city\": \"seoul\"," +
            "    \"zipCode\": \"200-120\"" +
            "  }" +
            "}";


    JsonNode jsonObjectNode = mapper.readTree(jsonObjectText);
    // mapper.readValue 로 대체 가능하다.
    // JsonNode readValue = mapper.readValue(jsonObjectText, JsonNode.class); 
    // mapper.readValue(jsonObjectText, JsonNode.class); 를 쓰면 
    // new TypeReference<T> 도 사용 가능해서 json string ==> POJO 변경에 유용하다.
    

    // 출력
    getPrettyJsonString(jsonObjectNode);

    // JSON 배열 문자열
    String jsonArrayText =
            "[" +
            "  {" +
            "    \"name\": \"batman\"," +
            "    \"age\": 24" +
            "  }," +
            "  {" +
            "    \"name\": \"superman\"," +
            "    \"age\": 36" +
            "  }," +
            "  {" +
            "    \"name\": \"flash\"," +
            "    \"age\": 30" +
            "  }" +
            "]";

    JsonNode jsonArrayNode = mapper.readTree(jsonArrayText);
    getPrettyJsonString(jsonArrayNode);

}

콘솔 출력:


2. POJO 기반 생성

앞서 만들었던 Address 클래스와 Java에서 제공하는 Map, List 클래스를 사용하겠다.

@Test
void createJsonNodeWithPojo() {

    // 사용자가 정의한 클래스
    Address userInfo = Address.builder()
            .city("seoul")
            .road("jonro-street")
            .zipcode("111-222")
            .build();

    JsonNode userInfoJsonNode = mapper.convertValue(userInfo, JsonNode.class);
    getPrettyJsonString(userInfoJsonNode);

    // 자바에서 제공하는 클래스 : Map
    Map<String, Object> userMap = new HashMap<>();
    userMap.put("isMemeber", true);
    userMap.put("signupDate", LocalDateTime.now());
    userMap.put("detail", userInfo);

    JsonNode userInfoMapJsonNode = mapper.convertValue(userMap, JsonNode.class);
    getPrettyJsonString(userInfoMapJsonNode);

    // 자바에서 제공하는 클래스 : List
    List<Address> addressList = new ArrayList<>();
    addressList.add(new Address("Seoul", "jongro", "111-111"));
    addressList.add(new Address("Gyeongi", "somewhere", "222-222"));
    addressList.add(new Address("Busan", "haeundae", "333-333"));

    JsonNode addressListJsonNode = mapper.convertValue(addressList, JsonNode.class);
    getPrettyJsonString(addressListJsonNode);


    // 참고: 역으로 JsonNode 에서 Java Map, List, 그외 POJO 로 변경하고 싶다면 아래처럼 하면 된다.
    List<Address> addresses 
    	= mapper.convertValue(addressListJsonNode, new TypeReference<List<Address>>() {});
    
    // 단순하게 TypeReference 의 타입 파라미터에 원하는 결과 타입을 작성하면 된다.

}

콘솔 출력:


3. JsonNodeFactory.instance로 간단 생성

그런데 위처럼 이미 제공된 문자열이나 POJO 를 통한 JsonNode 를 생성하는 게 아니라,
데이터가 없는 JsonNode 객체를 생성하고 데이터를 조금씩 넣고 싶은 경우도 있다.
이럴 때 필요한 것이 바로 JsonNodeFactory.instance이다.

추상 클래스인 JsonNode의 자식 클래스 인스턴스를 생성하는 메소드를 제공해준다.


참고로 JsonNodeFactory.instance 는 싱글톤이면서 stateless 하기 때문에
어디서나 안심하고 가져다 쓸 수 있다.


이제 코드로 사용법을 보자.
@Test
void createEmptyJsonNode() {
    // ObjectNode (ex: {"wow" : "good"}) 생성
    ObjectNode objectNode = JsonNodeFactory.instance.objectNode();
    objectNode.put("name", "dailyCode").put("age", "secret");
    getPrettyJsonString(objectNode);

    // ArrayNode (ex: ["wow", "good"]) 생성
    ArrayNode arrayNode = JsonNodeFactory.instance.arrayNode();
    arrayNode.add(1).add(2).add(3);
    getPrettyJsonString(arrayNode);
}

콘솔 출력:




JsonNode 타입변환 생략하기

생성을 했으니 이제 JsonNode을 통한 Json 조작을 해볼 것이다.
그런데 앞서 설명했지만 JsonNode는 추상 클래스이고 이를 통한 Json 조작은 안된다고 했다.
그 대신 JsonNode 를 상속하여 구현한 자식 클래스의 인스턴트들이 그 역할을 한다고 했다.

그러니 우리가 위에서 작성한 코드는 다음과 같은 type casting 을 해줘야 한다.

if (jsonObjectNode.isObject()) {
    ObjectNode objectNode  = (ObjectNode) jsonObjectNode;
    objectNode.put("wow", "yeah");
}

if (jsonArrayNode.isArray()) {
    ArrayNode arrayNode = (ArrayNode) jsonArrayNode;
    arrayNode.add(1);
}

그런데 JsonNode 객체를 생성하고 나서 이런 형변환을 일일이 해야할까? 아니다.

위에서는 readTree, readValue, convertValue 를 통해서 JsonNode를 생성했지만,
readValue, convertValue 처럼 반환 타입을 지정할 수 있는 API에서는
ObjectNode (또는 ArrayNode) 로 바로 생성하면 이런 과정을 단축 시킬 수 있다.


// 문자열에서 생성하는 법
ObjectNode jsonObjectNode = mapper.readValue(jsonObjectText, ObjectNode.class);
jsonObjectNode.put("more", "special");

ArrayNode jsonArrayNode = mapper.readValue(jsonArrayText, ArrayNode.class);
jsonArrayNode.add(1);


// POJO 에서 생성하는 법
ObjectNode userInfoJsonNode = mapper.convertValue(pojo, ObjectNode.class);
userInfoJsonNode.put("isProgrammer", true);

현재 문자열 혹은 Pojo 가 어떤 타입의 JsonNode 로 생성할 수 있는지만 정확히 안다면,
위처럼 간단하게 ObjectNode 또는 ArrayNode 를 생성할 수 있다.




Json 조작하기

길었지만 지금까지 JsonNode 기본 생성법을 배웠다.
이제 조작하는 방법은 알아보자.


1. Json 데이터 삽입

아래 코드는 사용자 정보를 Json 형태로 만들고 나서,
해당 Json을 하나의 파일로 저장하는 예제이다.


@Test
void controlMyJsonNodeTest() {

    // 사용자 정보를 담는 Json 을 생성해본다. 일단 빈 껍데기를 만든다.
    ObjectNode userInfoJson = JsonNodeFactory.instance.objectNode();

    // 사용자 기본 정보를 넣어준다.
    userInfoJson.put("name", "dailyCode");  // 이름
    userInfoJson.put("id", "devToroko");    // 아이디
    userInfoJson.put("phoneNumber", "010-0000-0000");   // 전화번호

    // POJO 정보도 주입, putPOJO 를 사용하면 된다.
    userInfoJson.putPOJO("address", new Address("Seoul", "Jongro", "111-222"));

    List<String> favoriteFoodList
            = List.of("pizza", "hamburger", "gookBab");
    userInfoJson.putPOJO("favoriteFoodList", favoriteFoodList);

    // 가족 정보도 생성하고 담겠다.
    ObjectNode familyInfoMap = JsonNodeFactory.instance.objectNode();
    ObjectNode fatherInfo = JsonNodeFactory.instance.objectNode();
    ObjectNode motherInfo = JsonNodeFactory.instance.objectNode();

    fatherInfo.put("name", "아버지"); // 한글 파싱도 잘되는지 확인
    fatherInfo.put("age", "64");
    fatherInfo.put("phoneNumber", "010-yyyy-yyyy");

    motherInfo.put("name", "어머니"); 
    motherInfo.put("age", "59");
    motherInfo.put("phoneNumber", "010-xxxx-xxxx");

    // JsonNode 를 넣기 위해서는 set 을 써야한다.
    familyInfoMap.set("FATHER", fatherInfo);
    familyInfoMap.set("MOTHER", motherInfo);

    userInfoJson.set("familyInfo", familyInfoMap);
    
    // 최종적으로 Json을 파일에 저장해봤다.
    try {
     mapper.writerWithDefaultPrettyPrinter()
           .writeValue(Paths.get("C:/upload/json/user1.json").toFile(), userInfoJson);
           // mapper.writeValue 만 해도 된다. 다만 이쁘게 들여쓰기가 되지 않는다.
    } catch (IOException e) {
        e.printStackTrace();
    }
}

저장된 json 파일 내용




2. Json 데이터 조회하기

Json 데이터 삽입 목차에서 생성한 json 정보를 이번에는 조회해보자.


코드

참고:
findValue 는 재귀적으로 데이터를 찾고
path,get 은 가장 껍데기에서만 프로퍼티 찾는다.
복잡한 ObjectNode 의 경우 findValue를 잘 활용하면 큰 도움이 된다.

// Json 에서 얼마나 깊이 있든 findValue 는 재귀적으로 프로퍼티 값을 찾아낸다.
log.info("find My Mother Info");
JsonNode mother = userInfoJson.findValue("MOTHER");
getPrettyJsonString(mother);

System.out.println();


// 재귀적으로 찾는 findValue 와 달리 json 객체의 제일 첫 level 부터
// 아래로 차근차근 찾고 싶으면 path 또는 get 사용
log.info("find step by step");

// 1. path 방식
String fatherAge = userInfoJson.path("familyInfo")
				.path("FATHER")
                		.path("age").asText();
                        
System.out.println("fatherAge = " + fatherAge);
System.out.println("path가 값을 못 찾는 경우 : "
        + userInfoJson.path("FATHER").getClass().getName()); // 못찾으면 MissingNode


// 2. get 방식
String motherAge = userInfoJson.get("familyInfo").get("MOTHER").get("age").asText();
System.out.println("motherAge = " + motherAge);
System.out.println("get이 값 못 찾는 경우 :" + (userInfoJson.get("MOTHER"))); // 못찾으면 null

System.out.println();

// Json 객체 내에 있는 모든 phoneNumber 속성 값들을 읽어와서 List에 담는다.
log.info("get All PhoneNumber Properties value");
List<JsonNode> phoneNumbers = userInfoJson.findValues("phoneNumber");
phoneNumbers.forEach(System.out::println);

System.out.println();

// Json 객체에서 어머니의 phoneNumber 만 얻어오기
log.info("get Only My Mother PhoneNumber");
JsonNode mother1 = userInfoJson.findValue("MOTHER");
JsonNode phoneNumber = mother1.findValue("phoneNumber");
System.out.println("phoneNumber = " + phoneNumber + "\n");


// phoneNumber 속성명 갖는 모든 JsonNode 를 읽어오기
log.info("get All Json Object That has 'phoneNumber' Properties");
List<JsonNode> phoneNumber1 = userInfoJson.findParents("phoneNumber");
phoneNumber1.forEach(System.out::println);

콘솔 출력




3. Json 데이터 삭제 및 수정

Json 데이터 삽입 목차에서 작성한 코드에서
userInfoJson.set("familyInfo", familyInfoMap); 다음에 아래 코드를 작성하자.

// 사용자 자신의 전화번호, id , 나이 정보 모두 지우기
userInfoJson.remove("phoneNumber");
userInfoJson.remove(List.of("id", "age"));

// 부모님의 전화 번호를 모두 null 로 수정하기
JsonNode familyInfo = userInfoJson.path("familyInfo");
List<JsonNode> familyInfoList = ((ObjectNode) familyInfo).findParents("phoneNumber");
familyInfoList.forEach(node -> {
    ObjectNode objectNode = (ObjectNode) node;
    
    // 속성 자체를 삭제
    //objectNode.remove("phoneNumber");
    
    // 속성을 지우지 말고 null로 값 수정
    objectNode.set("phoneNumber", JsonNodeFactory.instance.nullNode());
});

저장된 json 파일 내용



이번에는 가족 정보를 모두 지워버리겠다.
바로 위에서 작성한 수정, 삭제 코드는 모두 지우고 아래 코드만 남기고 실행시켜 보자.

// 가족 정보 제거
JsonNode familyInfo = userInfoJson.path("familyInfo");
((ObjectNode)familyInfo).removeAll();

저장된 json 파일 조회:




✨ 참조한 링크

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

0개의 댓글