ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA 일대일 양방향 매핑에서 발생한 무한 순환 참조 문제와 해결
    프로젝트, 트러블슈팅 2024. 12. 23. 15:51

    발생한 문제

    게시글(Post)과 게시글 내용(PostContent)을 분리해서 관리하기 위해 일대일 양방향 매핑을 사용했다. 그런데 이 과정에서 StackOverflowError가 발생했다.

     

    문제가 된 코드는 다음과 같다:

    @Entity
    public class PostContent {
        @OneToOne
        private Post post;
        
        public void setPost(Post post) {
            this.post = post;
            post.setPostContent(this);  // 여기서 Post의 setPostContent 호출
        }
    }
    
    @Entity
    public class Post {
        @OneToOne(mappedBy = "post")
        private PostContent postContent;
        
        public void setPostContent(PostContent postContent) {
            this.postContent = postContent;
            postContent.setPost(this);  // 여기서 다시 PostContent의 setPost 호출
        }
    }

     

    무한 참조 순환은 왜 일어났을까?

    위 코드에서 post.setPostContent()를 호출하면:

    1. Post의 setPostContent() 실행
      • postContent 설정
      • PostContent의 setPost() 호출
    2. PostContent의 setPost() 실행
      • post 설정
      • 다시 Post의 setPostContent() 호출
    3. 1번으로 돌아가서 무한 반복...

    결국 이런 순환 호출로 인해 스택이 계속 쌓이다가 StackOverflowError가 발생한다.

     

    실제 에러메세지는 이렇다. 

    java.lang.StackOverflowError
        at se.sowl.postHubingdomain.post.domain.Post.setPostContent()
        at se.sowl.postHubingdomain.post.domain.PostContent.setPost()
        at se.sowl.postHubingdomain.post.domain.Post.setPostContent()
        at se.sowl.postHubingdomain.post.domain.PostContent.setPost()
        ... (계속 반복)

    해결 방법

    나는 이 문제를 해결하기 위해 각 setter에 조건문을 추가했다:

    @Entity
    public class PostContent {
       @OneToOne
       private Post post;
       
       public void setPost(Post post) {
           // 현재 PostContent 객체의 post 필드를 매개변수로 받은 post로 설정
           this.post = post;
           
           // 양방향 관계 설정을 위한 체크:
           // 1. post가 null이 아니고 (NPE 방지)
           // 2. 매개변수로 받은 post의 postContent가 현재 객체가 아닐 때만 
           // (이미 연결되어 있지 않은 경우에만 설정)
           if (post != null && post.getPostContent() != this) {
               // post에도 현재 PostContent 객체를 설정
               post.setPostContent(this);
           }
       }
    }
    
    @Entity
    public class Post {
       @OneToOne(mappedBy = "post")
       private PostContent postContent;
       
       public void setPostContent(PostContent postContent) {
           // 현재 Post 객체의 postContent 필드를 매개변수로 받은 postContent로 설정
           this.postContent = postContent;
           
           // 양방향 관계 설정을 위한 체크:
           // 1. postContent가 null이 아니고 (NPE 방지)
           // 2. 매개변수로 받은 postContent의 post가 현재 객체가 아닐 때만 
           // (이미 연결되어 있지 않은 경우에만 설정)
           if (postContent != null && postContent.getPost() != this) {
               // postContent에도 현재 Post 객체를 설정
               postContent.setPost(this);
           }
       }
    }

     

    여기서:

    1. post != null: null 체크로 NPE( NullPointerException )를 방지한다.
    2. post.getPostContent() != this: post의 content가 현재 content가 아닌 경우에만 새로 설정한다.

    이렇게 하면 양방향 관계 설정이 한 번만 이루어지고 무한 루프가 발생하지 않는다.


    배운 점

    이번 무한 순환 참조 문제를 해결하면서 몇 가지 중요한 점을 배웠다.

     

    첫째, 양방향 관계 설정은 신중하게 해야 한다. Post와 PostContent처럼 서로를 참조하는 관계에서는 한쪽이 수정되면 다른 쪽도 함께 수정되어야 하는데, 이 과정에서 무한 루프같은 문제가 발생할 수 있다. 그래서 양방향 관계를 맺을 때는 어떤 상황이 발생할 수 있는지 미리 잘 고려해야 한다.

     

    둘째, 무한 순환 참조 문제는 조건문으로 해결할 수 있다. 나는 처음에 단순히 setter만 호출했다가 무한 루프에 빠졌다. 하지만 현재 객체가 이미 상대방과 연결되어 있는지 확인하는 조건문을 추가함으로써 이 문제를 해결할 수 있었다. post.getPostContent() != this와 같은 조건 체크로 불필요한 순환 호출을 방지할 수 있다는 걸 알게 됐다.

     

    셋째, null 체크의 중요성을 깨달았다. Java에서 null인 객체의 메서드를 호출하면 NullPointerException이 발생한다. 그래서 post != null처럼 null 체크를 꼭 해줘야 안전한 코드가 된다는 것을 배웠다.

     

     

Designed by Tistory.