실무에서 JPA를 사용하던 중, A Entity와 C Entity를 M:N 관계로 엮어야 하는 일이 생겼다.
인프런으로 JPA 공부할 때 영한 센세가 M:N 관계는 중간 테이블 만들어서 분리하는 것이 좋다고 하셨는데, 실무에서 활용해 볼 기회가 되었다!
중간 역할을 해줄 B Entity를 생성했고 A, B, C를 아래와 같이 연결했다.
@Entity
@Data
public class A {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT COMMENT 'A 일련번호'")
private Long aId;
@OneToMany(mappedBy = "a", fetch = FetchType.LAZY)
private Set<B> b = new HashSet<>();
}
@Entity
@Data
public class B {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT COMMENT 'B 일련번호'")
private Long bId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "a_id")
private A a;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "c_id")
private C c;
}
@Entity
@Data
public class C {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT COMMENT 'C 일련번호'")
private Long cId;
@OneToMany(mappedBy = "c", fetch = FetchType.LAZY)
private Set<B> b = new HashSet<>();
}
영한 센세 말씀대로 중간 테이블도 만들고, 지연 로딩으로도 설정했으니 잘 작동하겠지 당연히!?
하지만 스프링 부트 실행하니 JPA 쿼리가 끝없이 콘솔에 찍히고 있었고
결국 StackOverFlowException과 마주치게 되었다.
로그를 보니 equals(), hashCode(), toString() 등이 보였고, Entity는 A, B, C 번갈아가며 연속으로 찍혀있었다.
이게 뭐람...
검색해보니 JPA에서 양방향으로 연결된 Entity를 그대로 조회하는 경우,
서로의 정보를 순환하면서 조회하다가 StackOverFlow가 발생하게 된다고 한다.
이를 순환 참조(Circular reference)라고 한다.
순환 참조란, 참조하는 대상이 서로 물려 있어서 참조할 수 없게 되는 현상을 말한다.
Spring Boot는 REST API 구현 시, @RequestBody나 @ResponseBody가 붙은 객체를 가져와
Jackson의 ObjectMapper를 사용해 직렬화, 역직렬화 한다.
Jackson은 Entity의 getter를 호출하고,
직렬화를 이용해 JSON 형태로 객체 변환 후 Entity를 View로 전달한다.
이 getter를 호출하는 과정에서 순환 참조가 발생해 StackOverFlowException과 마주하게 된다.
public class User {
public int id;
public String name;
@JsonIgnore
public List<Item> userItems;
}
{
"id" : 1,
"name" : kkk
}
@JsonManagedReference
@JsonBackReference
@Entity
@Data
public class A {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT COMMENT 'A 일련번호'")
private Long aId;
@JsonManagedReference
@OneToMany(mappedBy = "a", fetch = FetchType.LAZY)
private Set<B> b = new HashSet<>();
}
@Entity
@Data
public class B {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT COMMENT 'B 일련번호'")
private Long bId;
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "a_id")
private A a;
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "c_id")
private C c;
}
@Entity
@Data
public class C {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(columnDefinition = "BIGINT COMMENT 'C 일련번호'")
private Long cId;
@JsonManagedReference
@OneToMany(mappedBy = "c", fetch = FetchType.LAZY)
private Set<B> b = new HashSet<>();
}
@Entity
@JsonIgnoreProperties({"children"})
public class Parent {
@Id
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> children;
}
@Entity
public class Child {
@Id
private Long id;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
실무에서 DTO를 사용하지 않아
어쩔 수 없이 @JsonManagedReference와 @JsonBackReference를 적용했지만
가장 좋은 방법은 DTO로 return 하는 것이다!