[Trouble Shooting] 무한 순환참조 이슈

2023. 1. 18. 14:48Project/시네마그램

토이 프로젝트를 하면서 순환참조 문제가 종종 발생하였습니다. 처음에는 당황했지만 차근차근 코드를 따라가 보니 해결할 수 있었습니다. 

 

이번 포스팅에서는 크게 두 Case로 나눠서 어떻게 오류를 해결했는지 과정을 작성해 보겠습니다. 

 

Case 1. Jackson 라이브러리를 사용하여 엔티티를 JSON으로 변환 시, 무한참조 이슈 

관련 모델

  • User
  • Image

 

Image

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@NoArgsConstructor
@Getter
@Entity
public class Image extends BaseTimeEntity {
    
	...
    @JoinColumn(name = "userId")
    @ManyToOne(fetch = FetchType.LAZY) // 에러
    private User user;
   	...
}

 

콘솔 에러

No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer

 

 

원인 파악

JSON 변환 과정에서 Hibernate 프록시 객체를 직렬화하는 데 필요한 직렬화 도구(serializer)가 없음을 나타냅니다. Hibernate는 지연 로딩을 위해 엔티티를 프록시 객체로 감싸는데, 이 프록시 객체는 직렬화 도구 없이는 JSON으로 변환될 수 없습니다. 이 문제는 종종 Jackson 라이브러리를 사용하여 엔티티를 JSON으로 변환하려고 할 때 발생합니다.

jackson라이브러리란?
자바 오브젝트를 json으로 파싱 해주는 역할을 합니다.

 

 

해결하는 여러 방법들

  1. 1번 : application 파일에 spring.jackson.serialization.fail-on-empty-beans=false 설정해 주기
    1. 직렬화 실패 시 오류를 무시하도록 하여 문제를 회피합니다. 하지만 이는 오류를 숨기는 것일 뿐 근본적인 해결책은 아닙니다.
  2. 2번 : 오류가 나는 필드의 LAZY 설정을 EAGER로 바꿔주기
    1. 이 방법은 필요한 데이터를 미리 로딩하지만, N+1 문제를 유발할 수 있어 주의가 필요합니다.
  3. 3번: 오류가 나는 필드에 @JsonIgnore를 설정해 주기
    1. 직렬화에서 특정 필드를 무시하도록 하여 문제를 해결할 수 있습니다.
  4. 4번: 각 Entity 쪽에 @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 를 설정해 주기
    1. Hibernate 프록시 객체의 특정 속성을 직렬화에서 제외하여 문제를 해결합니다.

 

4번을 선택, 그 이유는?

  • 프록시 초기화 문제 해결: Hibernate 프록시 객체에는 hibernateLazyInitializer와 handler라는 내부 속성이 있습니다. 이 속성들이 직렬화 과정에서 문제를 일으킬 수 있는데, 이 어노테이션을 사용함으로써 해당 속성들이 직렬화 대상에서 제외되어 문제가 해결됩니다.
  • 데이터 무결성 유지: LAZY 로딩을 EAGER 로딩으로 변경하는 것보다 데이터 무결성 관리 측면에서 안전합니다. EAGER 로딩은 불필요한 데이터 로딩과 성능 저하를 유발할 수 있습니다.
  • 간결한 구현: 이 방법은 간단하게 어노테이션 몇 줄을 추가하는 것으로 해결이 가능합니다.

 

적용 및 해결

User

@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
@Entity
public class User extends BaseTimeEntity {
    
	...
    @OneToMany(mappedBy = "user")
    @JsonIgnoreProperties({"user"})
    private List<Image> images;

}

 

Image

@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
@Entity
public class Image extends BaseTimeEntity {
   	...
    
    @JsonIgnoreProperties({"images"})
    @JoinColumn(name = "userId")
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

}

 

Case 2-1. 양방향 매핑관계, 무한참조 이슈

Apicontroller의 리턴 코드입니다. 

return new ResponseEntity<>(new ResDto<>(1,"피드 성공", images), HttpStatus.OK);

 

이경우 Image 객체를 리턴합니다, 이때 메시지 컨버터가 발동되어 모든 getter를 발동시켜 json으로 바꿔 던져줍니다. 

 

Image를 리턴한다고 하니 모델 내부를 살펴볼 필요성이 있습니다. 

likes가 있고 그 안으로 들어가면 image가 있습니다. 이렇게 무한참조가 걸리는 것입니다. 

public class Image extends BaseTimeEntity {
    ...
    @OneToMany(mappedBy = "image")
    private List<Likes> likes;
}
public class Likes extends BaseTimeEntity {
	...
    @JoinColumn(name = "imageId")
    @ManyToOne(fetch = FetchType.LAZY)
    private Image image;
}

 

 

적용 및 해결

이를 해결하기 위해선 Image를 리턴할 때 likes내부에 있는 image가 또다시 리턴이 안되게 막아주면 됩니다. 

public class Image extends BaseTimeEntity {
    ...

    @JsonIgnoreProperties({"image"})
    @OneToMany(mappedBy = "image")
    private List<Likes> likes;
}

 

또 다른 예를 살펴보겠습니다.

Case 2-2. 양방향 매핑관계, 무한참조 이슈

관련 모델

  • Image
  • Likes
  • User

 

그럼 어떤 무한 참조가 걸렸는지 확인해봐야 합니다. 

Image

@Entity
public class Image extends BaseTimeEntity {
   
    @JsonIgnoreProperties({"images"}) 
    @JoinColumn(name = "userId")
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

    @JsonIgnoreProperties({"image"})
    @OneToMany(mappedBy = "image")
    private List<Likes> likes;

 

}

Image 선택하면 user와 likes 정보가 나옵니다.

 

Likes

@Entity
public class Likes extends BaseTimeEntity {

    @JoinColumn(name = "imageId")
    @ManyToOne(fetch = FetchType.LAZY)
    private Image image;

    @JoinColumn(name = "userId")
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;


}

우선 Likes안으로 들어가 보겠습니다.

user 정보가 있고, user안으로 들어가면 images 정보가 있습니다.

 

User

@Entity
public class User extends BaseTimeEntity {
   
    @OneToMany(mappedBy = "user")
    @JsonIgnoreProperties({"user"})
    private List<Image> images;

}

 

그런데 images안에는 likes정보가 있고

  likes안에 user가

    user안에 images가

      ..... 이런 식으로 무한참조가 걸려있는 상황입니다.

 

차근차근 첫 시작을 보면서 어디를 막아야 할지 확인해 보겠습니다.

image를 선택했을 때 user, likes정보가 나오는 것은 괜찮습니다. 하지만 likes안에 user안의 images가 나오는 게 문제입니다.

 

image를 선택하면 user, likes의 정보를 볼 수 있는데, 

  • likes안에 user 나오는 건 OK
  • user안에 image를 안 나오게 막아야 합니다. 

해결하는 여러 방법들

  • 1번: 엔티티 대신 DTO(Data Transfer Object)를 만들어 필요한 데이터만 전송
    • 이렇게 하면 원하는 데이터만 클라이언트에 전달할 수 있습니다. 이 방법은 무한 참조 문제를 근본적으로 해결하고, 보내는 데이터의 양을 줄여 성능을 개선할 수 있습니다.
  • 2번: @JsonIgnoreProperties({"필드명"}) 사용
    • 특정 필드에서만 발생하는 경우, @JsonIgnoreProperties는 해당 문제를 직접적이고 명확하게 해결 가능합니다.

2번을 선택, 그 이유는?

결정적으로 2번을 선택한 이유는 특정 필드에서만 무한 참조 문제가 발생한다고 판단했습니다. 

  • 복잡한 양방향 관계에서 특정 필드를 쉽게 제거함으로써 코드의 가독성을 높이고, 엔티티 간 관계를 이해하기 쉽게 만듭니다.
  • DTO를 사용하는 것은 추가적인 클래스를 생성하고, 데이터를 엔티티에서 DTO로 변환하는 작업이 필요한 반면, @JsonIgnoreProperties는 기존 엔티티 구조를 유지하면서 빠르게 적용할 수 있어 개발 효율성이 높습니다.
  • 특정 필드에서만 무한 참조 문제 발생시, @JsonIgnoreProperties는 해당 문제를 직관적으로 해결 가능합니다. 

이러한 이유로, 복잡한 DTO 구현 대신 @JsonIgnoreProperties를 사용해서 빠르고 간단하게 해결하였습니다.

 

적용 및 해결

Likes

@Entity
public class Likes extends BaseTimeEntity {

    @JsonIgnoreProperties({"images"}) // 적용
    @JoinColumn(name = "userId")
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;


}

이렇게 하면 Likes를 통해 User 정보를 조회할 때 Userimages 필드는 JSON 직렬화에서 제외되어 무한 참조 문제가 해결됩니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

참고:

https://stackoverflow.com/questions/67353793/what-does-jsonignorepropertieshibernatelazyinitializer-handler-do

https://boomoro.wordpress.com/