[JPA] 양방향 무한 참조

양방향 무한 참조

JPA를 이용할 때 발생할 수 있는 무한 참조를 알아보고 해결해보자.

Post, Reply

게시물과 댓글을 위한 Entity가 있다고 해보자.

package com.vividswan.blog.model;

import java.sql.Timestamp;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;

import org.hibernate.annotations.CreationTimestamp;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;


@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Board {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment
	private int id;
	
	@Column(nullable = false, length = 100)
	private String title;
	
	@Lob
	private String content;
	
	private int count; // 조회수
	
	@OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade =  CascadeType.REMOVE)
	@OrderBy("id desc")
	private List<Reply> replys;
	
	@ManyToOne // Many = Board, One = User
	@JoinColumn(name = "userId")
	private User user;
	
	@CreationTimestamp
	private Timestamp createDate;
}
package com.vividswan.blog.model;

import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import org.hibernate.annotations.CreationTimestamp;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Reply {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	
	@Column(nullable = false, length = 200)
	private String content;
	
	@ManyToOne
	@JoinColumn(name = "boardId")
	private Board board;
	
	@ManyToOne
	@JoinColumn(name = "userId")
	private User user;
	
	@CreationTimestamp
	private Timestamp createDate;
	
	public Reply(User user, Board board, String content) {
		this.user = user;
		this.board = board;
		this.content = content;
	}
}

Board Model에서 Reply를 부르는 부분을 살펴보자.

	@OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade =  CascadeType.REMOVE)
	@OrderBy("id desc")
	private List<Reply> replys;

댓글을 EAGER 방법으로 가져오고 OneToMany이기 때문에 데이터베이스에서 따로 FK로 저장되진 않는다.
하지만 JPA는 서버에서 클라이언트로 게시판 정보를 보내줄 때 댓글 정보도 같이 보내주게 된다.

Reply Model에서 Reply를 부르는 부분을 살펴보자.

	@ManyToOne
	@JoinColumn(name = "boardId")
	private Board board;

ManyToOne 임으로 Reply의 데이터베이스에서 Board에 대한 FK를 “boardId”로 갖고 있다.
이 또한 JPA가 서버에서 클라이언트로 댓글 정보를 보내줄 때 게시판 정보도 같이 보내주게 된다.

무한 참초 발생

package com.vividswan.blog.test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.vividswan.blog.model.Board;
import com.vividswan.blog.repository.BoardRepository;

@RestController
public class InfiniteReferenceTest {
	
	@Autowired BoardRepository boardRepository;
		
	@GetMapping("/test/inReference")
	public Board getPost() {
		return boardRepository.findById(1).get();
	}
}

위의 간단한 테스트로 게시판 정보를 클라이언트에게 보내줘보자.
이때, id=1인 게시판을 미리 생성하고, 그 게시판에 댓글도 미리 생성한 뒤 보내주면 무한 참조를 확인할 수 있다.

1

이런 상황이 발생한 이유는 위의 Board와 Reply의 코드에서 확인할 수 있다.
JPA에게 Board 정보를 들고 올 때 Reply 정보도 들고 와달라고 요청했다.
근데 Reply 정보를 들고 올 때 역시 Board 정보를 들고 와달라고 요청했다.
그렇다면 Board에 등록된 Reply를 들고 오면서 또다시 Board를 들고 오고 이렇게 서로를 계속 참조하는 것이다.
이러한 상황을 무한 참조라고 말한다.

해결 방법

해결 방법에는 여러 가지 방법이 있지만 여기선 JsonIgnoreProperties를 이용해보자.
Board Model 객체를 다음과 같이 수정한다.

@OneToMany(mappedBy = "board", fetch = FetchType.EAGER, cascade =  CascadeType.REMOVE)
	@JsonIgnoreProperties({"board"})
	@OrderBy("id desc")
	private List<Reply> replys;

@JsonIgnoreProperties({"board"})라는 어노테이션이 추가된 걸 확인할 수 있다.
이 어노테이션의 의미로는 board에 의해 호출된 reply에서는 board 자료를 가져오지 말라는 어노테이션이다.
더 생각해 볼 건, board에서 reply의 자료를 가져오는 경우에만 해당하므로, 만약 reply만 따로 불러온다면 reply의 FK로 Board의 자료를 가져올 것이다.

2

위와 같이 reply에서 다시 board의 자료를 가져오지 않기 때문에 무한 참조가 발생하지 않는다.