![[JPA-Error] 게시물 조회 순환참조 stackoverflow](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvR8LM%2FbtsDKQkezQe%2FWCVulsjkvsRxiCkFoIjOh1%2Fimg.png)
현재 상황
양방향 참조 관계에 있는 Entity를 조회하는 과정에서 StackOverFlow 예외가 발생하였다.
일단 양방향 관계에 있는 Member와 Board Entity이다.
Member Entity
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"loginId", "password", "username", "nickName", "email", "phone"})
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "member_id")
private Long id;
private String loginId;
private String password;
private String username;
private String nickName;
private String email;
private String phone;
private LocalDateTime createdDate;
@OneToMany(mappedBy = "member")
private List<Board> boards = new ArrayList<>();
}
Board Entity
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "board_id")
private Long id;
private String title; // 제목
private String content; // 내용
private long viewCnt; // 조회수
private long likeCnt; // 좋아요
private LocalDateTime createdDate; // 작성 일자
private LocalDateTime editDate; // 수정 일자
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
}
아래 사진을 보면 검색 기능이 구현되어 있다. 나는 AJAX를 통해 비동기 호출을 할 것이기 때문에 html 내부에 jQuery 코드를 작성했다.
AJAX 비동기 호출
<script>
function searchBoardList() {
let searchTitle = $("#searchText").val();
// 기존의 게시물을 지우는 부분 추가
$("tbody").empty();
$.ajax({
type: "POST",
url: "/boards/search",
data: JSON.stringify({condition: searchTitle}),
contentType: "application/json",
// Controller로부터 받은 List를 화면에 뿌림
success: function (response) {
for (var i = 0; i < response.length; i++) {
var board = response[i];
// 데이터를 이용하여 화면에 표시하는 코드 작성
// 예시: 테이블의 tbody에 행을 추가하는 방식으로 데이터를 표시
var row = "<tr>" +
"<td>" + board.boardId + "</td>" +
"<td>" + board.boardTitle + "</td>" +
"<td>" + board.boardConctent + "</td>" +
"<td>" + board.memberNickName + "</td>" +
"<td>" + board.createdDate + "</td>" +
"<td>" + board.boardViewCnt + "</td>" +
"<td>" + board.boardLikeCnt + "</td>" +
"<td><a href='/board/" + board.boardId + "' class='btn btn-primary'>자세히 보기</a></td>" +
"</tr>";
$("tbody").append(row);
}
},
error: function (error) {
}
});
}
</script>
검색창에 조회할 게시물을 검색하면 "/boards/search" 경로로 JSON 형태의 데이터가 전송된다. Controller를 보면 매개변수 부분에 @RequestBody 애너테이션을 사용하여 JSON -> 객체 변환과정을 거쳤다.
Controller의 요청을 Repository에서 처리한다. Board 엔티티와 Member 엔티티는 양방향 연관관계이고 성능 최적화를 위해 페치조인을 통해 데이터를 조회하였다.
그 결과 StackOverflow 예외가 발생하였다.
원인
Board와 Member 엔티티가 양방향 관계로 서로를 참조하고 있기 때문이다.
- Board를 조회하면서 연관관계에 있는 Member 조회
- ex. id가 3인 Board와 연관관계에 있는 id가 1인 Member 조회
- Member를 조회하면서 연관관계에 있는 Board 조회
- ex. id가 1인 Member와 연관관계에 있는 id가 3인 Board조회
이를 해결하기 위해서는 양방향 관계에 있는 Entity를 그대로 반환하지 않으면 된다.
해결
조회한 Entity를 그대로 반환하지 않고 DTO로 변환하여 반환한다.
DTO
화면에 보여주기 위해 필요한 데이터로만 구성된 DTO 생성한다.
@Data
public class ResponseSearchConditionDto {
private Long boardId;
private String boardTitle;
private String boardContent;
private String memberNickName;
private LocalDateTime createdDate;
private long boardViewCnt;
private long boardLikeCnt;
}
Controller
Entity -> DTO로 변환하여 반환한다.
@Controller
@RequiredArgsConstructor
@Slf4j
public class BoardController {
private final BoardService boardService;
/**
* 양방향 연관관계 그대로 반환X
@ResponseBody
@PostMapping("/boards/search")
public List<Board> getSearchedBoards(@RequestBody BoardSearchConditionVO vo) {
return boardService.getPostsBySearch(vo);
}
*/
@ResponseBody
@PostMapping("/boards/search")
public List<ResponseSearchConditionDto> getSearchedBoards(@RequestBody BoardSearchConditionVO vo) {
List<Board> boards = boardService.getPostsBySearch(vo);
List<ResponseSearchConditionDto> result = new ArrayList<>();
for (Board board : boards) {
result.add(convertToResponseSearchConditionDtoList(board));
}
return result;
}
private ResponseSearchConditionDto convertToResponseSearchConditionDtoList(Board board) {
ResponseSearchConditionDto searchBoard = new ResponseSearchConditionDto();
searchBoard.setBoardId(board.getId());
searchBoard.setBoardTitle(board.getTitle());
searchBoard.setBoardContent(board.getContent());
searchBoard.setMemberNickName(board.getMember().getNickName());
searchBoard.setCreatedDate(board.getCreatedDate());
searchBoard.setBoardViewCnt(board.getViewCnt());
searchBoard.setBoardLikeCnt(board.getLikeCnt());
return searchBoard;
}
}