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 기본 생성자 혹은 GsonBuilder를 통해 이용한다.
Gson gson = new Gson();
Gson gson = new GsonBuilder().create();
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"}
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'}
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은 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에 특정 시나리오에 따라 동적으로 비즈니스 로직에 맞춰 필드나 클래스를 제외할 수 있다.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을 통해 얻을 수 있는 장점은 다음과 같다.
또한 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()
);
}
}
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
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"
}
]
}
Gson gson = new GsonBuilder().serializeNulls().create();
System.out.println(gson.toJson(company));
> {"name": "En#", "address": null, "number": null, "employees": null}
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 사용에 대한 교보재로써 사용되고 있다.