@Entity
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private String content;
(생략)
}
@Entity
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "article_id")
private Article article;
@Column
private String nickname;
@Column
private String body;
(생략)
}
SqlExceptionHelper : Referential integrity constraint violation

외래키(foreign key)로 연결되어 있어서 댓글이 있는 글을 삭제하려고 하면 제약 조건 때문에 삭제할 때 에러가 발생.참조 무결성@OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE) // cascade 대신 orphanRemoval = true 또한 가능.
@ToString.Exclude // 순환 참조 방지
private List<Comment> comments = new ArrayList<>();
cascade
CascadeType.REMOVE로 설정할 시 부모 엔터티가 삭제될 때 연결 되어 있는 자식 엔터티도 같이 삭제 되는 설정.orphanRemoval
true로 설정할 시 부모, 자식 관계가 끊어져서, 즉 고아(Orphan)가 될때 해당 데이터를 자동으로 삭제함.이 문제에서 중요하다고 생각 되는 부분이 바로 mappedBy
기존 코드를 보면 comment에서 @ManyToOne으로 article을 단방향 참조를 하고 있음.@OneToMany를 준다고 해도 양방향 관계가 되진 못함.mappedBy로 연관관계의 주인을 지정해줌으로써 양방향 관계가 됨.
- 단방향에서는 문제가 되지 않지만 양방향에서 문제가 발생함.
- 데이터베이스는
외래키(foreign key)하나로 두 테이블이 연관관계를 맺지만, 객체의 양방향 관계는 A에서 B를 참조, B에서 A를 참조, 즉 참조가 2곳에서 이루어짐.Ex)comment,article을 엔티티를 양방향 연관 관계를 갖도록 매핑했을 때
comment가 새로운article을 들어가도록 변경한다고 가정하면
comment에서article을 수정할지,article을List<comment>를 바꿔야할 지 혼란이 옴.
하지만, 데이터베이스에서는 외래키 값 article_id을 사용하면 됨.
- 객체에서는 두 방식 다 옳지만, 데이터베이스를 객체지향적으로 사용하는
JPA입장에서는 혼란스러움.
이러한 문제를 해결하기 위해 양방향 매핑에서 객체의 두 관계 중 하나를 연관관계의 주인으로 지정해야함.
StackOverflowError가 발생.2024-12-29T17:05:59.713+09:00 ERROR 4796 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed: java.lang.StackOverflowError] with root cause
java.lang.StackOverflowError: null
at java.base/java.lang.Long.toString(Long.java:490) ~[na:na]
at java.base/java.lang.Long.toString(Long.java:1416) ~[na:na]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.project.articlecomment.entity.Comment.toString(Comment.java:13) ~[classes/:na]
at java.base/java.lang.String.valueOf(String.java:4220) ~[na:na]
at java.base/java.lang.StringBuilder.append(StringBuilder.java:173) ~[na:na]
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:457) ~[na:na]
at org.hibernate.collection.spi.PersistentBag.toString(PersistentBag.java:611) ~[hibernate-core-6.2.2.Final.jar:6.2.2.Final]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.project.articlecomment.entity.Article.toString(Article.java:10) ~[classes/:na]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.project.articlecomment.entity.Comment.toString(Comment.java:13) ~[classes/:na]
at java.base/java.lang.String.valueOf(String.java:4220) ~[na:na]
at java.base/java.lang.StringBuilder.append(StringBuilder.java:173) ~[na:na]
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:457) ~[na:na]
at org.hibernate.collection.spi.PersistentBag.toString(PersistentBag.java:611) ~[hibernate-core-6.2.2.Final.jar:6.2.2.Final]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.project.articlecomment.entity.Article.toString(Article.java:10) ~[classes/:na]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.project.articlecomment.entity.Comment.toString(Comment.java:13) ~[classes/:na]
at java.base/java.lang.String.valueOf(String.java:4220) ~[na:na]
at java.base/java.lang.StringBuilder.append(StringBuilder.java:173) ~[na:na]
at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:457) ~[na:na]
at org.hibernate.collection.spi.PersistentBag.toString(PersistentBag.java:611) ~[hibernate-core-6.2.2.Final.jar:6.2.2.Final]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.project.articlecomment.entity.Article.toString(Article.java:10) ~[classes/:na]
at java.base/java.lang.StringConcatHelper.stringOf(StringConcatHelper.java:453) ~[na:na]
at com.project.articlecomment.entity.Comment.toString(Comment.java:13) ~[classes/:na]
at com.project.articlecomment.entity.Article.toString(Article.java:10) ~[classes/:na]
at com.project.articlecomment.entity.Comment.toString(Comment.java:13) ~[classes/:na]
무한 재귀(Infinite recursion)라 함.@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "article")
private List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "article_id")
private Article article;
}
Article의 toString()이 호출됨 → comments 리스트 출력.
comments 리스트의 각 Comment의 toString() 호출 → 다시 Article 참조 출력.
Article의 toString() 호출 → 다시 comments 출력.
무한 반복...
@ToString.Exclude@ToString에노테이션을 삭제하는 것도 해결 방법 중 하나임.@Entity
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE) // CascadeType.REMOVE or orphanRemoval = true 사용하면됨.
@ToString.Exclude // 순환 참조 방지
private List<Comment> comments = new ArrayList<>();
...
}