[Java] Gson을 이용한 Json Serialization & Deserialization

2022. 4. 29. 12:38Java/Basic

반응형
  1. Gson
  2. 테스트를 위한 사전 설정
    1. html escaping 테스트
    2. LocalDateTime 관련 객체 테스트
  3. Gson 객체 생성하기
    1. GsonUtils
    2. disableHtmlEscaping
    3. setFieldNamingPolicy
    4. setDateFormat
    5. serializeNulls
  4. LocalDateTime 관련 객체 serialize
    1. 테스트 코드 작성 및 에러 발생
    2. TypeAdapter 정의하기
    3. Gson 객체에 TypeAdapter 등록하기
    4. 결과
  5. 최종 GsonUtils 코드

1. Gson

1.1. Gson?

Java 객체를 Json 문법으로 변환해주는 Java 라이브러리입니다.
Json 문자를 Java 객체로 변환하는것도 제공합니다.
Gson 2.9.x 버전은 Java 7 이상 버전부터 지원합니다.

Java 객체를 Json 문법으로 변환하는 것을 Json 직렬화(serialization)이라고 부르고, 반대로 Json 문법으로 작성된 문자열을 Java 객체로 변환하는 것을 역직렬화(deserialization) 라고 부릅니다.


1.2. dependency 추가

Spring에서는 dependency에 라이브러리를 추가하는 것으로 간단히 Gson을 이용할 수 있습니다.

1.2.1. gradle

gradle을 이용하고 있다면, build.gradle에 gson dependency를 추가해주면 됩니다.

dependencies {
    ...
    implementation 'com.google.code.gson:gson:2.9.0'
}

1.2.2. maven

maven을 이용하고 있다면 pom.xml에 dependency를 추가해주면 됩니다.

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.9.0</version>
</dependency>

1.2.3. 일반

만일, Spring 프로젝트가 아닌 일반 Java 프로젝트를 이용하고 있다면, Gson Github 프로젝트에 나와있는 Gson jar downloads 링크를 통해 jar파일을 다운받아 lib 디렉토리에 직접 설정해 주면 됩니다.


2. 테스트를 위한 사전 설정

2.1. html escaping 테스트

@ConfigurationProperties를 이용하여 playground.gson-test 하위 프로퍼티를 읽어들일 것입니다.

disableHtmlEscaping() 옵션 설정에 따른 직렬화 형태를 확인하기 위해 아래와 같은 프로퍼티를 사전에 정의합니다.

관련 포스팅 : [Spring Boot Core] Spring Boot Relaxed Binding using yaml


2.1.1. 프로퍼티

프로젝트에서 로드 가능한 프로퍼티 파일에 아래와같은 프로퍼티 값을 추가했습니다.

playground:
  gson-test:
    html-escaping-test: "<html><head></head><body>1234</body></html>"
    test: test

2.1.2. 프로퍼티 로드 Component 클래스

프로퍼티를 로드할 Component 클래스를 정의했습니다.

@Data
@Component
@ConfigurationProperties(prefix = "playground.gson-test")
public class GsonTest {
    private String htmlEscapingTest;
    private String stringTest;
}

2.2. LocalDateTime 관련 객체 테스트

LocalDateTime, LocalDate, LocalTime 타입의 직렬화/역직렬화 테스트를 위해 아래와 같은 클래스 파일을 생성했습니다.

@Builder
@Data
public class TestValue {
    private String name;
    private int age;
    private String job;
    private Date date;
    private LocalDateTime localDateTime;
    private LocalDate localDate;
    private LocalTime localTime;
}

3. Gson 객체 생성하기

Gson 클래스에는 객체를 json형식으로 직렬화 하기 위한 다양한 메서드들을 제공하고 있습니다.
그중 GsonBuilder는 라이브러리에서 제공하고 있는 다양한 직렬화/역직렬화 옵션을 설정하여 Gson객체를 생성해주는 기능을 제공합니다.

기본적으로 프로젝트 내에서 이용할 직렬화 옵션이 공통적으로 적용될거라면 static으로 생성하여 활용하는 것도 좋습니다.

GsonBuilder를 이용하여 설정할 수 있는 기본적인 옵션에 대해 알아보고, 각 옵션을 설정하고 설정하지 않을 때에 대한 response 변화를 확인해봅시다.


3.1. GsonUtils

프로젝트 내에서 일반적으로 이용될 Gson 객체 생성 규칙을 담은 GsonUtils 클래스를 아래와 같이 정의하였습니다.

이어서 소개될 각 옵션들에 대한 특성을 확인하고, 본인이 구현하고자하는 직렬화 양식에 맞춰 gson을 커스텀 하면 됩니다.

public class GsonUtils {
    private static String PATTERN_DATE = "yyyy-MM-dd";
    private static String PATTERN_TIME = "HH:mm:ss";
    private static String PATTERN_DATETIME = String.format("%s %s", PATTERN_DATE, PATTERN_TIME);

    private static Gson gson = new GsonBuilder()
            .disableHtmlEscaping()
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .setDateFormat(PATTERN_DATETIME)
            .create();

    public static String toJson(Object o) {
        String result = gson.toJson(o);
        if("string".equals(result))
            return null;
        return result;
    }

    public static <T> T fromJson(String s, Class<T> clazz) {
        try {
            return gson.fromJson(s, clazz);
        } catch(JsonSyntaxException e) {
            log.error(e.getMessage());
        }
        return null;
    }
}

Gson에서는 toJson 메서드에 null이 들어올 경우 문자열 "string"로 변환합니다. 위의 코드는 문자열 "string"로 내려주는 것을 방지하기 위해 if 분기문이 추가된 것이니, 불필요하다면 관련 코드를 제거하면 됩니다.


3.2. disableHtmlEscaping

gson은 기본적으로 html 문자를 escape 합니다. (<과, > 같은 문자)

disableHtmlEscaping() 옵션을 이용할 경우, html 문자를 escape 하지 않습니다.

3.2.1. 기본

Gson은 기본적으로 Html Escape 합니다.
<html><head></head><body>1234</body></html> 라는 내용이 들어있는 test1값은 아래와같이 직렬화됩니다.

01-1

3.2.2. disableHtmlEscaping() 옵션 설정

만일 disableHtmlEscaping() 옵션을 설정할 경우 Html Escape되지 않고 그대로 직렬화됩니다.

01-2


3.2.3. Test 코드

@SpringBootTest
class GsonUtilsTest {
    @Autowired
    private ValueProperties valueProperties;

    @Test
    void htmlEscapeTest() {
        String htmlTest1 = valueProperties.getHtmlTest();
        System.out.println(htmlTest1);

        String htmlTest2 = GsonUtils.toJson(valueProperties);
        System.out.println(htmlTest2);
    }
}

3.2.4. 결과

<html><head></head><body>1234</body></html>
{"htmlEscapingTest":"\u003chtml\u003e\u003chead\u003e\u003c/head\u003e\u003cbody\u003e1234\u003c/body\u003e\u003c/html\u003e","stringTest":"test"}

disableHtmlEscaping() 설정 전


<html><head></head><body>1234</body></html>
{"htmlEscapingTest":"<html><head></head><body>1234</body></html>","stringTest":"test"}

disableHtmlEscaping() 설정 후


3.3. setFieldNamingPolicy

필드명 정책의 기본값은 IDENTITY로, 클래스에 정의된 필드명 그대로 만드는것입니다.
각 FieldNamingPolicy에 따른 json 응답값을 확인하고 표현하고자하는 정책을 이용하면 됩니다.

3.3.1. IDENTITY

{"htmlEscapingTest":"<html><head></head><body>1234</body></html>","stringTest":"test"}

3.3.2. LOWER_CASE_WITH_UNDERSCORES

{"html_escaping_test":"<html><head></head><body>1234</body></html>","string_test":"test"}

3.3.3. LOWER_CASE_WITH_DASHES

{"html-escaping-test":"<html><head></head><body>1234</body></html>","string-test":"test"}

3.3.4. LOWER_CASE_WITH_DOTS

{"html.escaping.test":"<html><head></head><body>1234</body></html>","string.test":"test"}

3.3.5. UPPER_CAMEL_CASE

{"HtmlEscapingTest":"<html><head></head><body>1234</body></html>","StringTest":"test"}

3.3.6. UPPER_CAMEL_CASE_WITH_SPACES

{"Html Escaping Test":"<html><head></head><body>1234</body></html>","String Test":"test"}

3.3.7. UPPER_CASE_WITH_UNDERSCORES

{"HTML_ESCAPING_TEST":"<html><head></head><body>1234</body></html>","STRING_TEST":"test"}


3.4. setDateFormat

java.util.Date 의 포맷 형식을 정의합니다.


3.5. serializeNulls

기본적으로 Gson은 null인 필드값은 직렬화 대상에서 제외시킵니다.
만약, 필드값이 null인 경우에도 직렬화에 포함시키고 싶다면 serializeNulls() 옵션을 사용하면 됩니다.

{"name":"coco","age":0,"local_date":"2022-04-29","local_time":"11:25:27"}

serializeNulls() 설정 전


{"name":"coco","age":0,"job":null,"date":null,"local_date_time":null,"local_date":"2022-04-29","local_time":"11:24:27"}

serializeNulls() 설정 후


4. LocalDateTime 관련 객체 serialize

4.1. 테스트 코드 작성 및 에러 발생

사전에 미리 생성했었던 TestValue 클래스를 활용하여 LocalDate, LocalDateTime, LocalTime 객체를 직렬화하는 테스트를 해보도록 합니다.

에러코드를 보면 TypeAdapter 를 정의하여 LocalDateTime 객체를 json으로 변환하기 위한 규칙을 정의하라고 쓰여있습니다.

@Test
void gsonTest() {
    TestValue testValue1 = TestValue.builder().name("coco").age(24).job("programmer").date(new Date())
            .localDate(LocalDate.now()).localTime(LocalTime.now()).localDateTime(LocalDateTime.now()).build();
    String sTestValue1 = GsonUtils.toJson(testValue1);
    System.out.println(sTestValue1);
    System.out.println(GsonUtils.fromJson(sTestValue1, TestValue.class));

    System.out.println();

    TestValue testValue2 = TestValue.builder().name("coco")
            .localDate(LocalDate.now()).localTime(LocalTime.now()).build();
    String sTestValue2 = GsonUtils.toJson(testValue2);
    System.out.println(sTestValue2);
    System.out.println(GsonUtils.fromJson(sTestValue2, TestValue.class));
}

위의 Test 코드를 실행해보면 아래와 같은 에러가 발생됩니다.
에러 코드를 읽어보니, LocalDateTime 타입을 직렬화 하는 과정에서 발생된 에러임을 확인할 수 있습니다.

Failed making field 'java.time.LocalDateTime#date' accessible; either change its visibility or write a custom TypeAdapter for its declaring type
com.google.gson.JsonIOException: Failed making field 'java.time.LocalDateTime#date' accessible; either change its visibility or write a custom TypeAdapter for its declaring type
	at app//com.google.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:22)
	at app//com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:158)
	at app//com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:101)
	at app//com.google.gson.Gson.getAdapter(Gson.java:501)
	at app//com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:116)
	at app//com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:165)
	at app//com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:101)
	at app//com.google.gson.Gson.getAdapter(Gson.java:501)
	at app//com.google.gson.Gson.toJson(Gson.java:739)
    ...
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.time.LocalDate java.time.LocalDateTime.date accessible: module java.base does not "opens java.time" to unnamed module @6c779568
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
	at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
	at com.google.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:19)
	... 96 more

4.2. TypeAdapter 정의하기

gson 라이브러리에서 제공해주는 TypeAdapter를 이용하여 Gson을 이용하여 특정 객체를 직렬화/역직렬화 하는 방법을 정의할 수 있습니다.

LocalDateTime 객체를 직렬화하는 방법을 아래와 같이 정의해보았습니다.
저의 경우, yyyy-MM-dd HH:mm:ss 포맷으로 변환하도록 정의했습니다.

import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class GsonUtils {
    private static String PATTERN_DATE = "yyyy-MM-dd";
    private static String PATTERN_TIME = "HH:mm:ss";
    private static String PATTERN_DATETIME = String.format("%s %s", PATTERN_DATE, PATTERN_TIME);

    static class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
        DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_DATETIME);

        @Override
        public void write(JsonWriter out, LocalDateTime value) throws IOException {
            if(value != null)
                out.value(value.format(format));
        }

        @Override
        public LocalDateTime read(JsonReader in) throws IOException {
            return LocalDateTime.parse(in.nextString(), format);
        }
    }
}

마찬가지로 LocalDate나 LocalTime 에도 TypeAdapter를 설정합니다.

static class LocalDateAdapter extends TypeAdapter<LocalDate> {
    DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_DATE);

    @Override
    public void write(JsonWriter out, LocalDate value) throws IOException {
        out.value(value.format(format));
    }

    @Override
    public LocalDate read(JsonReader in) throws IOException {
        return LocalDate.parse(in.nextString(), format);
    }
}

static class LocalTimeAdapter extends TypeAdapter<LocalTime> {
    DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_TIME);
    @Override
    public void write(JsonWriter out, LocalTime value) throws IOException {
        out.value(value.format(format));
    }

    @Override
    public LocalTime read(JsonReader in) throws IOException {
        return LocalTime.parse(in.nextString(), format);
    }
}

4.3. Gson 객체에 TypeAdapter 등록하기

위에서 선언한 TypeAdapter들을 Gson 객체에 등록합니다.
어댑터에서 null 처리를 하기 위해 nullSafe() 설정도 해줍니다.

private static Gson gson = new GsonBuilder()
        .disableHtmlEscaping()
        .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
        .setDateFormat(PATTERN_DATETIME)
        .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe())
        .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe())
        .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter().nullSafe())
        .create();

4.4. 결과

{"name":"coco","age":24,"job":"programmer","date":"2022-04-29 11:55:55","local_date_time":"2022-04-29 11:55:55","local_date":"2022-04-29","local_time":"11:55:55"}
TestValue(name=coco, age=24, job=programmer, date=Fri Apr 29 11:55:55 KST 2022, localDateTime=2022-04-29T11:55:55, localDate=2022-04-29, localTime=11:55:55)

{"name":"coco","age":0,"local_date":"2022-04-29","local_time":"11:55:55"}
TestValue(name=coco, age=0, job=null, date=null, localDateTime=null, localDate=2022-04-29, localTime=11:55:55)

5. 최종 GsonUtils 코드

import com.google.gson.*;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

@Slf4j
public class GsonUtils {
    private static String PATTERN_DATE = "yyyy-MM-dd";
    private static String PATTERN_TIME = "HH:mm:ss";
    private static String PATTERN_DATETIME = String.format("%s %s", PATTERN_DATE, PATTERN_TIME);

    private static Gson gson = new GsonBuilder()
            .disableHtmlEscaping()
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .setDateFormat(PATTERN_DATETIME)
            .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe())
            .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe())
            .registerTypeAdapter(LocalTime.class, new LocalTimeAdapter().nullSafe())
            .create();

    public static String toJson(Object o) {
        String result = gson.toJson(o);
        if("null".equals(result))
            return null;
        return result;
    }

    public static <T> T fromJson(String s, Class<T> clazz) {
        try {
            return gson.fromJson(s, clazz);
        } catch(JsonSyntaxException e) {
            log.error(e.getMessage());
        }
        return null;
    }
    static class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
        DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_DATETIME);

        @Override
        public void write(JsonWriter out, LocalDateTime value) throws IOException {
            if(value != null)
                out.value(value.format(format));
        }

        @Override
        public LocalDateTime read(JsonReader in) throws IOException {
            return LocalDateTime.parse(in.nextString(), format);
        }
    }

    static class LocalDateAdapter extends TypeAdapter<LocalDate> {
        DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_DATE);

        @Override
        public void write(JsonWriter out, LocalDate value) throws IOException {
            out.value(value.format(format));
        }

        @Override
        public LocalDate read(JsonReader in) throws IOException {
            return LocalDate.parse(in.nextString(), format);
        }
    }

    static class LocalTimeAdapter extends TypeAdapter<LocalTime> {
        DateTimeFormatter format = DateTimeFormatter.ofPattern(PATTERN_TIME);
        @Override
        public void write(JsonWriter out, LocalTime value) throws IOException {
            out.value(value.format(format));
        }

        @Override
        public LocalTime read(JsonReader in) throws IOException {
            return LocalTime.parse(in.nextString(), format);
        }
    }
}

++

  • Gson을 이용한 객체의 직렬화 및 역직렬화
  • Spring Boot에서 객체를 json 형식으로 변환하기
  • Gson을 이용하여 객체를 json 형식으로 변환하기
  • Java에서 객체를 json로 변환하기

※ GitHub에서 playground 프로젝트(v1.0.3)를 다운받아 볼 수 있습니다.

728x90
반응형