혹시 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 방식과 같다.
@Getter
@AllArgsConstructor
public static class ParentsResponseDto {
private Sting name;
private List<String> childrenNames;
}
위와 같이 Parents 의 응답 data를 이름과 아이들 이름으로 정하면,
더이상 Parents를 보여줄 때 Child 클래스를 들어갈 일이 없으므로
아래와 같이 반환한다.
{
name : "P"
childrenNames : {
name : "C"
}
}
이런 방식들로 순환 참조 에러를 해결할 수 있다.