1. 개요

이 사용방법(예제)에서는 Jackson에서 양방향 관계 를 처리하는 가장 좋은 방법을 살펴보겠습니다 .

먼저 Jackson JSON 무한 재귀 문제에 대해 논의합니다. 그런 다음 양방향 관계로 엔터티를 직렬화하는 방법을 살펴보겠습니다. 마지막으로 역직렬화합니다.

2. 무한 재귀

Jackson 무한 재귀 문제를 살펴보겠습니다. 다음 예제 에는 간단한 일대다 관계를 가진 " 사용자 "와 " 항목 "이라는 두 개의 엔터티가 있습니다 .

" 사용자 " 엔터티:

public class User {
    public int id;
    public String name;
    public List<Item> userItems;
}

" 항목 " 엔터티:

public class Item {
    public int id;
    public String itemName;
    public User owner;
}

" Item "의 인스턴스를 직렬화하려고 하면 Jackson이 JsonMappingException 예외를 발생시킵니다.

@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenSerializing_thenException()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    new ObjectMapper().writeValueAsString(item);
}

전체 예외 는 다음과 같습니다 .

com.fasterxml.jackson.databind.JsonMappingException:
Infinite recursion (StackOverflowError) 
(through reference chain: 
org.baeldung.jackson.bidirection.Item["owner"]
->org.baeldung.jackson.bidirection.User["userItems"]
->java.util.ArrayList[0]
->org.baeldung.jackson.bidirection.Item["owner"]
->…..

다음 몇 섹션에서 이 문제를 해결하는 방법을 살펴보겠습니다.

3. @JsonManagedReference , @JsonBackReference 사용

먼저 Jackson이 관계를 더 잘 처리할 수 있도록 @JsonManagedReference 및 @JsonBackReference로 관계어노테이션 을 달아 보겠습니다.

다음은 " 사용자 " 엔터티입니다.

public class User {
    public int id;
    public String name;

    @JsonManagedReference
    public List<Item> userItems;
}

그리고 " 항목 ":

public class Item {
    public int id;
    public String itemName;

    @JsonBackReference
    public User owner;
}

이제 새 엔터티를 테스트해 보겠습니다.

@Test
public void givenBidirectionRelation_whenUsingJacksonReferenceAnnotationWithSerialization_thenCorrect() throws JsonProcessingException {
    final User user = new User(1, "John");
    final Item item = new Item(2, "book", user);
    user.addItem(item);

    final String itemJson = new ObjectMapper().writeValueAsString(item);
    final String userJson = new ObjectMapper().writeValueAsString(user);

    assertThat(itemJson, containsString("book"));
    assertThat(itemJson, not(containsString("John")));

    assertThat(userJson, containsString("John"));
    assertThat(userJson, containsString("userItems"));
    assertThat(userJson, containsString("book"));
}

다음은 Item 개체를 직렬화한 결과입니다.

{
 "id":2,
 "itemName":"book"
}

다음은 User 개체를 직렬화한 결과입니다.

{
 "id":1,
 "name":"John",
 "userItems":[{
   "id":2,
   "itemName":"book"}]
}

참고:

  • @JsonManagedReference 는 정상적으로 직렬화되는 참조의 전방 부분입니다.
  • @JsonBackReference 는 참조의 뒷부분입니다. 직렬화에서 생략됩니다.
  • 직렬화된 항목 개체에는 사용자 개체 에 대한 참조가 포함되어 있지 않습니다 .

또한 어노테이션을 전환할 수 없습니다. 다음은 직렬화에 대해 작동합니다.

@JsonBackReference
public List<Item> userItems;

@JsonManagedReference
public User owner;

그러나 개체를 역직렬화하려고 하면 컬렉션에서 @JsonBackReference 를 사용할 수 없으므로 예외가 발생 합니다.

직렬화된 항목 개체에 사용자에 대한 참조가 포함되도록 하려면  @JsonIdentityInfo 를 사용해야 합니다 . 다음 섹션에서 이에 대해 살펴보겠습니다.

4. @JsonIdentityInfo 사용

이제 @JsonIdentityInfo 를 사용하여 양방향 관계가 있는 엔터티의 직렬화를 도울 수 있는 방법을 알아보겠습니다 .

" User " 엔터티 에 클래스 수준 어노테이션을 추가합니다 .

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class User { ... }

그리고 " 항목 " 엔터티:

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class Item { ... }

테스트 시간:

@Test
public void givenBidirectionRelation_whenUsingJsonIdentityInfo_thenCorrect()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, containsString("userItems"));
}

직렬화 결과는 다음과 같습니다.

{
 "id":2,
 "itemName":"book",
 "owner":
    {
        "id":1,
        "name":"John",
        "userItems":[2]
    }
}

5. @JsonIgnore 사용

또는 @JsonIgnore 어노테이션을 사용 하여 관계 측면 중 하나를 무시 하여 체인을 끊을 수 있습니다.

다음 예제에서는 직렬화에서 " User " 속성 " userItems " 를 무시하여 무한 재귀를 방지합니다 .

다음은 " 사용자 " 엔터티입니다.

public class User {
    public int id;
    public String name;

    @JsonIgnore
    public List<Item> userItems;
}

테스트는 다음과 같습니다.

@Test
public void givenBidirectionRelation_whenUsingJsonIgnore_thenCorrect()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, not(containsString("userItems")));
}

마지막으로 직렬화 결과는 다음과 같습니다.

{
 "id":2,
 "itemName":"book",
 "owner":
    {
        "id":1,
        "name":"John"
    }
}

6. @JsonView 사용

새로운 @JsonView 어노테이션을 사용하여 관계의 한쪽을 제외할 수도 있습니다.

다음 예제에서는 두 개의 JSON 보기( PublicInternal)를 사용합니다. 여기서 InternalPublic 을 확장합니다 .

public class Views {
    public static class Public {}

    public static class Internal extends Public {}
}

내부 보기 에 포함될 사용자 필드 userItems를 제외하고 공용 보기 에 모든 사용자항목 필드를 포함합니다.

다음은 "사용자" 엔터티입니다 .

public class User {
    @JsonView(Views.Public.class)
    public int id;

    @JsonView(Views.Public.class)
    public String name;

    @JsonView(Views.Internal.class)
    public List<Item> userItems;
}

다음은 " 항목" 엔터티입니다.

public class Item {
    @JsonView(Views.Public.class)
    public int id;

    @JsonView(Views.Public.class)
    public String itemName;

    @JsonView(Views.Public.class)
    public User owner;
}

공용 보기 를 사용하여 직렬화할 때 직렬화 에서 userItems 를 제외했기 때문에 올바르게 작동합니다 .

@Test
public void givenBidirectionRelation_whenUsingPublicJsonView_thenCorrect() 
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writerWithView(Views.Public.class)
      .writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, not(containsString("userItems")));
}

그러나 내부 보기 를 사용하여 직렬화하면 모든 필드가 포함되기 때문에 JsonMappingException 이 발생합니다.

@Test(expected = JsonMappingException.class)
public void givenBidirectionRelation_whenUsingInternalJsonView_thenException()
  throws JsonProcessingException {
 
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    new ObjectMapper()
      .writerWithView(Views.Internal.class)
      .writeValueAsString(item);
}

7. 사용자 지정 직렬 변환기 사용

다음으로 사용자 지정 직렬 변환기를 사용하여 양방향 관계로 엔터티를 직렬화하는 방법을 살펴보겠습니다.

다음 예제에서는 사용자 지정 serializer를 사용하여 " User " 속성 " userItems: " 를 직렬화합니다.

다음은 " 사용자 " 엔터티입니다.

public class User {
    public int id;
    public String name;

    @JsonSerialize(using = CustomListSerializer.class)
    public List<Item> userItems;
}

다음은 " CustomListSerializer: " 입니다.

public class CustomListSerializer extends StdSerializer<List<Item>>{

   public CustomListSerializer() {
        this(null);
    }

    public CustomListSerializer(Class<List> t) {
        super(t);
    }

    @Override
    public void serialize(
      List<Item> items, 
      JsonGenerator generator, 
      SerializerProvider provider) 
      throws IOException, JsonProcessingException {
        
        List<Integer> ids = new ArrayList<>();
        for (Item item : items) {
            ids.add(item.id);
        }
        generator.writeObject(ids);
    }
}

이제 직렬 변환기를 테스트해 보겠습니다. 알 수 있듯이 올바른 종류의 출력이 생성되고 있습니다.

@Test
public void givenBidirectionRelation_whenUsingCustomSerializer_thenCorrect()
  throws JsonProcessingException {
    User user = new User(1, "John");
    Item item = new Item(2, "book", user);
    user.addItem(item);

    String result = new ObjectMapper().writeValueAsString(item);

    assertThat(result, containsString("book"));
    assertThat(result, containsString("John"));
    assertThat(result, containsString("userItems"));
}

다음 은 사용자 지정 직렬 변환기를 사용한 직렬화 의 최종 출력 입니다.

{
 "id":2,
 "itemName":"book",
 "owner":
    {
        "id":1,
        "name":"John",
        "userItems":[2]
    }
}

8. @JsonIdentityInfo 로 역직렬화

이제 @JsonIdentityInfo 를 사용하여 양방향 관계로 엔터티를 역직렬화하는 방법을 살펴보겠습니다 .

다음은 " 사용자 " 엔터티입니다.

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class User { ... }

그리고 " 항목 " 엔터티:

@JsonIdentityInfo(
  generator = ObjectIdGenerators.PropertyGenerator.class, 
  property = "id")
public class Item { ... }

구문 분석하려는 일부 수동 JSON 데이터로 시작하여 올바르게 구성된 엔터티로 마무리하는 빠른 테스트를 작성합니다.

@Test
public void givenBidirectionRelation_whenDeserializingWithIdentity_thenCorrect() 
  throws JsonProcessingException, IOException {
    String json = 
      "{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";

    ItemWithIdentity item
      = new ObjectMapper().readerFor(ItemWithIdentity.class).readValue(json);
    
    assertEquals(2, item.id);
    assertEquals("book", item.itemName);
    assertEquals("John", item.owner.name);
}

9. 사용자 지정 디시리얼라이저 사용

마지막으로 사용자 지정 역직렬 변환기를 사용하여 양방향 관계로 엔터티를 역직렬화합니다.

다음 예제에서는 사용자 지정 디시리얼라이저를 사용하여 " User " 속성 " userItems: " 를 구문 분석합니다.

다음은 " 사용자 " 엔터티입니다.

public class User {
    public int id;
    public String name;

    @JsonDeserialize(using = CustomListDeserializer.class)
    public List<Item> userItems;
}

다음은 " CustomListDeserializer: " 입니다.

public class CustomListDeserializer extends StdDeserializer<List<Item>>{

    public CustomListDeserializer() {
        this(null);
    }

    public CustomListDeserializer(Class<?> vc) {
        super(vc);
    }

    @Override
    public List<Item> deserialize(
      JsonParser jsonparser, 
      DeserializationContext context) 
      throws IOException, JsonProcessingException {
        
        return new ArrayList<>();
    }
}

마지막으로 간단한 테스트는 다음과 같습니다.

@Test
public void givenBidirectionRelation_whenUsingCustomDeserializer_thenCorrect()
  throws JsonProcessingException, IOException {
    String json = 
      "{\"id\":2,\"itemName\":\"book\",\"owner\":{\"id\":1,\"name\":\"John\",\"userItems\":[2]}}";

    Item item = new ObjectMapper().readerFor(Item.class).readValue(json);
 
    assertEquals(2, item.id);
    assertEquals("book", item.itemName);
    assertEquals("John", item.owner.name);
}

10. 결론

이 기사에서는 Jackson을 사용하여 양방향 관계로 엔티티를 직렬화/역직렬화하는 방법을 설명했습니다.

이러한 모든 예제 및 코드 조각의 구현은 GitHub 프로젝트 에서 찾을 수 있습니다 . 이것은 Maven 기반 프로젝트이므로 그대로 가져오고 실행하기 쉬워야 합니다.

Jackson footer banner