상세 컨텐츠

본문 제목

[Spring] 장애 기록 - JsonMappingException: Infinite recursion (StackOverflowError)

Backend

by choiDev 2024. 2. 1. 00:13

본문

현재 몸 담고 있는 스터디 팀 (Meow Developers)에서는 오늘의 집을 클론 코딩을 하고 있습니다.
클론코딩하면서 단순히 동일한 기능을 찍어내겟다! 라기 보단 해당 기능을 어떤 생각으로 기획했는지
의도를 분석하고 최대한 동일한 환경을 구축하려고 노력하고 있습니다.

저희 팀은 기획자를 모시지 않고 있으며 개발자가 개발만? No
도메인을 이해 못하면 테스트 케이스도 제대로 못 뽑는다 라는 철학을 가지고 움직이고 있습니다.

 

본 글은 프로젝트를 진행하면서 2테이블의 관계를 OneToMany ManyToOne으로 관계를 맺고 사용하는데

직렬화 과정에서 

JsonMappingException: Infinite recursion (StackOverflowError)  에러가 떨어져 

원인 확인 및 해소 과정을 기록 해 봅니다.

 

설명을 위한 코드는 예제 코드로 진행했습니다.

 

문제 : A객체, B객체가 상호참조가 된 상태에서 Json 직렬화를 시도하니 장애가 발생

Exception :JsonMappingException: Infinite recursion (StackOverflowError)

여기서 잠깐!
  상호참조라는게 무슨뜻일까? 아래 코드를 보며 이해해보자

 

public class BagDto {

    private Long id;

	//ItemDto를 클래스 변수로 가짐
    private List<ItemDto> itemList;
}

public class ItemDto {

    //BagDto를 클래스 변수로 가짐
    private BagDto bag;
}

public static void main(String[] args){
    BagDto bag1 = new BagDto();

    ItemDto item1 = new ItemDto();

    // 상호간 참조 설정 (Bag 입장에서 item을 참조)
    bag1.setItemList(Arrays.asList(item1));

    // 상호간 참조 설정 (item 입장에서 Bag을 참조)
    item1.setBag(bag1);
}

 

코드에서 본대로 BagDto과 ItemDto는 각자를 변수로 가지고있다 이것만으로는 상호 참조가 활성화 되지 않고 

main 함수를 읽어보면 bag1과 item1은 서로를 변수로 셋팅했다  이부분이 상호 참조를 한 부분니다.

만약 bag1혹은 item1 둘중 한쪽만 set으로 참조 되고 상호참조가 되지 않았다면 에러는 발생하지 않습니다.

 


 

지금까지는 상호 참조를 알아 봤고

어떻게 상호 참조된 객체가 Json으로 직렬화를 시도할때 에러가 발생하는가를 알려면 어느 타이밍에 직렬화가 발생하고 해당 타이밍이 에러 발생 타이밍인지를 알고 있어야 합니다.

[Json 직렬화가 대표적으로 실행되는 경우]
1. HTTP 요청/응답 
2. Jackson혹은 Gson과 같은 직렬화/역직렬화 라이브러리를 활용하여 객체를 -> json같은 통신 가능한 데이터로 변경 시
3. 데이터베이스에 Json 객체를 저장할때

 

즉 Spring 서버로 HTTP 통신의 요청과 응답 시,

객체를 json으로 만들어 확인하려고 할때,

PostgreSql같은 Json을 저장하는 DB가 있다면 해당 하는곳에 ORM기술로 저장을 할때 등입니다.

 

[에러가 발생하는 코드]

public String letsGoStackOverFlowDtoVer() throws JsonProcessingException {
        BagDto bag1 = new BagDto();
        BagDto bag2 = new BagDto();

        ItemDto item1 = new ItemDto();
        ItemDto item2 = new ItemDto();

        // 양방향 참조 설정
        bag1.setItemList(Arrays.asList(item1));
        bag2.setItemList(Arrays.asList(item2));

        item1.setBag(bag1);
        item2.setBag(bag2);

        List<BagDto> bags = Arrays.asList(bag1, bag2);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        String json = objectMapper.writeValueAsString(bags);
        System.out.println(json);
        return "OK";
}

 

 

위에서 봤던 그대로 상호 참조가 된 객체를 json으로 직렬화하려고 할때 발생하는 코드 입니다. 

Json 직렬화 2번째 상황을 재현한 것이고 에러 메세지는 아래와 같다.

com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) 
(through reference chain: 
com.example.be_study.service.test.dto.ItemDto["bag"]->
com.example.be_study.service.test.dto.BagDto["itemList"]->
java.util.Arrays$ArrayList[0]->
com.example.be_study.service.test.dto.ItemDto["bag"]->
com.example.be_study.service.test.dto.BagDto["itemList"]->
java.util.Arrays$ArrayList[0]->
com.example.be_study.service.test.dto.ItemDto["bag"]->
com.example.be_study.service.test.dto.BagDto["itemList"]->
java.util.Arrays$ArrayList[0]->
com.example.be_study.service.test.dto.ItemDto["bag"]->
com.example.be_study.service.test.dto.BagDto["itemList"]->
java.util.Arrays$ArrayList[0]->
com.example.be_study.service.test.dto.ItemDto["bag"]->
com.example.be_study.service.test.dto.BagDto["itemList"]->
java.util.Arrays$ArrayList[0]->... 이하 생략 이게 계속 반복됨

해당 에러 메세지의 뜻은 

JsonMapping중 Exception이 발생했고 무한 재귀현상으로 인해 StackOverflow를 발생시킨 겁니다. 

그리고 뒤에 이어지는 참조 관계를 보면

ItemDto에 있는 bag이라는 객체를 직렬화 하려고 했더니 ->

bag은 List<ItemDto>를 가지고 있었고 ->

List<ItemDto>의 객체를 직렬화하기위해 0번째 index의 ItemDto를 직렬화를 시도 해보니 Bag을 참조 하고 있다.

 

이 순환 참조의 고리를 못벗어나 Json 직렬화 에러가 발생하게 됩니다. 

 

이것을 해소하는 방법은 다양하게 있습니다.

 

1. @JsonIgnore를 활용

2. @JsonManagedReference(직렬화 대상 어노테이션) @JsonBackReference (직렬화 제외 대상 어노테이션) 

3. 상호참조관계가 되지 않도록 설계한다. 

4. JPA Entity의 경우 Dto로 상호참조관계가 아니도록 변환해서 Json 직렬화한다.


1. @JsonIgnore를 통해 아예 직렬화 대상에서 제외하는 방법

@Getter
@Setter
public class ItemDto {

    private String itemName;

    @JsonIgnore
    private BagDto bag; //itemList에 달아도 되고 어쨋든, @JsonIgnore를 달아서 상호참조관계만 끊어주면됩니다.
}

@Getter
@Setter
public class BagDto {

    private Long id;

    private List<ItemDto> itemList;
}

결과값
[
    {
     "id":null,
     "itemList":[{"itemName":null}]
    },
    {
     "id":null,
     "itemList":[{"itemName":null}]
    }
]


2. @JsonManagedReference(직렬화 대상 어노테이션) @JsonBackReference (직렬화 제외 대상 어노테이션) 방법

@Getter
@Setter
public class BagDto {

    private Long id;

    @JsonManagedReference
    private List<ItemDto> itemList;
}


@Getter
@Setter
public class ItemDto {

    private String itemName;

    @JsonBackReference
    private BagDto bag;
}

 

이렇게 BagDto의 itemList를 직렬화 대상으로 선정하고 
ItemDto의 bag을 제외대상으로 선정한뒤 Json 직렬화를 하고 나면 에러 발생도 나지 않으며 결과값은 아래와 같이 bag이 제외된 채로 나온다

결과값:

[
    {
     "id":null,
     "itemList":[{"itemName":null}]
    },
    {
     "id":null,
     "itemList":[{"itemName":null}]
    }
]

 

역으로 itemList를 제외한다면 결과값은 아래처럼 표시한다.

[
    {"id":null},
    {"id":null}
]

 

이처럼 @JsonManagedReference 방식을 사용해서 제외를 할것이면 꼭 자신이 만들고자 하는 json 형태를 표현할수있도록 주의해서 달아야한다. 


3. 상호참조관계가 되지 않도록 설계한다. 

3번같은 경우는 사전 방지가 가능하나 JPA를 사용할 경우 OneToMany, ManyToOne 등을 활용해 상호 참조관계가 필수가 될경우 불가능한 해소 방법입니다.


4. JPA Entity의 경우 Dto로 상호참조관계가 아니도록 변환해서 Json 직렬화한다.

JPA Entity를 본연 그대로 Json 직렬화 하지말고 DTO를 만들어 상호참조관계를 제외한 설계를 한 상태로 직렬화를 하면 해당 에러를 회피 할 수있습니다.

 

긴 글 읽어 주셔서 감사합니다.

의아한 점이나 피드백 주시면 반영하도록 하겠습니다

감사합니다.

관련글 더보기