Jackson 라이브러리 기본기능 정리 - json 직렬화와 역직렬화
Jackson Annotation 예제
본 글은 다음 원문을 번역 및 수정한 글입니다.
https://www.baeldung.com/jackson-annotations
전체 코드는 다음 레파지토리에 있습니다. 필요하다면 클론 받으셔서 test 패키지에 있는 JacksonTest.class 를 보시면 한 눈에 볼 수 있습니다.
https://github.com/JunHoPark93/jackson-practice
Jackson Serialization Annotation
이번 내용은 Baeldung 기술 블로그의 Jackson 관련 내용을 정리하려고 합니다. 정리하게 된 계기는 사내에서도 주로 쓰는 라이브러리이고 제대로 모르고 지나친다면 문제가 될 수 있는 부분들이 존재하기 때문입니다.
참고로 예시에서 접근제한자가 public 인 것들이 있는 이유는 (직렬화, 역직렬화 할때 필요하기도 하고) getter setter 넣을 시 코드가 길어지고, Lombok을 쓰자니 Jackson 애노테이션과 자칫 혼동할 수 있기 때문에 그렇게 하였습니다.
여기서 말하는 직렬화란 객체를 전송가능한 형태로 말아주는걸 의미하고 역직렬화란 그 데이터들을 다시 자바 객체로 변환해주는 것으로 이해하시면 됩니다.
@JsonAnyGetter
JsonAnyGetter 는 Map 필드를 다루는데 유연성을 제공합니다.
Member 클래스가 Map으로 속성을 가지고 있다고 가정 해보겠습니다.
public class Member {
public String name;
private Map<String, String> properties;
public Member(String name) {
this.name = name;
}
@JsonAnyGetter
public Map<String, String> getProperties() {
return properties;
}
public void add(String attr, String val) {
this.properties.put(attr, val);
}
}
getProperties 메서드에 @JsonAnyGetter 가 붙어있습니다. 이 속성은 ObjectMapper를 이용해서 json String 으로 변환해보겠습니다.
@Test
public void json_any_getter() throws JsonProcessingException {
Member member = new Member("Jay");
member.add("favorite", "chicken");
member.add("hobby", "tennis");
String result = new ObjectMapper().writeValueAsString(member);
assertThat(result, containsString("favorite"));
assertThat(result, containsString("hobby"));
}
테스트는 통과합니다. 결과가 어떻게 나오는지 살펴보면,
{"name":"Jay","favorite":"chicken","hobby":"tennis"}
모든 속성이 동일한 depth로 있는것을 볼 수 있습니다. 즉 Map 타입이 전부 key-value 형식으로 json에 들어있는 것을 볼 수 있습니다. 그렇다면 @JsonAnyGetter 애노테이션을 제거하고 실행해볼까요?
{"name":"Jay","properties":{"favorite":"chicken","hobby":"tennis"}}
그러면 Map 필드는 다음과 같이 properties 로 한 번 감싸진 형태의 json으로 나오게 됩니다. 자바단에서 Map을 쓴다면 @JsonAnyGetter를 이용해서 필요에 맞게 커스텀 할 수 있겠죠.
@JsonGetter
@JsonGetter는 메서드 이름을 getter 메서드로 표현하기 위해 @JsonProperty 애노테이션의 대안으로 쓸 수 있습니다.
public class Member2 {
public int id;
private String name;
public Member2(int id, String name) {
this.id = id;
this.name = name;
}
@JsonGetter("name")
public String getTheName() {
return name;
}
}
@JsonGetter 의 이름으로 name을 썼는데요. 저 name 필드를 get 하는 것으로 쉽게 이해할 수 있습니다.
@Test
public void json_getter() throws JsonProcessingException {
Member2 member = new Member2(1, "Jay");
String result = new ObjectMapper().writeValueAsString(member);
assertThat(result, containsString("Jay"));
assertThat(result, containsString("1"));
}
매우 직관적으로 알 수 있겠네요.
@JsonPropertyOrder
@JsonPropertyOrder 를 통해서 직렬화 하는 속성의 순서를 정할 수 있습니다.
@JsonPropertyOrder({ "name", "id" })
public class Member3 {
public int id;
public String name;
public Member3(int id, String name) {
this.id = id;
this.name = name;
}
}
다음과 같이 순서를 지정하면 직렬화 시 필드의 순서를 결정할 수 있게 됩니다.
{"name":"My bean","id":1}
저 애노테이션이 없다면 id name 순으로 나오게 되겠죠.
@JsonValue
@JsonValue 는 전체 인스턴스를 직렬화할 때 사용하는 단일 메서드를 나타냅니다. 예를들어, enum에서 getName 메서드에 @JsonValue를 넣어주어 이름을 통해 직렬화하게 합니다.
말이 조금 어려울 수 있는데 예시를 볼까요.
public enum TypeEnumWithValue {
TYPE1(1, "치킨"), TYPE2(2, "피자");
private Integer id;
private String name;
TypeEnumWithValue(Integer id, String name) {
this.id = id;
this.name = name;
}
@JsonValue
public String getName() {
return name;
}
}
이 enum 은 TYPE1 과 TYPE2가 있습니다. 하단의 getName에 @JsonValue 애노테이션이 붙어있죠. 저 애노테이션이 없다면, 이 enum을 json 으로 직렬화를 할 때 enum 이름으로 직렬화가 됩니다.
new ObjectMapper().writeValueAsString(TypeEnumWithValue.TYPE1);
즉 이 결과가 다음과 같이 나오게 됩니다 (애노테이션 없을 시)
TYPE1
enum의 name 필드로 직렬화를 하고 싶다면 (치킨, 피자..) 저렇게 enum의 name이 나오면 원하는 결과를 얻을 수 없습니다. @JsonValue 애노테이션을 붙여준다면 직렬화 시 나오는 필드의 이름을 결정합니다. 결과적으로 다음과 같은 내용을 얻을 수 있습니다.
@JsonValue
public String getName() {
return name;
}
치킨
@JsonRootName
@JsonRootName 애노테이션은 root wrapper의 이름을 설정할 수 있습니다. 아래와 같은 json을,
{
"id": 1,
"name":"Jay"
}
{
"Member": {
"id": 1,
"name": "Jay"
}
}
위와 같이 Root 이름을 지정할 수 있습니다.
@JsonRootName(value = "member")
public class Member4 {
public int id;
public String name;
public Member4(int id, String name) {
this.id = id;
this.name = name;
}
}
위와 같이 JsonRootName 을 member로 지정해 놓고 결과를 출력한다면 wrapping 된 결과를 얻을 수 있습니다.
@Test
public void json_root_name() throws JsonProcessingException {
Member4 user = new Member4(1, "Jay");
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
String result = mapper.writeValueAsString(user);
System.out.println(result);
assertThat(result, containsString("Jay"));
assertThat(result, containsString("member"));
}
한 가지 주의 점이 있다면 ObjectMapper 설정을 변경 해주어야 합니다.
mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
@JsonSerialize
@JsonSerialize 는 엔티티를 marshalling 할 때 사용자가 지정한 커스텀 serializer를 쓸 수 있게 해줍니다. 무슨 말인지 하나씩 살펴봅시다. 직렬화를 어떻게 할지 사용자가 커스텀을 한다고 했을 때는 StdSerializer를 이용해서 구현할 수 있습니다.
StdSerializer를 상속받아서 커스텀한 클래스를 만듭니다.
public class CustomDateSerializer extends StdSerializer<Date> {
private static SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
public CustomDateSerializer() {
this(null);
}
public CustomDateSerializer(Class<Date> t) {
super(t);
}
@Override
public void serialize(Date value, JsonGenerator gen, SerializerProvider arg2) throws IOException {
gen.writeString(formatter.format(value));
}
}
커스텀할 부분을 serialize 라는 메서드를 재정의해서 만듭니다. 예시에는 날짜 데이터를 포매팅하고 있습니다.
@Test
public void json_serialize() throws ParseException, JsonProcessingException {
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
String toParse = "20-12-2014 02:30:00";
Date date = df.parse(toParse);
Event event = new Event("party", date);
String result = new ObjectMapper().writeValueAsString(event);
assertThat(result, containsString(toParse));
}
다음과 같이 객체 필드의 포맷은 변경하지 않고 직렬화 시 변경해주고 싶은 필드만 커스텀하게 변경할 수 있습니다.
Jackson Deserialization Annotations
이제는 역직렬화에 대해서 알아보겠습니다.
@JsonCreator
@JsonCreator를 이용해서 역직렬화 할 때 생성자나 팩토리를 수정할 수 있습니다.
{
"id":1,
"theName":"Jay"
}
이런 json이 있다고 할 때 역직렬화 할 객체에는 (이 json을 객체로 매핑하는 과정을 의미합니다) theName이라는 필드를 쓰기 싫습니다. 그냥 name이라는 필드를 쓰고 싶다고 가정한다면 다음과 같이 할 수 있습니다.
unmarshalling 과정의 조금의 절차를 넣어주면 됩니다. @JsonCreator 와 @JsonProperty를 사용함으로써 해결할 수 있습니다.
public class Member5 {
public int id;
public String name;
@JsonCreator
public Member5(@JsonProperty("id") int id, @JsonProperty("theName") String name) {
this.id = id;
this.name = name;
}
}
@JsonCreator를 생성자 위에 달아주고 프로퍼티르 이름을 json key 에 해당하는 값을 넣어줍니다. 그리고 원하는 변수명에 넣어주면 됩니다.
@Test
public void json_creator() throws JsonProcessingException {
String json = "{\n" +
" \"id\":1,\n" +
" \"theName\":\"Jay\"\n" +
"}";
Member5 member = new ObjectMapper().readerFor(Member5.class).readValue(json);
assertThat(member.name, is(member.name));
}
다음과 같이 objectmapper의 readFor과 readValue의 조합으로 원하는 객체로 역직렬화를 할 수 있습니다.
혹은 @JsonSetter 로 다음과 같이 만들 수 있습니다.
// 중략...
private String name;
@JsonSetter("name")
public void setTheName(String name) {
this.name = name;
}
@JacksonInject
@JacksonInject 는 json 데이터가 아닌 곳에서 값을 주입할 때 쓰입니다.
public class Member6 {
@JacksonInject
public int id;
public String name;
}
@Test
public void json_inject() throws JsonProcessingException {
String json = "{\"name\":\"Jay\"}";
InjectableValues injectableValues = new InjectableValues.Std().addValue(int.class, 5);
Member6 member = new ObjectMapper().reader(injectableValues).forType(Member6.class).readValue(json);
assertThat(member.id, is(5));
}
@JsonAnySetter
@JsonAnySetter 는 Map 을 다룰 때 유용합니다. 역직렬화 과정에서 json 프로퍼티의 값들을 map 에 손쉽게 담을 수 있습니다.
public class Member7 {
public String name;
public Map<String, String> properties = new HashMap<>();
@JsonAnySetter
public void add(String key, String value) {
properties.put(key, value);
}
}
@Test
public void json_any_setter() throws IOException {
String json = "{\"name\":\"Jay\",\"favorite\":\"chicken\",\"hobby\":\"tennis\"}";
Member7 member = new ObjectMapper().readerFor(Member7.class).readValue(json);
assertThat(member.properties.get("favorite"), is("chicken"));
}
그러면 위와 같은 프로퍼티들이 map 에 하나씩 담기게 됩니다.
@JsonDeserialize
@JsonDeserialize는 커스텀 역직렬화를 만들 수 있습니다.
public class Event2 {
public String name;
@JsonDeserialize(using = CustomDateDeserializer.class)
public Date eventDate;
}
커스텀 역직렬화 클래스는 다음과 같이 만들 수 있습니다.
public class CustomDateDeserializer extends StdDeserializer<Date> {
private static SimpleDateFormat formatter = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
public CustomDateDeserializer() {
this(null);
}
public CustomDateDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Date deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
String date = parser.getText();
try {
return formatter.parse(date);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}
StdDeserializer를 상속받은 직렬화기를 만들었는데 StdDeserializer의 상위 클래스인 JsonDeserializer를 상속받아서 만들어도 됩니다.
@Test
public void json_deserializer() throws JsonProcessingException {
String json = "{\"name\":\"Jay event\",\"eventDate\":\"20-12-2020 01:01:00\"}";
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy hh:mm:ss");
Event2 event = new ObjectMapper()
.readerFor(Event2.class)
.readValue(json);
assertThat(df.format(event.eventDate), is("20-12-2020 01:01:00"));
}
@JsonAlias
@JsonAlias는 역직렬화를 할 때 한 개 이상의 이름을 한 객체 필드에 매핑되게 설정할 수 있습니다.
public class Member8 {
@JsonAlias({"name", "his_name", "her_name"})
public String name;
public String hobby;
}
Member8 클래스는 name, his_name, her_name 중 어떤 것으로 들어오든 name에 매핑 시킬 수 있습니다.
@Test
public void json_alias() throws JsonProcessingException {
String json1 = "{\"name\":\"Jay\",\"hobby\":\"tennis\"}";
String json2 = "{\"his_name\":\"Jay\",\"hobby\":\"tennis\"}";
String json3 = "{\"her_name\":\"Jay\",\"hobby\":\"tennis\"}";
Member8 member1 = new ObjectMapper().readerFor(Member8.class).readValue(json1);
Member8 member2 = new ObjectMapper().readerFor(Member8.class).readValue(json2);
Member8 member3 = new ObjectMapper().readerFor(Member8.class).readValue(json3);
assertThat(member1.name, is("Jay"));
assertThat(member2.name, is("Jay"));
assertThat(member3.name, is("Jay"));
}
Jackson Property Inclusion Annotations
@JsonIgnoreProperties
@JsonIgnoreProperties 애노테이션은 클래스 레벨에 붙일 수 애노테이션이며 Jackson이 무시할 필드를 설정할 수 있습니다.
@JsonIgnoreProperties({"id"})
public class Member9 {
public int id;
public String name;
public Member9(int id, String name) {
this.id = id;
this.name = name;
}
}
@Test
public void json_ignore_properties() throws JsonProcessingException {
Member9 member = new Member9(1, "Jay");
String result = new ObjectMapper().writeValueAsString(member);
assertThat(result, containsString("Jay"));
assertThat(result, not(containsString("1")));
}
위와 같이 객체의 id 필드는 직렬화시 제외 시킨 결과가 나옵니다.
혹은 클래스레벨에서 쓰기 싫다면 필드에 붙이는 애노테이션도 존재합니다.
@JsonIgnore
@JsonIgnore
public int id;
public String name;
@JsonIgnoreProperties와 같이 id 가 직렬화시 제외됩니다.
혹은 이너 클래스 전부를 제외시키고 싶다면 @JsonIgnoreType을 쓰면 됩니다.
// 클래스 내부
@JsonIgnoreType
public static class Name {
public String firstName;
public String lastName;
}
@JsonInclude
@JsonInclude NON_NULL 은 null을 직렬화시 제외시킵니다.
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Member10 {
public int id;
public String name;
public Member10(int id, String name) {
this.id = id;
this.name = name;
}
}
@Test
public void json_include() throws JsonProcessingException {
Member10 member = new Member10(1, null);
String result = new ObjectMapper().writeValueAsString(member);
assertThat(result, containsString("1"));
assertThat(result, not(containsString("name")));
}
@JsonAutoDetect
@JsonAutoDetect를 이용하여 필드의 직렬화 대상을 정할 수 있습니다. 서두에 필드 접근제한자에 대해서 신경쓰지 말라고 했는데 요번 파트는 잘 보셔야 합니다.
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Member11 {
private int id;
private String name;
public Member11(int id, String name) {
this.id = id;
this.name = name;
}
}
id 와 name은 private 입니다. 접근할 수 있는 경로가 없습니다. 물론 getter 메서드를 만들면 해결되는 문제이긴 하지만, 없다고 가정하겠습니다.
그래서 @JsonAutoDetect의 가시성 범위를 설정합니다. ANY로 준다면 private 필드까지 모두 직렬화시 접근 가능합니다.
@Test
public void json_auto_detect_visibility_any() throws JsonProcessingException {
Member11 member = new Member11(1, "Jay");
String result = new ObjectMapper().writeValueAsString(member);
assertThat(result, containsString("1"));
assertThat(result, containsString("Jay"));
}
JsonAutoDetect의 가시성 범위는 ANY, NON_PRIVATE, PROTECTED_AND_PUBLIC 등등 여러 속성이 있으니 필요한 것들을 골라 쓰면 될 것 같습니다.
@JsonProperty
직렬화시 설정할 수 있는 이름을 지정하는 애노테이션 입니다.
private String name;
@JsonProperty("name")
public void setTheName(String name) {
this.name = name;
}
@JsonProperty("name")
public String getTheName() {
return name;
}
Jackson Annotation 비활성화
모든 애노테이션을 비활성화하려면 다음과 같은 옵션을 준 ObjectMapper를 이용하면 됩니다.
@Test
public void disable_jackson_annotation() {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(MapperFeature.USE_ANNOTATIONS);
// 중략
// 이 mapper를 사용하면, 애노테이션이 전부 비활성화 됩니다
}
제가 실제로 겪었던 문제
한 모듈에서 json 데이터를 가져올 때 어떤 DTO를 사용하고 있었습니다. 그 DTO 에서 @JsonDeserialize로 한 필드를 Hash 하면서 가져오고 있었죠. 당연히 중요하고 민감한 값이었습니다. 문제는 다른 모듈로 데이터를 전송하고 같은 DTO로 또 역직렬화를 한게 문제였습니다. 다른 필드들은 값이 같았지만 Hash 한 필드는 한 번 더 Hash가 진행되는 참사가 일어났습니다. DTO를 다른 것을 쓰면 되었지만 클래스가 너무 많아져서 하나의 DTO로 통신했던 것이 문제였습니다.
결국 한쪽에서 역직렬화할 때 사용하는 ObjectMapper를
mapper.disable(MapperFeature.USE_ANNOTATIONS);
설정을 통해 @JsonDeserialize를 비활성화 시켜서 해결을 하였지만 더 좋은 방법이 있는지 고민중입니다.
지금 까지 Jackson Annotation의 기본 사용방법을 알아봤는데요. 고급기능들은 업무나 사이드에서 맞닥뜨리게 될 때 정리를 하려고 합니다.