[Java] 유용한 Jackson 어노테이션 정리

🔥Log·2024년 7월 23일
0

Java

목록 보기
19/22

💡 글에서 사용한 코드: 깃헙

☕ 개요


Javascript 이외의 언어로 API 서버를 만든다면, json를 읽어들이고, json으로 데이터를 내보내는 작업들이 굉장히 중요하고, Java 진영에서는 json을 다루기 위해서 Jackson이라는 라이브러리가 매우 널리 사용되고 있다.

그래서 이번 글에서는 Jackson을 좀 더 잘 사용하기 위한 Jackson에서 제공하는 다양한 어노테이션들을 직접 사용해보도록 하겠다.

바~로 ㄱㄱ



데이터 읽기


샘플 클래스들을 만들어서, RestController에서 하나 하나 호출해보았고 그 결과를 열겨하도록 하겠다.

1) JsonAnyGetter

샘플

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import java.util.Map;

public class JsonAnyGetterDummy {

    @JsonAnyGetter
    public Map<String, String> field = Map.of("key1", "value1", "key2", "value2");

}

결과

{
  "key1": "value1",
  "key2": "value2"
}

JsonAnyGetter를 사용해서 field라는 키 값이 없는 json객체로 출력이 되었다.

2) JsonGetter

샘플

import com.fasterxml.jackson.annotation.JsonGetter;

public class JsonGetterDummy {

    private String field = "Hello world";

    @JsonGetter("JSON_GETTER")
    public String getField() {
        return field;
    }

}

결과

{
  "JSON_GETTER": "Hello world"
}

JsonGeter를 사용해서 json 데이터의 키 값을 지정해주었다.

3) JsonProperty

샘플

import com.fasterxml.jackson.annotation.JsonProperty;

public class JsonPropertyDummy {

    @JsonProperty("name")
    public String key1 = "value1";

}

결과

{
  "name": "value1"
}

실제로는 key1이라는 필드이지만, JsonProperty를 이용해서 json으로 보여지는 필드명을 name으로 지정해주었다.

4) JsonPropertyOrder

샘플

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({"field3", "field1", "field2"})
public class JsonPropertyOrderDummy {

    public String field1 = "one";
    public String field2 = "two";
    public String field3 = "three";

}

결과

{
  "field3": "three",
  "field1": "one",
  "field2": "two"
}

객체에서는 field1, field2, field3 순서로 변수가 정의되어 있지만, JsonPropertyOrder에 정의한대로 field3, field1, field2의 순서대로 json이 출력된 것을 확인할 수 있다.

5) JsonFormat

샘플

import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;

public class JsonFormatDummy {

    @JsonFormat(pattern = "yyyy-MM-dd")
    public LocalDateTime formattedDate = LocalDateTime.now();
    public LocalDateTime rawDate = LocalDateTime.now();

}

결과

{
  "formattedDate": "2024-07-23",
  "rawDate": "2024-07-23T14:45:44.12569596"
}

JsonFormat은 날짜나 시간 데이터에 사용할 수 있는 어노테이션이다.
변환하고자하는 포맷만 지정해주면, 손쉽게 데이터 형식을 변환해서 json값으로 보여줄 수 있다.

6) JsonRawValue

샘플

import com.fasterxml.jackson.annotation.JsonRawValue;

public class JsonRawValueDummy {

    @JsonRawValue
    public String field = "{\"key\":\"value\", \"key2\":\"value2\"}";

}

결과

{
  "field": {
    "key": "value",
    "key2": "value2"
  }
}

json형식을 만족하는 문자열에 JsonRawValue을 붙이면 알아서 파싱해서 json형식으로 데이터를 보여준다.

7) JsonRootName

샘플

@JsonRootName(value = "root-key")
public class JsonRootNameDummy {

    public String key1 = "value1";
    public String key2 = "value2";

}

결과

    @GetMapping("/JsonRootNameDummy")
    public String JsonRootNameDummy() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
        return mapper.writeValueAsString(new JsonRootNameDummy());
    }
{
   "root-key":{
      "key1":"value1",
      "key2":"value2"
   }
}

JsonRootName을 붙인 객체를 SerializationFeature.WRAP_ROOT_VALUE옵션이 활성화된 ObjectMapper로 읽으면, 해당 데이터에 key값을 지정할 수 있다.

8) JsonUnwrapped

샘플

import com.fasterxml.jackson.annotation.JsonUnwrapped;

public class JsonUnwrappedDummy {

    public String id = "ID";
    @JsonUnwrapped
    public Music music = new Music();

    public class Music {
        public String genre = "Pop";
        public String title = "Title";
    }

}

결과

{
  "id": "ID",
  "genre": "Pop",
  "title": "Title"
}

JsonUnwrapped를 붙인 객체는 루트 키로 묶어주지 않고 Flat하게 만든다.



데이터 쓰기


1) JsonAnySetter

public class JsonAnySetterDummy {

    public HashMap<String, String> anyProperties = new HashMap<>();

    @JsonAnySetter
    public void addAnyProperties(String key, String value) {
        anyProperties.put(key, value);
    }

}

JsonAnySetter는 Map에 데이터를 추가하는 메서드에 붙여서 사용할 수 있고, 타입을 만족하는 어떠한 Key-Value든 역직렬화가 가능해진다.

결과

    @GetMapping("/JsonAnySetterDummy")
    public JsonAnySetterDummy JsonAnySetterDummy() throws JsonProcessingException {
        String json = "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"}";

        return new ObjectMapper()
                .readerFor(JsonAnySetterDummy.class)
                .readValue(json);
    }
{
  "anyProperties": {
    "key1": "value1",
    "key2": "value2",
    "key3": "value3"
  }
}

2) JsonSetter

/**
 * json으로 값을 주입할 때는 Key값을 k1, k2만 사용할 수 있음.
 * 그 외에 Key값을 사용하면, 예외 발생
 */
public class JsonSetterDummy {

    public String key1;
    public String key2;

    @JsonSetter("k1")
    public void setKey1(String key1) {
        this.key1 = key1;
    }

    @JsonSetter("k2")
    public void setKey2(String key2) {
        this.key2 = key2;
    }

}

JsonAnySetter와 역할은 비슷하지만, 지정된 값만들 역직렬화할 수 있는 어노테이션이다.

결과

    @GetMapping("/JsonSetterDummy")
    public JsonSetterDummy JsonSetterDummy() throws JsonProcessingException {
        String json = "{\"k1\":\"value1\",\"k2\":\"value2\",\"k1\":\"value3\"}";

        return new ObjectMapper()
                .readerFor(JsonSetterDummy.class)
                .readValue(json);
    }
{
  "k1": "value3",
  "k2": "value2"
}

3) JsonAlias

import com.fasterxml.jackson.annotation.JsonAlias;

public class JsonAliasDummy {

    @JsonAlias({"k1"})
    public String key1;
    public String key2;
    public String key3;

}

JsonAlias는 이렇게 필드에 붙여서 사용할 수 있고, 원래는 json의 키 값이 key이어야 객체의 key1 변수에 할당이 되지만, json의 키 값이 k1이여도 할당될 수 있도록 해주는 어노테이션이다.



객체의 필드 다루기


1) JsonIgnore

import com.fasterxml.jackson.annotation.JsonIgnore;

public class JsonIgnoreDummy {

    @JsonIgnore
    public String key1 = "value1";
    public String key2 = "value2";
    @JsonIgnore
    public String key3 = "value3";

}

JsonIgnore를 사용해서 key1key3는 역직렬화 대상에서 제외하였고, 실제로 json 응답도 아래와 같이 나온다.

{
  "key2": "value2"
}

2) JsonIgnoreProperty

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties({"key1", "key3"})
public class JsonIgnorePropertiesDummy {

    public String key1 = "value1";
    public String key2 = "value2";
    public String key3 = "value3";

}

JsonIgnore와 동일한 역할을 하지만, 클래스 레벨붙여서 사용하고 무시할 필드들을 JsonIgnoreProperties를 사용해서 지정해주었다.

3) JsonIgnoreType

import com.fasterxml.jackson.annotation.JsonIgnoreType;

public class JsonIgnoreTypeDummy {

    public IWantToBeIgnored key1 = new IWantToBeIgnored();
    public String key2 = "value2";

    @JsonIgnoreType
    public class IWantToBeIgnored {
        public String ignore1 = "ignore1";
        public String ignore2 = "ignore2";
    }

}

JsonIgrnoeType은 클래스에 붙여서 사용할 수 있는데, JsonIgnoreType이 붙은 클래스는 어디서 사용되든 역직렬화되지 않는다.

4) JsonInclude

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.AllArgsConstructor;

@JsonInclude(Include.NON_EMPTY)
@AllArgsConstructor
public class JsonIncludeDummy {

    public String key1;
    public String key2;
    public String key3;

}

JsonInclude는 어노테이션에 명시된 조건에 만족해야만 역직렬화한다.

    @GetMapping("/JsonIncludeDummy")
    public JsonIncludeDummy JsonIncludeDummy() {
        return new JsonIncludeDummy("value1", null, " ");
    }

이런 메서드를 만들어서 결과를 확인해보면, 아래와 같이 null인 값은 역직렬화되지 않은 것을 확인할 수 있다.

{
  "key1": "value1",
  "key3": " "
}

5) JsonIncludeProperties

import com.fasterxml.jackson.annotation.JsonIncludeProperties;
import lombok.AllArgsConstructor;

@JsonIncludeProperties({ "key1", "key2" })
@AllArgsConstructor
public class JsonIncludePropertiesDummy {

    public String key1;
    public String key2;
    public String key3;

}

JsonIncludeProperties에 명시된 필드 외에는 역직렬화하지 않는다.



직렬화/역직렬화


1) JsonValue

import com.fasterxml.jackson.annotation.JsonValue;
import java.util.HashMap;

public class JsonValueDummy {

    public String id = "id1";
    public Person person = new Person();

    public class Person {
        public String name = "name2";
        public int age = 30;
    }

    @JsonValue
    public HashMap<String, String> jsonResult() {
        HashMap<String, String> result = new HashMap<>();
        result.put("id", id);
        result.put("name", person.name);
        return result;
    }

}

JsonValueJsonValue가 정의되어 있는 객체가 역직렬화될 때, 어떤 식으로 역직렬화되게 할 것인지를 정의하는 어노테이션이다.
위 예시의 경우, jsonResult라는 역직렬화 용 메서드를 정의했고, 객체 본연의 모양새는 완전히 무시한 임의의 역직렬화 로직을 작성해보았다.

역직렬화의 결과는 당연하게도 아래와 같다.

{
  "name": "name2",
  "id": "id1"
}

2) JsonSerialize, JsonDeserialize

어노테이션의 이름에서도 알 수 있듯이 특정 필드 또는 객체에 대한 직렬화/역직렬화 방식을 커스텀할 수 있는 어노테이션이다.

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;

public class DummySerializer extends StdSerializer<String> {

    public DummySerializer() {
        this(null);
    }

    public DummySerializer(Class<String> t) {
        super(t);
    }

    @Override
    public void serialize(String s, JsonGenerator generator, SerializerProvider provider) throws IOException {
        generator.writeString("제목: " + s);
    }

}

나는 문자열 앞에 제목: 을 붙이는 직렬화 클래스를 하나 작성해주었고,

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import java.io.IOException;

public class DummyDeserializer extends StdDeserializer<String> {

    public DummyDeserializer() {
        this(null);
    }

    public DummyDeserializer(Class<String> t) {
        super(t);
    }

    @Override
    public String deserialize(JsonParser parser, DeserializationContext context) throws IOException, JacksonException {
        return parser.getText().strip();
    }

}

문자열 양 옆에 붙어있는 공백들을 모두 제거하는 역직렬화 클래스를 작성해주었다.

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class JsonSerializeDummy {

    @JsonSerialize(using = DummySerializer.class)
    @JsonDeserialize(using = DummyDeserializer.class)
    public String title1;

    public String title2;

}

그리고 이런 식으로 샘플 클래스를 하나 만들어주었다.

    @GetMapping("/JsonSerializeDummy")
    public JsonSerializeDummy JsonSerializeDummy() {
        return new JsonSerializeDummy("Title1", "Title2");
    }

그리고, 직렬화 테스트를 위해서 RestController에 이런 메서드를 하나 만들어서 호출해보았고, 그 결과는 아래와 같다.

{
  "title1": "제목: Title1",
  "title2": "Title2"
}

커스텀한 직렬화 클래스를 사용한 title1 필드에만 잘 적용된 것을 확인할 수 있다.

    @GetMapping("/JsonDeserializerDummy")
    public String JsonDeserializerDummy() throws JsonProcessingException {
        String json = "{\"title1\":\"    Title1          \", \"title2\":\"    Title2          \"}";

        JsonSerializeDummy dummy = new ObjectMapper()
                .readerFor(JsonSerializeDummy.class)
                .readValue(json);
        return dummy.getTitle1() + "," + dummy.getTitle2();
    }

이번엔 역직렬화 테스트를 위해서 이런 API를 하나 만들어주었고, 그 결과는 아래와 같다.

Title1,    Title2          

title1에만 공백을 제거하는 역직렬화 클래스를 적용해주었으므로, title1의 값인 "Title1"에만 공백 제거가 적용된 것을 확인할 수 있다.

3) JsonCreator

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
public class JsonCreatorDummy {

    private String title;
    private String content;

    @JsonCreator
    public JsonCreatorDummy(
            @JsonProperty("t") String title,
            @JsonProperty("c") String content
    ) {
        this.title = title;
        this.content = content;
    }

}

JsonCreator는 역직렬화를 생성자 방식으로 구현할 수 있게 해주는 어노테이션이다.
비교적 직관적으로 json 객체를 Java 객체로 역직렬화할 수 있다.

{
  "t": "제목",
  "c": "내용"
}

위와 같은 json 객체가 아래와 같은 Java 객체로 되는 것이다.

public class JsonCreatorDummy {
    String title = "제목";
    String content = "내용";
}

4) JsonTypeInfo, JsonSubTypes

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

public class JsonTypeInfoDummy {

    public Music music = new HipHop();
    public Music music2 = new Pop();

    @JsonTypeInfo(
            use = JsonTypeInfo.Id.NAME,
            property = "type"
    )
    @JsonSubTypes({
            @JsonSubTypes.Type(value = HipHop.class, name = "hiphop"),
            @JsonSubTypes.Type(value = Pop.class, name = "pop")
    })
    public static class Music {
        public String title;
    }

    public static class HipHop extends Music{
        public String title = "HipHop dummy";
    }

    public static class Pop extends Music {
        public String title = "Pop dummy";
        public String comment = "Pop comment";
    }

}

이 어노테이션은 말로는 설명하기가 쉽지 않은데, json으로 역직렬화할 때 사용될 메타 정보들을 제공함으로써 Java입장에서는 좀 더 체계적으로 객체를 관리하면서 역직렬화되도록 하는 것을 도와주는 어노테이션이다.

역직렬화 결과를 보면 쉽게 이해될 수 있을 것이다.

{
  "music": {
    "type": "hiphop",
    "title": "HipHop dummy"
  },
  "music2": {
    "type": "pop",
    "title": "Pop dummy",
    "comment": "Pop comment"
  }
}

이게 그 결과인데, Java에 정의되어 있지 않던 type이라는 값이 추가되었고, 나머지는 원래의 규칙대로 역직렬화되었다.

이 어노테이션들은 하나의 값이 구분자의 역할을 하면서 나머지 Key값들을 동적으로 변화하는 종류의 데이터를 표현할 때 사용하기 적합하다.



객체 간의 참조 다루기


1) JsonManagedReference, JsonBackReference

이 어노테이션들은 객체 간의 순환참조로 발생하는 무한 역직렬화를 방지하게 해주는 어노테이션들이다.
특히나, JPA를 사용하면 객체 간의 관계 정의를 해야해서 순환참조를 하게 되는 일이 빈번한데 이 어노테이션들을 쓰면, 무한 역직렬화를 방지할 수 있다.

먼저, 무한 역직렬화의 예시를 보도록 하자.

import java.util.ArrayList;
import java.util.List;

public class UnmanagedParent {

    public String name;
    public List<UnmanagedChild> children = new ArrayList<>();

    public UnmanagedParent(String name) {
        this.name = name;
    }

    public void addChild(UnmanagedChild child) {
        children.add(child);
        child.setParent(this);
    }

}
import lombok.Setter;

@Setter
public class UnmanagedChild {

    public String name;
    public UnmanagedParent parent;

    public UnmanagedChild(String name) {
        this.name = name;
    }

}

UnmanagedParentUnmanagedChild가 1:N인 상황이고, 아래와 같이 API를 만들어보았다.

    @GetMapping("/Reference/unmanaged")
    public UnmanagedParent unmanaged() {
        UnmanagedParent parent = new UnmanagedParent("parent1");
        parent.addChild(new UnmanagedChild("child1"));
        parent.addChild(new UnmanagedChild("child2"));
        return parent;
    }

이 API를 호출해보자. ㅎㅎ

히이ㅣ익!! ㅋㅋ

역시나 무한 역직렬화가 발생한 것을 확인할 수 있다.

이번엔 JsonManagedReferenceJsonBackReference를 사용해서 무한 역직렬화를 방지해보자.

import com.fasterxml.jackson.annotation.JsonManagedReference;
import java.util.ArrayList;
import java.util.List;

public class Parent {

    public String name;
    @JsonManagedReference
    public List<Child> children = new ArrayList<>();

    public Parent(String name) {
        this.name = name;
    }

    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }

}
import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.Setter;

@Setter
public class Child {

    public String name;
    @JsonBackReference
    public Parent parent;

    public Child(String name) {
        this.name = name;
    }

}

그리고, 아래와 같이 API를 만들어주었다.

    @GetMapping("/Reference/managed")
    public Parent managed() {
        Parent parent = new Parent("parent1");
        parent.addChild(new Child("child1"));
        parent.addChild(new Child("child2"));
        return parent;
    }

이번에는 어떻게 동작할지 API를 호출해보자.

깔.끔.하게 무한 역직렬화 없이 잘 출력된 것을 확인할 수 있다.



🙏 참고


0개의 댓글