2023. 1. 18. 14:48ㆍProject/시네마그램
토이 프로젝트를 하면서 순환참조 문제가 종종 발생하였습니다. 처음에는 당황했지만 차근차근 코드를 따라가 보니 해결할 수 있었습니다.
이번 포스팅에서는 크게 두 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번 : application 파일에 spring.jackson.serialization.fail-on-empty-beans=false 설정해 주기
- 직렬화 실패 시 오류를 무시하도록 하여 문제를 회피합니다. 하지만 이는 오류를 숨기는 것일 뿐 근본적인 해결책은 아닙니다.
- 2번 : 오류가 나는 필드의 LAZY 설정을 EAGER로 바꿔주기
- 이 방법은 필요한 데이터를 미리 로딩하지만, N+1 문제를 유발할 수 있어 주의가 필요합니다.
- 3번: 오류가 나는 필드에 @JsonIgnore를 설정해 주기
- 직렬화에서 특정 필드를 무시하도록 하여 문제를 해결할 수 있습니다.
- 4번: 각 Entity 쪽에 @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 를 설정해 주기
- 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 정보를 조회할 때 User의 images 필드는 JSON 직렬화에서 제외되어 무한 참조 문제가 해결됩니다.
참고:
'Project > 시네마그램' 카테고리의 다른 글
[Cinemagram] Popular 페이지 렌더링 - (11) (0) | 2023.01.26 |
---|---|
[Cinemagram] 좋아요 기능 구현 - (10) (0) | 2023.01.25 |
[Cinemagram] 팔로우 기능 구현 - (9) (1) | 2022.12.26 |
[Trouble Shooting] JPA DTO Mapping - QLRM 라이브러리 사용 (0) | 2022.12.23 |
[Cinemagram] Feed 페이지 렌더링 - (8) (0) | 2022.12.19 |