[스프링부트] com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable Ser..
오류 내용
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain
이 에러는 Jackson 라이브러리가 객체를 JSON으로 직렬화하는 과정에서 발생하였다.
직렬화란 Object를 연속된 String 데이터나 연속된 Bytes 데이터로 바꾸는 것을 의미한다.
에러 메시지에서 나오는 내용을 해석하자면, Jackson이 org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor 클래스를 직렬화할 수 있는 방법을 찾지 못했다는 것이다.
발생 원인
문제 원인 코드 QuizService
public GetPagedQuizRes getQuizList(int size, int page) {
List<Quiz> quizList= quizRepository.findAllByStatus(ACTIVE);
List<Long> quizLikes = quizLikeRepository.findAllByUserAndStatus(1L, ACTIVE);
if(page > quizList.size()/size){
throw new BaseException(PAGE_COUNT_OVER);
}
PageRequest pageRequest = PageRequest.of(page, size);
int start = (int) pageRequest.getOffset();
int end = Math.min((start + pageRequest.getPageSize()), quizList.size());
Page<Quiz> pagingQuiz = new PageImpl<>(quizList.subList(start, end), pageRequest, quizList.size());
int total = quizList.size();
List<GetQuizRes> quizResList = pagingQuiz.getContent().stream()
.map(quiz -> new GetQuizRes(quiz.getTitle(), quiz.getType(), quiz.getStackUnit(), quiz.getQuizUnit(),
checkUserLikesQuiz(quizLikes, quiz)))
.collect(Collectors.toList());
return new GetPagedQuizRes(quizResList, total, page);
}
이 문제는 일반적으로 Hibernate의 지연 로딩(Lazy Loading) 기능 때문에 발생한다. Hibernate는 프록시(proxy)를 사용하여 지연 로딩을 구현하는데, 이로 인해 직렬화할 때 문제가 발생할 수 있다.
나의 경우 해당 코드에서 에러가 발생했는데, 두번째 line에서 quiz.getQuizUnit() 부분에서 에러가 났다.
Quiz 엔티티와 관계된 QuizUnit(ManyToOne)이 FetchType.LAZY로 설정되어 있다.
Jackson으로 Quiz 엔티티를 Serialize를 할때, LAZY 설정으로 비어있는 객체(QuizUnit)를 Serialize 하려고 해서 발생되는 문제이다.
해결 방법
1. DTO로 바꾸어 사용할 데이터만 반환해 사용
DTO에 엔티티를 직접 담기보다 사용할 데이터만 조회할 수 있도록 한다.
디버깅을 돌려봤을 때 QuizUnit의 프록시 객체를 조회하는 것을 확인할 수 있다.
중간객체를 설정하여 중간객체의 구체적인 값을 불러오고 DTO에 담아주는 식으로 로직을 수정하니 해결되었다.
2. 오류가 발생하는 컬럼에 @JsonIgnore 설정
해당 필드를 Json으로 변경시 제외된다.
Json의 응답결과에 @JsonIgnore이 선언되어 있는 객체가 빠지게 되어 오류를 해결할 수 있다.
그러나 선언한 엔티티의 값도 포함하고 싶은 경우 Json 응답 결과가 빠져버리게 되어 알 수 없다.
이런 경우 서비스 내 응답 결과로 해당 엔티티가 필요 없는 경우의 확장성까지 생각해 선언하는 것이 좋아보인다.
3. LAZY를 EAGER로 변경
하지만 이 방법은 N+1 문제를 발생시킬 수 있어 좋은 방법은 아닌 거 같다.
4. application 파일에 spring.jackson.serialization.fail-on-empty-beans=false 설정
오류만 안나오도록 하는거라 어떤 문제가 있을지는 모른다.
5. 각 Entity쪽에 @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 설정
정리 내용
- 가급적 LAZY 로딩 전략을 사용하고, 엔티티를 엮어서 함께 사용해야한다면 JPQL의 fetch join을 통해 한방 쿼리로 가져와 사용하자.
- 스프링부트는 @ResponseBody를 선언할 때 Object를 json으로 변환하기 위해 Jackson 라이브러리를 사용한다.
- Jackson 순환 참조 에러는 @OneToMany, @ManyToOne에서 반복 참조로 인하여 직렬화를 이용해 json 형태로 객체를 변환시킬 때 발생한다.