에러 발단
RestController에서 Answer를 반환하여 HttpMessageConverter가 JSON 형태로 변환시는 와중에 Answer와 Question의양방향 연관관계로 인해 "순환참조"가 발생했다. 그래서 굳이 필요하지 않은 answers에 @JsonIgnore를 붙여주었다
이제 순환참조는 해결됐다. 그러나 삭제하는 과정에서 Answer 객체를 반환할 때, 아래와 같은 에러가 발생했다
에러 전문
2022-04-17 01:21:29.446 ERROR 67772 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: codesquad.answer.Answer["question"]->codesquad.qua.Question_$$_jvstefe_2["handler"])] with root cause
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: codesquad.answer.Answer["question"]->codesquad.qua.Question_$$_jvstefe_2["handler"])
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:312) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:71) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:33) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1396) ~[jackson-databind-2.9.6.jar:2.9.6]
at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:913) ~[jackson-databind-2.9.6.jar:2.9.6]
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:286) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:102) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:272) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:180) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doDelete(FrameworkServlet.java:899) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:667) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:851) ~[spring-webmvc-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:742) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) ~[tomcat-embed-websocket-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:496) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.31.jar:8.5.31]
at net.rakugakibox.spring.boot.logback.access.tomcat.LogbackAccessTomcatValve.invoke(LogbackAccessTomcatValve.java:91) [logback-access-spring-boot-starter-2.7.1.jar:2.7.1]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:803) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1468) [tomcat-embed-core-8.5.31.jar:8.5.31]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.31.jar:8.5.31]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_322]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_322]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.31.jar:8.5.31]
at java.lang.Thread.run(Thread.java:750) [na:1.8.0_322]
에러 원인
댓글 생성 예시 코드
@PostMapping("/questions/{question-id}/answers")
@ResponseBody
public Answer create(@PathVariable("question-id") Long questionId, @RequestBody Answer answer, HttpSession session) {
Question question = questionRepository.findById(questionId)
.orElseThrow(NoSuchElementException::new);
User user = SessionUtil.getUserBySession(session);
answer.addQuestion(question);
System.out.println("AnswerController.create");
System.out.println("answer.getQuestion().getClass() = " + answer.getQuestion().getClass());
answerRepository.save(answer);
return answer;
}
question 객체를 조회하고, 연관관계 편의 메서드를 사용하고, answer를 저장했다
당연히 question은 "실제" 엔티티이므로 class를 출력할 때는 proxy 클래스가 아닌 실제 클래스가 나온다
댓글 삭제 예시 코드
@DeleteMapping("/questions/{question-id}/answers/{answer-id}")
@ResponseBody
public Answer remove(@PathVariable("answer-id") Long answerId, HttpSession session) {
User user = SessionUtil.getUserBySession(session);
Answer answer = answerRepository.findById(answerId)
.orElseThrow(NoSuchElementException::new);
answerRepository.delete(answer);
System.out.println("AnswerController.remove");
System.out.println("answer.getQuestion().getClass() = " + answer.getQuestion().getClass());
return answer;
}
answer 객체를 조회하고, answerRepository에서 answer를 삭제했다 그리고 나서 answer 객체를 반환한다
여기서 에러가 발생하게 된다
왜 발생하는걸까?
댓글 생성에서 question 의 class 출력 값이 다른 것을 알 수 있다
answer에서 question 객체는 fetch type이 LAZY이다
따라서 answer를 조회할 때, question은 현재 "프록시" 객체이다
그러므로 question의 클래스를 출력하면 "프록시" 객체가 출력이 되기 때문에 위와 같은 값을 볼 수 있다
이제 여기서 answer에는 프록시 객체 question이 들어있다
HttpMessageConverter가 answer의 getXXX를 이용하여 JSON으로 변환하는 과정에서 프록시 객체를 만나게 된다
이때 위와 같은 에러가 발생한다
나는 answer에서 question 객체를 @JsonIgnore를 사용할 수도 없고, 어떻게 해결하면 좋을까?
에러 해결
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 사용
위 어노테이션을 클래스 레벨에 사용하면 해결된다
@JsonIgnoreProperties는 지정한 JSON의 property를 무시한다는 뜻으로
JSON property 중 hibernateLazyInitializer와 handler을 무시한다라는 말이다
그렇다면 hibernateLazyInitializer와 handler는 무엇일까?
어떤 엔티티를 조회하여 반환할 때 Jackson 라이브러리를 사용하여 JSON 문자열 형식으로 변환한다
여기서 연관된 엔티티 중에 LAZY loading을 사용하는 객체를 변환할 경우 엔티티의 모든 필드 및 추가로 "hibernateLazyInitializer", "handler" 필드를 JSON 형식으로 변환한다
그래서 추가된 필드를 무시하는 @JsonIgnoreProperties를 사용하여 문제를 해결할 수 있다
- stackoverflow
하지만 이 방법은 표면적으로 해결하는 것이지 제대로(?) 해결한 게 아니다
그래서 answer를 조회할 때 바로 "실제" 엔티티를 조회하면 문제를 해결할 수 있지 않을까라는 생각이 들었다
@EntityGraph를 사용하여 문제 해결
answer를 조회할 때 "실제" question 엔티티를 조회하는 방법으로 fetch join이 있다
spring data jpa에서는 @EntityGraph을 통하여 fetch join을 할 수 있다
@EntityGraph(attributePaths = {"question"})
Optional<Answer> findFetchJoinById(Long id);
예시 코드에서 answer 객체를 찾아올 때 위 코드를 통하여 answer를 조회하면 아래와 같은 결과값을 얻을 수 있다
이제는 question "프록시" 객체가 아닌 실제 객체이므로 반환하는데 이상 없게 된다
REFERENCES
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) ??
'Spring' 카테고리의 다른 글
@Scheduled를 사용하여 API를 주기적으로 호출 (0) | 2022.06.07 |
---|---|
MapStruct (0) | 2022.05.30 |
spring boot 2.6.x에 swagger 3.0 설정해보자(+수정 openapi 3.0 2022.05.30) (0) | 2022.05.11 |
Filed Injection을 지양하자! (0) | 2022.05.02 |
HandlerMapping에서 order는 어디에 사용될까? (0) | 2022.03.11 |