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로 변환하면 순환 참조를 피할 수 있다.
'Spring' 카테고리의 다른 글
Json 직렬/역직렬, @RequestBody, @ModelAttribute (0) | 2023.08.04 |
---|---|
Spring boot DI 주입 방식 (필드/수정자/생성자) (0) | 2023.07.22 |
스프링) Spring boot dto/lombok/validation (23-06-30) (0) | 2023.07.13 |
스프링) Spring boot ExceptionHandler/ Optional (23-06-28) (0) | 2023.07.13 |
스프링) Spring boot Request data (23-06-26) (0) | 2023.07.13 |