[Spring, Java] Error - 댓글이 있는 글 삭제하기. (참조 무결성 제약 조건 위반, 무한 재귀(Infinite recursion)로 인해StackOverflowError)

하쮸·2024년 12월 9일

Error, Why, What, How

목록 보기
6/68

1. 문제점.

@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

  • 글(article)과 댓글(comment)이 외래키(foreign key)로 연결되어 있어서 댓글이 있는 글을 삭제하려고 하면 제약 조건 때문에 삭제할 때 에러가 발생.
    • 1번글에 댓글id가 1, 2, 3인 댓글이 있을 경우에 댓글을 먼저 삭제하고 글을 삭제해야 가능했음.
    • 참조 무결성

2. 해결.

@OneToMany(mappedBy = "article", cascade = CascadeType.REMOVE)	// cascade 대신 orphanRemoval = true 또한 가능.
@ToString.Exclude // 순환 참조 방지
private List<Comment> comments = new ArrayList<>();
  • cascade

    • CascadeType.REMOVE로 설정할 시 부모 엔터티가 삭제될 때 연결 되어 있는 자식 엔터티도 같이 삭제 되는 설정.
    • 오라클에서 사용하는 cascade를 떠올리면 됨.
  • orphanRemoval

    • true로 설정할 시 부모, 자식 관계가 끊어져서, 즉 고아(Orphan)가 될때 해당 데이터를 자동으로 삭제함.
  • 이 문제에서 중요하다고 생각 되는 부분이 바로 mappedBy

    • 기존 코드를 보면 comment에서 @ManyToOne으로 article을 단방향 참조를 하고 있음.
    • article에 @OneToMany를 준다고 해도 양방향 관계가 되진 못함.
    • 즉, comment에서 n : 1로 참조하는 단방향, article에서 1 : n으로 참조하는 단방향.
      이런 상태가 됨.
      • 여기서 mappedBy로 연관관계의 주인을 지정해줌으로써 양방향 관계가 됨.
  • 단방향에서는 문제가 되지 않지만 양방향에서 문제가 발생함.
    • 데이터베이스는 외래키(foreign key) 하나로 두 테이블이 연관관계를 맺지만, 객체의 양방향 관계는 A에서 B를 참조, B에서 A를 참조, 즉 참조가 2곳에서 이루어짐.
    • Ex) comment, article을 엔티티를 양방향 연관 관계를 갖도록 매핑했을 때
      comment가 새로운 article을 들어가도록 변경한다고 가정하면
      comment에서 article을 수정할지, articleList<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)라 함.
    • 무한 재귀(Infinite recursion) 문제는 객체 간의 관계를 정의할 때 A 객체가 B 객체를 참조하고, B 객체도 다시 A 객체를 참조함으로써 발생하는 문제.
    • 무한 루프 등의 문제가 발생할 수 있음.
      • 즉, Article과 Comment 엔티티 간의 양방향 연관 관계에서 발생한 무한 순환 참조.
      • toString() 메서드를 통해 두 객체가 서로를 참조하면서 무한히 계속 호출해서 발생.
@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이 Comment를 리스트로 포함하고, Comment가 다시 Article을 참조함.
    • 만약 toString()을 호출하거나 JSON으로 직렬화하려고 하면 아래와 같은 순환이 발생.

Article의 toString()이 호출됨 → comments 리스트 출력.
comments 리스트의 각 Comment의 toString() 호출 → 다시 Article 참조 출력.
Article의 toString() 호출 → 다시 comments 출력.
무한 반복...

  • @ToString.Exclude
    • @ToString.Exclude를 사용해서 자동으로 생성되는 toString() 메서드를 특정 필드만 제외시킴.
      • 즉, 순환 참조가 발생할 가능성이 있는 필드를 제외.
  • @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<>();

    			...
}

3. 참고.

profile
Every cloud has a silver lining.

0개의 댓글