게시글 및 게시글 태그 관리 프로젝트를 진행하던 중 순환 참조 문제를 마주하게 되었고 이를 해결했던 글을 적어 보고자 합니다.
// Board 클래스
public class Board {
@Id
@Column(name = "article_id")
private String articleId;
@Column(name = "writer")
private String writer;
@Column(name = "board_title")
private String boardTitle;
// 생략
...
@Builder.Default
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<BoardTag> boardTags = new ArrayList<>();
}
// BoardTag 클래스
public class BoardTag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "board_tag_id", nullable = false)
private Long boardTagId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "article_id", nullable = false)
private Board board;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "tag_id", nullable = false)
private TagObj tagObj;
}
// TagObj 클래스
// ... 생략
Service Layer
public List<Board> findAllBoard() {
return boardRepository.findAllBoard();
}
Repository Layer
public List<Board> findAllBoard() {
return em.createQuery( "SELECT b FROM Board b LEFT JOIN FETCH b.boardTags", Board.class)
.getResultList();
}
stackoverflow
가 발생하게 됩니다.Spring Boot가 JSON으로 객체를 직렬화할 때, 해당 객체가 다른 객체를 참조하고 있고 그 참조된 객체 또한 다시 해당객체를 참고하고 있어 무한히 객체를 참조하는 과정에서
StackOverflowError
가 발생하게 됩니다.
※ Spring Boot가
REST API
메시지를 구현 할 때, JSON 형태로 메시지를 전달 하게 됩니다. 이때 Object를 JSON으로 변환하기 위하여 Jackson 라이브러리를 이용하는데, Jackson 라이브러리에서 Entity의getter
를 호출하고, 직렬화를 이용해 JSON으로 변환하게 전송가능한 형태로 바꾸어 줍니다. 이를마샬링(Marshalling)
이라고 합니다. 마샬링 과정 중 엔티티의getter
를 호출하는 과정에서 연관된 엔티티를 계속해서 불러오다 보면 순환참조가 발생하여StackOverflowError
가 발생하게 됩니다.
FetchType.LAZY
로 사용하고 있고,@JsonIgnore
어노테이션 사용// Board 클래스
public class Board {
// 생략
...
@JsonIgnore // 순환참조 방지
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private List<BoardTag> boardTags = new ArrayList<>();
}
@JsonManagedReference
와 @JsonBackReference
어노테이션 사용하기@JsonManagedReference
와 @JsonBackReference
어노테이션을 사용하여 엔티티 간의 양방향 참조를 지정합니다. @JsonManagedReference
를 사용하여 부모 엔티티에서 자식 엔티티를 참조하고, @JsonBackReference
를 사용하여 자식 엔티티에서 부모 엔티티를 참조합니다. @JsonManagedReference
는 역참조를 관리하는 객체에서 사용하며, @JsonBackReference
는 역참조를 하지 않는 객체에서 사용합니다.// Board 클래스
public class Board {
// 생략
...
@OneToMany(mappedBy = "board", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonManagedReference
private List<BoardTag> boardTags = new ArrayList<>();
}
// BoardTag 클래스
public class BoardTag {
// 생략
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "article_id", nullable = false)
@JsonBackReference
private Board board;
}
양방향 매핑
입니다만, 이러한 원인이 되는 부분은 엔티티 자체를 response로 return
한데 있습니다. 따라서, 엔티티를 return 하지말고, DTO 객체를 만들어서 필요한 데이터만 클라이언트로 return 하게되면 해당 문제를 방지할 수 있습니다./**
* 게시글 목록을 리턴할 Response 클래스
* Entity 클래스를 생성자 파라미터로 받아 DTO 로 변환하여 응답합니다.
* Board 엔티티를 그대로 반환하는 경우 Board 와 BoardTag 의 무한참조를 방지하기 위해
* 별도의 responseDto 객체를 생성합니다.
*/
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BoardResponseDto {
private String articleId;
private String boardTitle;
private String writer;
private LocalDateTime regDate;
private List<BoardTagResponseDto> boardTags;
/* Entity -> Dto */
public BoardResponseDto(Board board) {
this.articleId = board.getArticleId();
this.boardTitle = board.getBoardTitle();
this.writer = board.getWriter();
this.regDate = board.getRegDate();
this.boardTags = board.getBoardTags().stream().map(BoardTagResponseDto::new).collect(
Collectors.toList());
}
/**
* 리스트 형태로 전달
*/
public static List<BoardResponseDto> from(List<Board> boards) {
return boards.stream()
.map(BoardResponseDto::new)
.collect(Collectors.toList());
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class BoardTagResponseDto {
private Long boardTagId;
private TagObjResponseDto tagObjResponseDto;
/* Entity -> DTO */
public BoardTagResponseDto(BoardTag boardTag) {
this.boardTagId = boardTag.getBoardTagId();
this.tagObjResponseDto = new TagObjResponseDto(boardTag.getTagObj());
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TagObjResponseDto {
private Long tagId;
private String tagName;
private String parentTagId;
private String childTagId;
public TagObjResponseDto(TagObj tagObj) {
this.tagId = tagObj.getTagId();
this.tagName = tagObj.getTagName();
this.parentTagId = tagObj.getParentTagId();
this.childTagId = tagObj.getChildTagId();
}
}
// Controller Layer
@GetMapping("/api/boards/")
public ResponseEntity<List<BoardResponseDto>> findAllBoard() {
return ResponseEntity.ok().body(boardService.findAllBoard());
}
// Service Layer
public List<BoardResponseDto> findAllBoard() {
List<Board> allBoard = boardRepository.findAllBoard();
return BoardResponseDto.from(allBoard);
}