Spring 순환 참조 에러

꾸준하게 달리기~·2023년 8월 20일
0

스프링 + 자바

목록 보기
18/20
post-thumbnail

들어가기 앞서

혹시 toString() 매서드를 알고 있는가?

직관적으로 해석해도, String으로 만든다.
라는 느낌이다.

음.. 무슨소리인지 모르겠는 걸? 헤헤.. 싶으시면 다음 코드를 보자.

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));

        ArrayList<Integer> intArr = new ArrayList<>();
        intArr.add(1);
        intArr.add(2);
        intArr.add(3);
        intArr.add(4);

        System.out.println("숫자 배열의 toString은 " + intArr.toString());


        ArrayList<String> stringArr = new ArrayList<>();
        stringArr.add("가");
        stringArr.add("나");
        stringArr.add("다");
        stringArr.add("라");

        System.out.println("문자 배열의 toString은 " + stringArr.toString());

    }

}

해당 코드의 실행 결과는 아래와 같다.

숫자 배열의 toString은[1, 2, 3, 4]
문자 배열의 toString은[가, 나, 다, 라]

나는, 숫자 ArrayList 혹은 문자 ArrayList
toString매서드를 만든적이 없다.
그런데 어떻게 위와 같이 출력이 되는걸까?

그 비밀은 아래와 같다.
toString은 어떻게 실행되어라! 라고
이미 만들어져있는것이다.

소스 코드를 들어가 살펴보면,
append 에서 각각의 원소를 ,
시작과 끝엔 [,]을 붙이라고 나와있는 모습을 볼 수있다.

    public String toString() {
        Iterator<E> it = iterator();
        if (! it.hasNext())
            return "[]";

        StringBuilder sb = new StringBuilder();
        sb.append('[');
        for (;;) {
            E e = it.next();
            sb.append(e == this ? "(this Collection)" : e);
            if (! it.hasNext())
                return sb.append(']').toString();
            sb.append(',').append(' ');
        }
    }

이렇게 기본으로 내장된 함수에는, 우리가 편리하게 사용할 수 있도록
구현되어있는 수없이 많은 매서드와 기본값들이 있다.

여기까지 이해가 가셨다면, 본론으로 들어가겠다.



순환 참조 에러

흔히 관계형 DB와 Spring data JPA를 기반으로 개발을 하다보면,
FK와 같은 외래 키를 설정해야 하고,
해당 내용을
@OneToMany 등의 어노테이션으로 작성한다.

쉬운 예시로는 아래와 같다.

public class Parents {
    String name;
    
    @OneToMany
    List<Child> children;
}

class Child {
    String name;
    
    @ManyToOne
    Parents parents;
}

부모 클래스에서 여러개의 Child 를 갖고있고, (친자식 여러명 가능)
자식 클래스에서 하나의 Parents를 갖고있는 모습이다. (친부모는 한명)

그럼, 부모 P 1명, 자식 C 1명이 있다고 하자.
우리가 해당 data를
Jackson 라이브러리의 기본값으로 조회하면
무슨 일이 일어날까?

다음과 같다.

 	{
  		name : "P"
      	children : 	{
      					{
      						name : "C"
      						parents : 	{
      										name : "P"
      										childeren : 	{
      															name : "C"
      															parents : 	{
      
      
      //이 사이에도 수없이 많은 parents와 children의 향연 (무한)
      //이 사이에도 수없이 많은 parents와 children의 향연
      //이 사이에도 수없이 많은 parents와 children의 향연
      
      
    																		}
    														}
    									}
    					}

    				}
	}

위와 같이 계속해서
parents -> children -> parents -> .... 이런식으로 무한히 조회된다.
이게 서로가 서로를 참조하는 순환 참조 에러이다.



순환 참조 에러 이유

Spring boot에는 기본적으로
Jackson 라이브러리가 존재한다.

해당 라이브러리는 데이터를 Json 형태로
직렬화, 역직렬화, 보여주는 방식 등 json에 관한 라이브러리이다.

이제 눈치가 빠른 사람은 이해했을지도 모른다.

들어가기 앞서 부분을 다시 보면,
나는 toString을 정의한적이 없다.

마찬가지이다.

Jackson 라이브러리에서,
Data를 조회하는 방식을 위처럼 만들어놓았기 때문에,
parents -> children -> parents -> .... 의 굴레가 끝나지 않고 계속되는 것이다.

즉, 라이브러리 설계가
직렬화를 하여 data를 보여줄때 처음부터 그렇게 작동되도록 설계된것이다.

설계하는 분들은 순환 참조를 가정하지 않았으니까.




그럼 어떻게 해결할까?

  • @JsonManagedReference , @JsonBackReference

@OneToMany@JsonManagedReference를,
@ManyToOne@JsonBackReference 를 붙이면 해결된다.

public class Parents {
    String name;
    
    @OneToMany
    @JsonManagedReference
    List<Child> children;
}

class Child {
    String name;
    
    @ManyToOne
    @JsonBackReference
    Parents parents;
}

Jackson 라이브러리는
각각의 애너테이션이 붙은 필드를 역직렬화 할때
순환 참조 에러를 방지한다.
즉, 다음과 같이 어느 선에서 끊어준다는 이야기이다.

{
  name : "P"
  children : 	{
  					name : "C"
				}
}



그리고, 다른 방법도 있다.
맨 위에서의 toString 방식과 같다.

  • data를 보여주는 형식을 직접 설정하는것이다.
   @Getter
   @AllArgsConstructor
   public static class ParentsResponseDto {
   
		private Sting name;
        private List<String> childrenNames;
        
   }

위와 같이 Parents 의 응답 data를 이름과 아이들 이름으로 정하면,
더이상 Parents를 보여줄 때 Child 클래스를 들어갈 일이 없으므로
아래와 같이 반환한다.

{
  name : "P"
  	childrenNames : {
  					name : "C"
					}
}

이런 방식들로 순환 참조 에러를 해결할 수 있다.

profile
반갑습니다~! 좋은하루 보내세요 :)

0개의 댓글