reviewList
를 반환하기 위해 Entity를 JSON으로 직렬화 후 반환하는데서 순환참조 라는 문제가 생겼다.
❓ 순환참조란 ❓
: JPA에서 양방향으로 연결된 엔티티를 JSON 형태로 직렬화하는 과정에서, 서로의 정보를 계속 순환하며 참조하여 StackOverflowError를 발생시키는 현상
1.A Entity
(이하A
)와B Entity
(이하B
)가 양방향으로 연결된 상태
2.A
를 JSON으로 직렬화하기 위해A
가 참조하고 있는B
를 조회
3.B
를 조회하는 과정에서B
가 참조하고 있는A
를 조회
4.A
를 조회하는 과정에서A
가 참조하고 있는B
를 조회
5. 무한반복... ☠️
먼저 Entity를 보면, Review
와 Note
및 Tag
는 다대일 관계로 연결되어있다. 아직 Tag
와는 단방향 연결만 해놨기 때문에, Review
와 Note
의 관계만 놓고 보도록 하겠다.
/* Review Entity */
public class Review {
// ...생략...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "note_id")
private Note note;
}
/* Note Entity */
public class Note {
// ...생략...
@OneToMany(mappedBy = "note", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private final List<Review> reviews = new ArrayList<>();
}
이전에도 잠시 테스트 용으로 Review
Entity 하나를 반환하는 코드를 짜다가 순환참조 오류가 났던 적이 있는데, 이때는 오류만 안나면 됐던터라 N:1 매핑이 되어있는 필드에 @JsonIgnore
을 붙여서 간단하게 해결했었다.
public class Review {
// ...생략...
@JsonIgnore // <- 이런식으로!
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "note_id")
private Note note;
}
이후로 딱히 Entity를 반환할 일이 없어서 대충 넘어가고 있었는데, Review
리스트를 반환해야 하는데서 발목을 잡혔다.
반환하는거야 별 문제가 없었지만 문제는 반환값이
[
{
"id": 1,
"body": "단어책 1~15p",
"isCompleted": true
},
{
"id": 2,
"body": "단어책 16~20p",
"isCompleted": true
},
...
]
이런식으로 note
와 tag
만 쏙 빼고 나온 것이었다. 🤯😧
찾아보니 @JsonIgnore
는 직렬화할 때 해당 필드를 아예 무시하는거라 Review
Entity를 JSON으로 직렬화할 때 note
와 tag
가 아예 무시되어서 안나온거였다..
그래서 어노테이션으로 대충 해결할 수 없을까 싶어서 이것저것 찾다보니 @JsonIgnore
은 별로 추천하는 방법이 아니라고 한다.
대신 @JsonIgnore
의 대체 방법이자 순환참조를 해결하는 방법으로 가장 많이 추천되는 것이 바로 "DTO를 반환" 하는 것이었다. 또한 Entity를 직접 반환하는 것 자체도 썩 좋은 방법이 아니라고 한다.
그래서 Review
Entity와 거의 동일한 형태의 DTO를 아래와 같이 작성했다.
/* ReviewDTO */
public static class ReviewDTO {
private Long id;
private String body;
private Boolean isCompleted;
private Note note;
private Tag tag;
// Entity => DTO
public ReviewDTO(Review review) {
this.id = review.getId();
this.body = review.getBody();
this.isCompleted = review.getIsCompleted();
this.note = review.getNote();
this.tag = review.getTag();
}
}
/* service */
public List<ReviewDTO> reviewList() {
List<Review> reviewList = reviewRepository.findAll();
List<ReviewDTO> reviewDTOList = reviewList.stream()
.map(o->new ReviewDTO(o))
.collect(Collectors.toList());
return reviewDTOList;
}
아 근데~~!! reviewList
가 reviewDTOList
로 변환되는거까진 잘됐는데 return할때 계속!! 계속!!!! 순환참조 오류가 났다. 🤬🤬🤬 터미널에 Hibernate 찍히는걸 보니까 앞에서 내가 짠대로 쿼리 돌아갈때는 잘만 돌아가놓고 return할때 쿼리가 한번 더 찍히는게 도무지 이해가 안가서 화가 무진장 났는데 머리를 한번 식히고 생각해보니..... note
와 tag
부분이 매우엄청겁나많이 수상해보였다.
한번.. 생각을 해보자? 나야?
ReviewDTO
를 JSON으로 직렬화할건데,ReviewDTO
의 필드인 Note
도 JSON으로 직렬화해서 보여줘야겠지.Note
를 보면 얘는 Review
를 참조하고 있다.Review
를 또 봐.Review
는 또 Note
를 참조하고 있네?Note
를 또 봐.뭐야 이게!! 바보다 바보 🫠
그래서 ReviewDTO
를 직렬화할때 Note
를 직렬화할 포맷을 따로 작성했다. (Tag
도 마찬가지) 그래서 완성한 코드는 아래와 같다.
/* ReviewDTO */
@Getter
@Setter
public static class ReviewDTO {
/* ReviewDTO의 필드들 */
private Long id;
private String body;
private Boolean isCompleted;
private ReviewNoteDTO note;
private ReviewTagDTO tag;
/* Review => ReviewDTO */
public ReviewDTO(Review review) {
this.id = review.getId();
this.body = review.getBody();
this.isCompleted = review.getIsCompleted();
this.note = new ReviewNoteDTO(review.getNote());
this.tag = new ReviewTagDTO(review.getTag());
}
/* Review => ReviewDTO 할때 참조할 NoteDTO */
@Getter
public static class ReviewNoteDTO {
private Long id;
/* Note => NoteDTO */
public ReviewNoteDTO(Note note) {
this.id = note.getId();
}
}
/* Review => ReviewDTO 할때 참조할 TagDTO */
@Getter
public static class ReviewTagDTO {
private Long id;
private String name;
/* Tag => TagDTO */
public ReviewTagDTO(Tag tag) {
this.id = tag.getId();
this.name = tag.getName();
}
}
}
service 코드는 앞과 동일하다. 이렇게 반환할 정보를 깔끔하게 담은 DTO
를 활용하면!
이렇게 오류 없이 잘 나온다! 🥳🥳
➕ 2023.06.17 추가
한참 뒤에 덧붙이자면.. 어차피 필드도 많지 않으니 이렇게 쓰는게 다른데서도 안꼬이고 편하더라..public class ReviewResponseDTO { private Long noteId; private Long tagId; private String tagName; ... public ReviewResponseDTO(Review review) { this.noteId = review.getNote().getId(); this.tagId = review.getTag().getId(); this.tagName = review.getTag().getName(); ... } }