RestAPI(7) 양방향 관계와 재귀

별빛사막·2024년 12월 24일

1. 오류의 원인

"org.springframework.http.converter.HttpMessageConversionException:
Typedefinition error: [simple type, class 
org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]\r\n\tat 
org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.wri
teInternal(AbstractJackson2HttpMessageConverter.java:492)\r\n\tat 
org.springframework.

예를 들어, Post 엔티티와 PostComment 엔티티가 OneToManyManyToOne 관계로 서로 연결되어 있다.

  • 예시:

    @Entity
    public class Post {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
        private List<PostComment> comments;
    }
    
    @Entity
    public class PostComment {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @ManyToOne
        @JoinColumn(name = "post_id")
        private Post post;
    }

1.1 양방향 관계와 재귀

Postcomments 리스트를 통해 여러 PostComment를 참조하고, PostCommentpost 필드를 통해 다시 Post를 참조한다.
즉, Post → PostComment → Post → PostComment → ...가 무한 반복되는 재귀 참조가 발생한다.

1.2 문제 발생 시점

Spring에서 Post 엔티티를 JSON으로 변환(Serialization)하려고 할 때, Jackson(혹은 기타 JSON 처리 라이브러리)이 PostPostComment 간의 무한 참조를 처리하지 못해 오류가 발생한다.

1.3 구체적인 오류 설명

오류 메시지에서 org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor가 언급되는 이유는 Hibernate의 지연 로딩(Lazy Loading) 때문이다.

Hibernate는 관계된 엔티티를 Proxy 객체로 관리하며, 직렬화 시 이 Proxy 객체를 Jackson이 올바르게 처리하지 못할 경우 에러가 발생한다.

  • 직렬화 : 객체(Object)를 바이트(byte) 형태로 변환하는 과정
    ex. JSON 직렬화는 자바 객체를 JSON 형식으로 변환하는 것을 의미

  • Jackson : 자바에서 객체를 JSON으로 변환하거나(JSON 직렬화), JSON 데이터를 객체로 변환(JSON 역직렬화)하는 데 사용되는 라이브러리


2. 양방향 재귀란?

양방향 재귀는 엔티티 A와 엔티티 B가 서로를 참조하면서 끝없이 참조가 반복되는 상황을 말한다.

2.1 직렬화 과정

  • Postcomments 필드를 직렬화하려고 하면, PostCommentpost 필드가 직렬화된다.
  • 다시 Postcomments 필드가 직렬화되면서 무한 루프가 발생한다.

2.2 결과

JSON 생성이 끝나지 않으므로 오류가 발생하거나, 매우 큰 JSON이 생성되어 성능 문제가 발생한다.


3. 해결 방법

3.1 @JsonIgnore 사용

한쪽 관계를 직렬화 대상에서 제외한다.

@Entity
public class PostComment {
    @ManyToOne
    @JoinColumn(name = "post_id")
    @JsonIgnore
    private Post post;
}

이 방법은 JSON 직렬화 시 PostCommentpost 필드를 무시하게 한다.


3.2 @JsonManagedReference@JsonBackReference 사용

Jackson의 @JsonManagedReference@JsonBackReference를 사용하여 직렬화 순서를 지정한다.

@Entity
public class Post {
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
    @JsonManagedReference
    private List<PostComment> comments;
}

@Entity
public class PostComment {
    @ManyToOne
    @JoinColumn(name = "post_id")
    @JsonBackReference
    private Post post;
}
  • @JsonManagedReference는 직렬화의 시작점 역할을 한다.
  • @JsonBackReference는 직렬화에서 제외되어 재귀를 방지한다.

3.3 DTO 사용

양방향 엔티티를 그대로 직렬화하지 않고, 필요한 데이터만 포함하는 DTO(Data Transfer Object)를 생성하여 반환한다.

public class PostDTO {
    private Long id;
    private List<CommentDTO> comments;
}

public class CommentDTO {
    private Long id;
    private String content;
}

3.4 Hibernate 초기화

Lazy Loading 문제를 해결하기 위해 Hibernate.initialize()를 사용하여 필요한 데이터를 미리 초기화한다.


3.5 clear()와 함께 사용

EntityManager.clear()를 사용하여 영속성 컨텍스트를 초기화한 뒤 데이터를 다시 조회한다.

@Transactional
public void updateAndClearContext() {
    // 엔티티 업데이트
    Post post = postRepository.findById(1L).orElseThrow();
    post.setTitle("Updated Title");

    // 영속성 컨텍스트 비우기
    entityManager.flush();
    entityManager.clear();

    // 변경된 데이터 다시 조회
    Post updatedPost = postRepository.findById(1L).orElseThrow();
    System.out.println(updatedPost.getTitle()); // "Updated Title"
}

4. 요약

양방향 재귀는 엔티티 간의 상호 참조가 무한 반복되는 상황으로, JSON 직렬화 중에 발생할 수 있다.
이를 해결하려면 @JsonIgnore@JsonManagedReference/@JsonBackReference를 사용하거나 DTO로 변환하여 반환하는 방법을 사용한다.
또한, 필요에 따라 Hibernate의 초기화 방법이나 EntityManager.clear() 등을 활용할 수 있다.

profile
조금씩 매일 성장하자

0개의 댓글