💡 글에서 사용한 코드: 깃헙
Javascript 이외의 언어로 API 서버를 만든다면, json를 읽어들이고, json으로 데이터를 내보내는 작업들이 굉장히 중요하고, Java 진영에서는 json을 다루기 위해서 Jackson이라는 라이브러리가 매우 널리 사용되고 있다.
그래서 이번 글에서는 Jackson을 좀 더 잘 사용하기 위한 Jackson에서 제공하는 다양한 어노테이션들을 직접 사용해보도록 하겠다.
바~로 ㄱㄱ
샘플 클래스들을 만들어서, RestController에서 하나 하나 호출해보았고 그 결과를 열겨하도록 하겠다.
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객체로 출력이 되었다.
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 데이터의 키 값을 지정해주었다.
import com.fasterxml.jackson.annotation.JsonProperty;
public class JsonPropertyDummy {
@JsonProperty("name")
public String key1 = "value1";
}
{
"name": "value1"
}
실제로는 key1
이라는 필드이지만, JsonProperty
를 이용해서 json으로 보여지는 필드명을 name
으로 지정해주었다.
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이 출력된 것을 확인할 수 있다.
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값으로 보여줄 수 있다.
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형식으로 데이터를 보여준다.
@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값을 지정할 수 있다.
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하게 만든다.
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"
}
}
/**
* 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"
}
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
이여도 할당될 수 있도록 해주는 어노테이션이다.
import com.fasterxml.jackson.annotation.JsonIgnore;
public class JsonIgnoreDummy {
@JsonIgnore
public String key1 = "value1";
public String key2 = "value2";
@JsonIgnore
public String key3 = "value3";
}
JsonIgnore
를 사용해서 key1
과 key3
는 역직렬화 대상에서 제외하였고, 실제로 json 응답도 아래와 같이 나온다.
{
"key2": "value2"
}
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
를 사용해서 지정해주었다.
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
이 붙은 클래스는 어디서 사용되든 역직렬화되지 않는다.
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": " "
}
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
에 명시된 필드 외에는 역직렬화하지 않는다.
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;
}
}
JsonValue
는 JsonValue
가 정의되어 있는 객체가 역직렬화될 때, 어떤 식으로 역직렬화되게 할 것인지를 정의하는 어노테이션이다.
위 예시의 경우, jsonResult
라는 역직렬화 용 메서드를 정의했고, 객체 본연의 모양새는 완전히 무시한 임의의 역직렬화 로직을 작성해보았다.
역직렬화의 결과는 당연하게도 아래와 같다.
{
"name": "name2",
"id": "id1"
}
어노테이션의 이름에서도 알 수 있듯이 특정 필드 또는 객체에 대한 직렬화/역직렬화 방식을 커스텀할 수 있는 어노테이션이다.
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"에만 공백 제거가 적용된 것을 확인할 수 있다.
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 = "내용";
}
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값들을 동적으로 변화하는 종류의 데이터를 표현할 때 사용하기 적합하다.
이 어노테이션들은 객체 간의 순환참조로 발생하는 무한 역직렬화를 방지하게 해주는 어노테이션들이다.
특히나, 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;
}
}
UnmanagedParent
와 UnmanagedChild
가 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를 호출해보자. ㅎㅎ
히이ㅣ익!! ㅋㅋ
역시나 무한 역직렬화가 발생한 것을 확인할 수 있다.
이번엔 JsonManagedReference
와 JsonBackReference
를 사용해서 무한 역직렬화를 방지해보자.
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를 호출해보자.
깔.끔.하게 무한 역직렬화 없이 잘 출력된 것을 확인할 수 있다.