글 순서 : 프로젝트 생성(의존성의 관계) -> 발생했던 상황 -> 에러 코드 -> 원인 확인 -> 문제의 지점 체크 -> 해결 방안
핵심 상황:
Spring Boot의 initializar를 통해 WebFlux 라이브러리를 추가한 예제 프로젝트에서
1. RestTemplate.exchange()를 사용할 때,
2. 또 WebClient를 사용했을 때 발생한 Exception이다.
3. 두 Request는 공통적으로 사용자 정의 객체를 담고있는 List와 Flux이다.
Error 내용
응답 메시지
{
"timestamp": "2019-08-20T17:40:30.328+0000",
"path": "/non-blocking",
"status": 500,
"error": "Internal Server Error",
"message": "Type definition error: [simple type, class com.example.webclientdemo.demo.repository.User]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.webclientdemo.demo.repository.User` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n at [Source: UNKNOWN; line: -1, column: -1]"
}
오류 로그 (blocking)
2019-08-21 03:02:02.870 INFO 15544 --- [ctor-http-nio-2] c.e.w.demo.domain.HelloController : Starting BLOCKING, type = false
2019-08-21 03:02:06.043 ERROR 15544 --- [ctor-http-nio-2] a.w.r.e.AbstractErrorWebExceptionHandler : [4d67195c] 500 Server Error for HTTP GET "/blocking"
org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.example.webclientdemo.demo.repository.User]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.webclientdemo.demo.repository.User` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (PushbackInputStream); line: 1, column: 3] (through reference chain: java.util.ArrayList[0])
주요 포인트 : (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
오류 로그 (non-blocking)
2019-08-21 02:40:28.071 INFO 10904 --- [ctor-http-nio-2] c.e.w.demo.domain.HelloController : Starting NON_BLOCKING, type = false
2019-08-21 02:40:28.140 INFO 10904 --- [ctor-http-nio-2] c.e.w.demo.domain.HelloController : Exiting NON-BLOCKING
2019-08-21 02:40:30.325 WARN 10904 --- [ctor-http-nio-6] io.netty.util.ReferenceCountUtil : Failed to release a message: DefaultLastHttpContent(data: PooledSlicedByteBuf(freed), decoderResult: success)
io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
at io.netty.util.internal.ReferenceCountUpdater.toLiveRealRefCnt(ReferenceCountUpdater.java:74) ~[netty-common-4.1.38.Final.jar:4.1.38.Final]
2019-08-21 02:40:30.332 ERROR 10904 --- [ctor-http-nio-8] a.w.r.e.AbstractErrorWebExceptionHandler : [15d850da] 500 Server Error for HTTP GET "/non-blocking"
org.springframework.core.codec.CodecException: Type definition error: [simple type, class com.example.webclientdemo.demo.repository.User]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.webclientdemo.demo.repository.User` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
주요 포인트 : org.springframework.core.codec.CodecException: (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
위 내용의 오류와 함께 500 Server error를 던져준다.
기본 생성자가 존재하지 않는다는 에러 메시지를 확인할 수 있다.
로그 내용 중com.example.webclientdemo.repository.User의 클래스 경로가 확인할 수 있는데 이 객체를 JSON으로 Encoder/Decoder 할 때 오류가 발생한 것이다.
결론부터 말하면 간단하게 무인자 생성자 생성을 해주면 해결된다.
public class User {
private String name;
private String email;
//추가 되어야할 코드
//public User(){}
public User(String name, String email) {
this.name = name;
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Controller에서 어떻게 활용 했는지도 확인이 필요할 것이다.
@RestController
public class HelloController {
@GetMapping("/slow-service-users")
private List<User> getAllUsers() throws InterruptedException {
Thread.sleep(2000L);
return Arrays.asList(
new User("restTemplate","pp@Sync")
, new User("WebClient","ss@Async")
, new User("sounds good to me","yy@POP"));
}
@GetMapping(value = "/blocking")
public <T> List<User> getBlocking() {
boolean type = getType();
log.info("Starting BLOCKING, type = " + type);
final String uri = getSlowServiceUri(type);
var restTemplate = new RestTemplate();
ResponseEntity<List<User>> response = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference<List<User>>() {});
List<User> result = response.getBody();
result.forEach(o -> log.info(o.toString()));
log.info("Exiting BLOCKING");
return result;
}
@GetMapping(value = "/non-blocking", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public <T> Flux<User> getNonBlocking(){
boolean type = true;
log.info("Starting NON_BLOCKING, type = " + type);
Flux<User> flux = WebClient.create()
.get()
.uri(getSlowServiceUri(type))
.retrieve()
.bodyToFlux(User.class);
flux.subscribe(obj -> log.info(obj.toString()));
log.info("Exiting NON-BLOCKING");
return flux;
}
private String getSlowServiceUri(boolean getA) {
return getA ? "http://localhost:8080/slow-service-users" : "";
}
}
localhost:8080/blocking 요청이 오면 slow-service-users를 호출하여 그 반환 값을 가져다 주는 방식이고 Thread를 잠깐 재워서 지연시키는 과정에서 Blocking을 확인하는 예제이다.
이때 객체를 List에 담아 반환하고 그 반환한 것을
ParameterizedTypeReference<List<User>>(){}
에 감싸서 ResponseEntity<List<User>>로 받는 코드인데 이 구문에서 HttpMessageConversionException이 발생한 것.
바로 아랫줄에 AbstractJacson2HttpMessageConverter.java:242의 encodeValue부터 추적해보니,
protected Object _readMapAndClose(JsonParser p0, JavaType valueType)
throws IOException
{
try (JsonParser p = p0) {
Object result;
JsonToken t = _initForReading(p, valueType);
final DeserializationConfig cfg = getDeserializationConfig();
final DeserializationContext ctxt = createDeserializationContext(p, cfg);
if (t == JsonToken.VALUE_NULL) {
// Ask JsonDeserializer what 'null value' to use:
result = _findRootDeserializer(ctxt, valueType).getNullValue(ctxt);
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = null;
} else {
JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, valueType);
if (cfg.useRootWrapping()) {
result = _unwrapAndDeserialize(p, ctxt, cfg, valueType, deser);
} else {
//포인터 지점
result = deser.deserialize(p, ctxt);
}
ctxt.checkUnresolvedObjectId();
}
if (cfg.isEnabled(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)) {
_verifyNoTrailingTokens(p, ctxt, valueType);
}
return result;
}
}
ObjectMapper 클래스의 _readMapAndClose 메서드의 deser.deserialize(p, ctxt); 구문에서 jackson.databind.exc.InvalidDefinitionException이 발생한 것.
Convert를 하기 위해서 무인자 생성자를 추가하니 데이터를 정상적으로 가지고 올 수 있었다.
※다른 포스팅에서 같은 내용의 문제는 DTO 객체에 getter/setter가 없어서 발생한 에러가 있는 것으로 확인.
내 경험은 getter/setter/NoArgsConstructor 의 조건이 맞아야 바인딩 시 직렬화/역직렬화가 가능하다는 결론이 첫 번째이다.
직렬화를 할 때 무인자 생성자를 통해 객체를 생성하고 getter/setter를 활용한다는 것인가?
나는 두 번째 해결 방법에서 더 많은 생각을 하게 되었다.
두 번째 방법은 이후 포스팅에서 남길 예정이다.
예제 소스코드 :
'WEB' 카테고리의 다른 글
highcharts exports 한글로 변경하기 (0) | 2021.01.08 |
---|---|
javascript JSON object validation (0) | 2020.04.04 |
spring-integration의 sftp.session.DefaultSftpSessionFactory 설정 (0) | 2020.02.17 |
application.properties 설정 (0) | 2020.02.03 |
CodexException: 500 Server Error / Generic.ver (0) | 2019.08.25 |