[Java/Android] Gson 라이브러리 하나부터 열까지

mhyun, Park·2023년 6월 4일
3

Gson은 Google에서 개발한 자바 라이브러리로, JSON 데이터와 자바 객체 간의 직렬화 및 역질렬화를 처리하는 데 사용된다.
Gson을 사용하면 JSON(JavaScript Object Notation) 형식의 데이터를 자바 객체로 변환하거나, 자바 객체를 Json 형식으로 변환하는 작업 시 별도의 Mapping 로직 구현없이 간편하게 처리할 수 있기때문에 Json 작업을 위한 필수 라이브러리로 사용되고 있다.

이번 포스팅은 Gson이 2022년 2월에 Release한 v2.9.0을 기준으로 설명한다.
https://mvnrepository.com/artifact/com.google.code.gson/gson

Gson의 주요 기능 및 사용법

기본적으로 Gson 라이브러리는 아래와 같이 Gson 기본 생성자 혹은 GsonBuilder를 통해 이용한다.

Gson gson = new Gson();
Gson gson = new GsonBuilder().create();

1. toJson() - 객체를 Json으로 직렬화

Gson은 자바의 기본 데이터 유형, 컬렉션(List, Map), 사용자 정의 객체 등 수많은 종류의 data 객체를 Json으로 변환할 수 있다.

Gson gson = new Gson();
Person person = new Person("mh", 30, "mh@example.com");

// field 이름을 key로 지정하여 json string을 생성한다.
String json = gson.toJson(person);
System.out.println(json);

> {"name":"mh", "age":30, "email":"mh@example.com"}

2. fromJson() - Json을 객체로 역직렬화

Gson은 Json 데이터를 읽고 Reflection을 통해 key에 mapping되는 필드 값을 자동으로 할당하고 객체를 생성한다.

Gson gson = new Gson();
String json = "{\"name\":\"mh\", \"age\":30, \"email\":\"mh@example.com\"}";

// json key와 일치하는 이름을 가진 field에 값을 할당한다.
Person person = gson.fromJson(json, Person.class);
System.out.println(person);

> Person{name='mh', age=30, email='mh@example.com'}

3. @Since, setVersion() - Versioning 정책 설정

Gson은 @Since annotation을 지원하며, 특정 버전부터 해당 요소가 도입되었음을 나타낸다.
해당 annotation은 개발자에게 지원되는 기능이나 변경 사항이 언제부터 사용할 수 있는 알려주어 개발과 유지 보수 과정에서 도움을 줄 수 있다.

예를들어, 아래의 예시와 같이 email 필드에 Since(1.1) annotation을 사용하여 해당 필드가 Gson 라이브러리의 1.1 버전부터 지원된다는 것을 나타낼 수 있다. 그리고 1.1 버전 미만의 Gson을 사용하는 경우에는 email 필드는 무시되어 처리된다.

public class Person {
    @Since(1.0)
    private String name;
    
    @Since(1.0)
    private int age;
    
    @Since(1.1)
    private String email;
}    

Gson 1.0 버전에서의 직렬화

Gson gson = new GsonBuilder().setVersion(1.0).create();
Person person = new Person("mh", 30, "mh@example.com");

String json = gson.toJson(person);
System.out.println(json);

> {"name":"mh", "age":30}

Gson 1.0 버전에서의 역직렬화

Gson gson = new GsonBuilder().setVersion(1.0).create();
String json = "{\"name\":\"mh\", \"age\":30, \"email\":\"mh@example.com\"}";

Person person = gson.fromJson(json, Person.class);
System.out.println(person);

> Person{name='mh', age=30, email=null}

@Since annotation을 통해 얻을 수 있는 장점은 다음과 같다.

  • 명확성 : 개발자에게 특정 기능이나 변경 사항이 어느 버전부터 지원되는지 명확하게 알려줄 수 있다. 이를 통해 개발자는 Gson 라이브러리의 업데이트 또는 기능 사용 시점을 결정하는 데 도움을 준다.
  • 호환성 : 각 기능의 지원 범위를 쉽게 파악할 수 있을뿐만 아니라 Gson 라이브러리의 변경 사항이 있을 때 개발자들이 해당 버전에 따라 작업을 수행할 수 있도록 안내할 수 있다. (하위호환성)

4. setExclusionStrategies() - 객체 직렬화/역직렬화 제외 전략 설정

Gson은 setExclusionStrategies 메서드를 통해 객체 직렬화/역직렬화 과정에서 제외할 필드 또는 클래스를 설정할 수 있으며 ExclusionStrategy 인터페이스를 구현한 객체를 매개변수로 받아 처리한다.

다음 예제는 Field 제외 전략을 설정하는 과정을 나타낸다.

1. 제외 전략에 대한 Custom Annotation 정의

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Exclude {
}

2. 제외 전략 설정

public class MyExclusionStrategy implements ExclusionStrategy {
    @Override
    public boolean shouldSkipField(FieldAttributes fieldAttributes) {
        // @Exclude annotation이 붙은 Field는 skip한다.
        return fieldAttributes.getAnnotation(Exclude.class) != null;
    }
    
    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
        return false;
    }
}

3. 제외 전략을 적용한 Gson 생성 및 운용

public class Person {
    private String name;
    private int age;
    
    @Exclude
    private String email;
}    
Gson gson = new GsonBuilder()
	.setExclusionStrategies(new AnnotationBasedExclusionStrategy())
    .create();
    
Person person = new Person("mh", 30, "mh@example.com");
String json = gson.toJson(person);

System.out.println(json);

> {"name":"mh","age":30}

이렇게 Gson은 setExclusionStrategies 메서드를 통해 객체 직렬화 동작을 보다 세밀하게 제어할 수 있어 안정성과 유연성을 제공할 수 있으며 해당 메서드를 통해 얻을 수 있는 장점은 다음과 같다.

  • 정교한 직렬화 제어 : 객체의 특정 정보를 숨기거나, 보안상 민감한 정보나 크기가 큰 필드를 제어하여 직렬화할 필요가 없는 부분을 제외할 수 있다.
  • 유연한 사용자 정의 : ExclusionStrategy 인터페이스를 구현하여 사용자 정의 직렬화 제외 전략을 만들 수 있다. 이를 통해 개발자는 Runtime에 특정 시나리오에 따라 동적으로 비즈니스 로직에 맞춰 필드나 클래스를 제외할 수 있다.

5. @SeriralizedName - Custom 직렬화 및 역직렬화 설정 (1)

Gson은 @SeriralizedName annotation을 사용하여 커스텀 직렬화 및 역직렬화를 설정할 수 있는 기능도 제공한다.
이를 통해 Gson이 객체의 특정 필드를 Json으로 변환하거나 Json 데이터를 특정 필드로 역직렬화할 때 사용자 정의 로직을 적용하여 객체의 필드와 Json 데이터의 Key를 mapping할 수 있다.

public class Person {
	@SerializedName("full_name")
    private String fullName;
    
	@SerializedName("birth_day")
    private String birthDay;
}    

// @SerializedName에 명시된 keyName으로 설정된 것을 볼 수 있다.
> {"full_name":"park amugae", "birth_day":"1992-11-13"}

이러한 @SeriralizedName annotation을 통해 얻을 수 있는 장점은 다음과 같다.

  • 정확성 : 객체 필드 이름과 Json Key가 다른 경우에도 정확한 mapping이 가능하다.
  • 유연성 : Json 데이터 구조가 변경되어 객체 필드 이름이 변경되었더라도 mapping을 유지할 수 있다.
  • 가독성 : 필드와 Json key의 mapping이 annotation을 통해 명시되기 때문에 개발자는 어떤 필드와 어떤 Json key와 연관되어 있는지 쉽게 파악할 수 있다.
  • 일관성 : 각기 다른 여러 개의 Json 데이터 소스를 처리할 때 공통된 key 값을 사용함으로써 일관성있는 처리가 가능하다.

6. @JsonAdapter, registerTypeAdapter() - Custom 직렬화 및 역직렬화 설정 (2)

또한 Gson은 @JsonAdapter annotation을 사용하여 특정 필드나 데이터 형식을 Custom하게 직렬화/역직렬화 처리할 수 있는 기능을 제공한다.
예를 들어 다음과 같은 Person 객체는 코드 운용의 편의성을 위해 birtyDay의 타입으로 Calendar를 사용하고 있다고 가정하자.

public class Person {
    private String name;
    private Calendar birthDay;
}   

하지만, 소통하고 있는 Server나 다른 Service에선 Timestamp (long) 형식으로 birthDay data를 다루고 있다고 한다면..

> {"name":"mh", "birthDay":721612800000}

Gson은 birthDay에 대해 역직렬화 수행시 long 에서 Calendar type으로 강제로 변환할 수 없는 상황이 발생하게 된다. 이에 따라, 개발자는 해당 필드에 대해 custom 변환을 통해 처리해야하며, Gson에서는 이를 위해 "이런 유형의 데이터가 오면 이렇게 다루어라" 를 명시하는 registerTypeAdapter 메서드를 제공하고 있다.

1. Person 객체 변환을 위한 Adapter 클래스 구현

private static class PersonAdapter implements JsonDeserializer<Person>, JsonSerializer<Person> {

    @Override
    public Person deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        if (json == null || json.getAsJsonObject() == null) {
            return null;
        }

        final JsonObject jsonObject = json.getAsJsonObject();
        final String name = jsonObject.get("name").getAsString();
        final long birthDay = jsonObject.get("birthDay").getAsLong();

        final Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(birthDay);

        return new Person(name, calendar);
    }

    @Override
    public JsonElement serialize(Person person, Type typeOfSrc, JsonSerializationContext context) {
        if (person == null) {
            throw new NullPointerException("person object is null");
        }

        final JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("name", person.getName());
        jsonObject.addProperty("birthDay", person.getBirthDay().getTimeInMillis());

        return jsonObject;
    }
}

2. PersonAdapter를 통한 변환이 필요한 데이터 객체 또는 필드에 @JsonAdapter Annotation 적용

@JsonAdapter(PersonAdapter.class)
public class Person {
    private String name;
    private Calendar birthDay;
}

3. GsonBuilder에 PersonAdapter에 대한 TypeAdapter 등록

Gson gson = new GsonBuilder()
	.registerTypeAdapter(Person.class, new PersonAdapter())
    .create();
String json = "{\"name\":\"mh\",\"birthDay\":721612800000}";
Person person = gson.fromJson(json, Person.class);

> Person{"name":"mh","birthDay":{"year":1992,"month":10,"dayOfMonth":13,"hourOfDay":9,"minute":0,"second":0}}

이렇게 Gson의 'registerTypeAdapter' 메서드를 통해 복잡한 변환 로직을 Gson 내부에 캡슐화할 수 있게 된다.
만약 TypeAdapter를 사용하지 못했다면, 개발자는 다음과 같이 Mapping을 위한 Adapter 객체와 같은 성격을 가지는 별도의 data 객체를 하나 더 운용해야했을 것이다.

ex) Mapper를 통한 객체 변환

// Json 과 maaping 되는 data 객체
public class PersonDTO {
    private String name;
    private long birthDay;
}   

// PersonDTO 와 maaping 되는 data 객체
public class Person {
    private String name;
    private Calendar birthDay;
}   

// PersonDTO <--> Person 변환을 위한 Adapter
class PersonAdapter {
    public static PersonDTO toDTO(Person person) {
        final Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(person.getBirthDay());
        
        return new PersonDTO(person.getName(), calendar);
    }
    
    public static Person fromDTO(PersonDTO personDto) {
        return Person(
            personDto.getName(),
            personDto.getBirthDay().getTimeInMillis()
        );
    }
}

7. TypeToken - Generic Type의 직렬화 및 역직렬화 설정

Gson은 기본적으로 Type이 명시된 Collection에 대해 직렬화/역직렬화 기능을 제공한다.

ex) List

Person employee1 = new Person("park", 30, "서울");
Person employee2 = new Person("kim", 29, "경기도");
Person employee3 = new Person("choi", 20, "충청남도");
Company company = new Company("En#", List.of(employee1, employee2, employee3));

// Company class에 명시된 List<Person>에 대한 직렬화/역직렬화 수행

Gson gson = new GsonBuilder().setPrettyPrinting().create();
String json = gson.toJson(company);
System.out.println(json);

Company deserializedCompany = gson.fromJson(json, Company.class);
System.out.println(deserializedCompany);
{
  "name": "En#",
  "employees": [
    {
      "name": "park",
      "age": 30,
      "city": "서울"
    },
    {
      "name": "kim",
      "age": 29,
      "city": "경기도"
    },
    {
      "name": "choi",
      "age": 20,
      "city": "충청남도"
    }
  ]
}

Company{name='En#', employees=[Person{name='park', age=30, city='서울'}, Person{name='kim', age=29, city='경기도'}, Person{name='choi', age=20, city='충청남도'}]}

ex) Map

Person employee1 = new Person("park", 30, "서울");
Person employee2 = new Person("kim", 29, "경기도");
Person employee3 = new Person("choi", 20, "충청남도");
Company company = new Company("En#", Map.of(1, employee1, 2, employee2, 3, employee3));

// Company class에 명시된 Map<Inteager, Person>에 대한 직렬화/역직렬화 수행

Gson gson = new GsonBuilder().setPrettyPrinting().create();
String json = gson.toJson(company);
System.out.println(json);

Company deserializedCompany = gson.fromJson(json, Company.class);
System.out.println(deserializedCompany);
{
  "name": "En#",
  "employees {
    "3": {
      "name": "choi",
      "age": 20,
      "city": "충청남도"
    },
    "2": {
      "name": "kim",
      "age": 29,
      "city": "경기도"
    },
    "1": {
      "name": "park",
      "age": 30,
      "city": "서울"
    }
  }
}

Company{name='En#', employees={3=Person{name='choi', age=20, city='충청남도'}, 2=Person{name='kim', age=29, city='경기도'}, 1=Person{name='park', age=30, city='서울'}}}

이는, Company 클래스에 이미 List 혹은 Map<Inteager, Person> 타입의 필드가 선언되어 있기 때문에 Gson은 역직렬화할 때 필드의 타입 정보를 활용하여 해당 타입으로 객체를 생성할 수 있었던 것이다.

하지만, 아래의 예제와 같은 Json이 있을 경우 List에 대한 역직렬화가 불가능하다.

Gson gson = new GsonBuilder().setPrettyPrinting().create();
String jsonList = "[{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}, {\"name\":\"Jane\",\"age\":25,\"city\":\"London\"}]";

// List<Person> 역직렬화
List<Person> personList = gson.fromJson(jsonList, List.class);

왜냐하면, Gson이 List.class Map.class 만으론 어떤 타입의 요소인지 명확히 알 수 없기 때문이다. 즉, 이와 같은 경우엔 TypeToken 을 직접적으로 명시하여 역직렬화에 대한 Generic Type 정보를 명시적으로 제공하여야 한다.

Gson gson = new GsonBuilder().setPrettyPrinting().create();

String jsonList = "[{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}, {\"name\":\"Jane\",\"age\":25,\"city\":\"London\"}]";

// List<Person> 역직렬화
Type listType = new TypeToken<List<Person>>() {}.getType();
List<Person> personList = gson.fromJson(jsonList, listType);

System.out.println("List<Person>:");
for (Person person : personList) {
    System.out.println("Name: " + person.getName());
    System.out.println("Age: " + person.getAge());
    System.out.println("City: " + person.getCity());
}

System.out.println("-------------------------");
  
String jsonMap = "{\"key1\":[{\"name\":\"John\",\"age\":30,\"city\":\"New York\"}], \"key2\":[{\"name\":\"Jane\",\"age\":25,\"city\":\"London\"}]}";
  
// Map<String, List<Person>> 역직렬화
Type mapType = new TypeToken<Map<String, List<Person>>>() {}.getType();
Map<String, List<Person>> personMap = gson.fromJson(jsonMap, mapType);

System.out.println("Map<String, List<Person>>:");
for (Map.Entry<String, List<Person>> entry : personMap.entrySet()) {
    String key = entry.getKey();
    List<Person> value = entry.getValue();
    System.out.println("Key: " + key);
    for (Person person : value) {
        System.out.println("Name: " + person.getName());
        System.out.println("Age: " + person.getAge());
        System.out.println("City: " + person.getCity());
    }
}
List<Person>:
Name: John
Age: 30
City: New York

Name: Jane
Age: 25
City: London
--------------------------
Map<String, List<Person>>:
Key: key1
Name: John
Age: 30
City: New York

Key: key2
Name: Jane
Age: 25
City: London

8. setPrettyPrinting() - 자동 개행 및 줄 바꿈이 적용된 jsonString을 생성

Gson gson = new GsonBuilder().setPrettyPrinting().create();
System.out.println(gson.toJson(company));

{
  "name": "En#",
  "address": "seoul",
  "number": "02-123-4567",
  "employees": [
    {
      "name": "kim",
      "age": "29"
    },
    {
      "name": "park",
      "age": "30"
    },
    {
      "name": "choi",
      "age": "27"
    }
  ]
}

9. serializeNulls() - 필드 값이 null이여도 무시하지 않고 "null"로 출력

Gson gson = new GsonBuilder().serializeNulls().create();
System.out.println(gson.toJson(company));

> {"name": "En#", "address": null, "number": null, "employees": null}

10. setDateFormat() - Field로 존재하는 Date 객체에 대한 기본 format을 설정

public class Person {
    private Date birthDay;
}   

Gson gson = new GsonBuilder().setDateFormat("MM/dd/yy HH:mm:ss").create();
System.out.println(gson.toJson(person));

> {"birthDay": "11/13/92 09:00:00"}

이번 포스팅에선 Gson library가 제공하는 주요 API와 사용법에 대해 알아보았다.
이렇게 Gson을 사용하면 Json 데이터와 객체 간의 변환 작업을 간단하고 편리하게 처리할 수 있으며, 유연하고 강력한 기능덕분에 Gson은 수많은 Java 기반 프로젝트에서 Json 사용에 대한 교보재로써 사용되고 있다.

profile
Android Framework Developer

0개의 댓글