velog에서 이전한 글 입니다.

순환참조


다음과 같은 ERD가 있다.
등록된 book을 findAll한다고 했을 때 문제가 발생한다.

List<Book> all = bookRepository.findAll();
System.out.println(all);

Book entity에 @ToString 을 적용했다면 println에서 자동으로 toString이 동작한다.
예상한 결과는

[
    {
        "id": 1,
        "title": "자바의정석",
        "author": "남궁성",
        "price": 10000,
        "stock": 10,
        "bookStore": {
            "id": 1,
            "name": "스파르타 서울",
            "address": "서울시 강남구"
        },
      "purchase" : {
              "id" : 1,
            "member" : {
                ...
                  ..
            }
      }
    },
      ...

이런 느낌이었지만 실제 결과는 계속 순환되며 참조되어 StackOverflowError가 발생했다.
book -> bookStore -> book -> bookStore .... 와 같이 계속 순환되는 참조가 있다.
findAll이 Error를 던지지는 않는다. findAll은 [com.example.jpa_relation_test.entity.Book@429a501f, com.example.jpa_relation_test.entity.Book@34d511c0, ...] 참조 주소만 가지고 있을 뿐 참조를 계속 따라들어가는 동작을 하진 않는다.

(Book)
    public BookStore getBookStore() {
        return this.bookStore;
    }

    public List<Purchase> getPurchases() {
        return this.purchases;
    }

    public String toString() {
        Long var10000 = this.getId();
        return "Book("...." + "bookStore=" + this.getBookStore() + ", purchases=" + this.getPurchases() + ")";
    }

lombok의 ToString은 위와 같이 toString을 만들고 참조 객체를 그대로 전달한다. 객체의 String은 toString으로 동작하고

(BookStore)
    public List<Book> getBookList() {
        return this.bookList;
    }

    public String toString() {
        Long var10000 = this.getId();
        return "BookStore("...." + "bookList=" + this.getBookList() + ")";
    }

BookStore의 toString은 bookList를 전달하면서 순환이 시작된다.
즉 String으로 출력할 때(return이나 print) 순환이 발생한다.

순환참조 해결방법

@JsonBackReference/@JsonManagedReference

(BookStore)
    @OneToMany(mappedBy = "bookStore")
    @JsonBackReference
    private List<Book> bookList = new ArrayList<>();

(Book)
    @ManyToOne
    @JsonManagedReference
    private BookStore bookStore;

위와 같이 jackson 어노테이션을 추가해줄 수 있다. One쪽이 Back, Many쪽이 Managed 이다.

    public List<BookStore> findAllBookStore() {
        List<BookStore> all = bookStoreRepository.findAll();
        return all;
    }

jackson을 추가한 후 bookStore를 findAll 했을 때

[
    {
        "id": 1,
        "name": "스파르타 서울",
        "address": "서울시 강남구"
    },
    {
        "id": 2,
        "name": "스파르타 부산",
        "address": "부산시 해운대구"
    }
  ...
]

순환 참조가 발생하지 않고 String을 만든 것을 볼 수 있다.
해당 방법은 추가 설정이 없다면 Many에 속한 entity를 조회할 때 One쪽을 불러와준다. 그래서 위의 결과는 bookStore가 One쪽이라 book을 불러오지 않은 것을 볼 수 있다.

(BookStore)
    @OneToMany(mappedBy = "bookStore")
    @JsonBackReference
    private List<Member> member = new ArrayList<>();

(Member)
    @ManyToOne
    @JoinColumn(name = "SpartaStoreId")
    @JsonManagedReference
    private BookStore bookStore;

위의 entity 관계에서 member를 불러온다면

    {
        "id": 1,
        "email": "sparta@sparta.com",
          ...
        "nickname": "스파르타",
        "bookStore": {
            "id": 1,
            "name": "스파르타 서울",
            "address": "서울시 강남구"
        }
    },

member는 Many입장이므로 one에 해당하는 bookStore를 불러온다.

Dto 사용하기

public class BookDto {

    @Getter
    @ToString
    public static class Response {
        private Long id;
        private String title;
        private String author;
        private Integer price;
        private Integer stock;
        private BookStoreDto bookStore;

        public Response(Book entity) {
            this.id = entity.getId();
            this.title = entity.getTitle();
            this.author = entity.getAuthor();
            this.price = entity.getPrice();
            this.stock = entity.getStock();
            this.bookStore = new BookStoreDto(entity.getBookStore());
        }
    }

    @Getter
    @ToString
    private static class BookStoreDto {
        private Long id;
        private String name;
        private String address;
        public BookStoreDto(BookStore entity){
            this.id = entity.getId();
            this.name = entity.getName();
            this.address = entity.getAddress();
        }
    }
}

Dto를 사용하여 순환되는 항목을 할당하지 않음으로 해결할 수 있다.
Book에서 BookStore를 참조할 때 BookStore의 private List<Book> bookList 항목이 순환을 발생시킨다.
Book을 기준으로 본다면 BookStore의 bookList를 볼 필요가 없으므로 Dto를 통해 걸러주고 걸러준 BookStoreDto를 Book에 담으면 된다.

    public List<BookDto.Response> findAllBook() {
        List<Book> all = bookRepository.findAll();
        return all.stream().map(BookDto.Response::new).collect(Collectors.toList());
    }

다음과 같이 Book을 BookDto.Response로 변환하면 순환 참조를 피할 수 있다.