백기선 님의 스프링 REST API 강의 실습 중 Spring 2.1.0에서 2.6.3으로 업그레이드 하는 과정에서 일부 테스트 케이스 응답으로 400 HTTP 상태 코드가 반환되어야 하는 것이 500 Internal Server Error 코드로 반환되는 문제 새로이 발생했다.
java.lang.AssertionError: Status expected:<400> but was:<500>
과 같은 AssertionError는 제대로 오류 메시지가 찍히지만 구체적으로 왜 이러한 일이 발생했는지에 대해서는 오류 메시지가 출력되지 않았다.
그러나 테스트 대상 API를 호출하는 과정에서 다음과 같은 로그가 공통적으로 출력되었다.
Resolved Exception:
Type = org.springframework.http.converter.HttpMessageNotWritableException
디버깅 모드로 실행했을 때 Errors를 json으로 serialize하는 ErrorsSerializer의 jsonGenerator.writeStartArray();
코드부터 제대로 실행되지 않는 것을 확인했다
private final EventService eventService;
private final ModelMapper modelMapper;
private final EventValidator eventValidator;
@PostMapping
public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto,
Errors errors,
@CurrentUser Account currentUser) {
if (errors.hasErrors()) {
return badRequest(errors);
}
eventValidator.validate(eventDto, errors);
if (errors.hasErrors()) {
return badRequest(errors);
}
Event event = modelMapper.map(eventDto, Event.class);
event.update();
event.setManager(currentUser);
Event newEvent = this.eventRepository.save(event);
WebMvcLinkBuilder selfLinkBuilder = linkTo(EventController.class).slash(newEvent.getId());
URI createdUri = selfLinkBuilder.toUri();
EventResource eventResource = new EventResource(newEvent);
// EventResource 안에 포함시키는 것이 더 권장됨.
eventResource.add(Link.of("/docs/index.html#resources-events-create").withRel("profile"));
eventResource.add(linkTo(EventController.class).withRel("query-events"));
eventResource.add(selfLinkBuilder.withRel("update-event"));
return ResponseEntity.created(createdUri).body(eventResource);
}
private ResponseEntity<ErrorsResource> badRequest(Errors errors) {
return ResponseEntity.badRequest().body(new ErrorsResource(errors));
}
public class ErrorsResource extends EntityModel<Errors> {
// JSON Array에 대해서는 자동 unwrap 기능 없음
public ErrorsResource(Errors content, Iterable<Link> links) {
super(content, links);
add(linkTo(methodOn(IndexController.class).index()).withRel("index"));
}
public ErrorsResource(Errors content) {
super(content, Links.NONE);
add(linkTo(methodOn(IndexController.class).index()).withRel("index"));
}
}
@JsonComponent
public class ErrorsSerializer extends JsonSerializer<Errors> {
@Override
public void serialize(Errors errors, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartArray(); // 여기서부터 제대로 실행되지 않고 500 응답이 반환되었다
// get field errors
errors.getFieldErrors().forEach(e -> {
try {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("objectName", e.getObjectName());
jsonGenerator.writeStringField("field", e.getField());
jsonGenerator.writeStringField("code", e.getCode());
jsonGenerator.writeStringField("defaultMessage", e.getDefaultMessage());
Object rejectedValue = e.getRejectedValue();
if (rejectedValue != null) {
jsonGenerator.writeStringField("rejectedValue", rejectedValue.toString());
}
jsonGenerator.writeEndObject();
} catch (IOException ioException) {
ioException.printStackTrace();
}
});
// get global errors
errors.getGlobalErrors().forEach(e -> {
try {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("objectName", e.getObjectName());
jsonGenerator.writeStringField("code", e.getCode());
jsonGenerator.writeStringField("defaultMessage", e.getDefaultMessage());
jsonGenerator.writeEndObject();
} catch (IOException ioException) {
ioException.printStackTrace();
}
});
jsonGenerator.writeEndArray();
}
}
반나절을 삽질하다 해당 강의 게시판에서 답을 찾았다(...)
스프링 부트 2.3으로 올라가면서 Jackson 라이브러리가 더이상 Array부터 만드는걸 허용하지 않습니다.
다음과 같이 Array 생성 전 필드명을 먼저 추가하니 400 응답 코드와 직렬화된 오류 내용이 정상적으로 반환되며 테스트를 제대로 통과한다.
(필드명 "errors"로 한번 더 wrapping된 것을 감안하여 테스트 케이스도 적절히 수정 필요함)
@Override
public void serialize(Errors errors, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeFieldName("errors"); // 추가한 코드
jsonGenerator.writeStartArray();
// get field errors
{
"errors": [
{
"objectName": "eventDto",
"field": "endEventDateTime",
"code": "NotNull",
"defaultMessage": "널이어서는 안됩니다"
},
{
"objectName": "eventDto",
"field": "closeEnrollmentDateTime",
"code": "NotNull",
"defaultMessage": "널이어서는 안됩니다"
},
{
"objectName": "eventDto",
"field": "description",
"code": "NotEmpty",
"defaultMessage": "비어 있을 수 없습니다"
},
{
"objectName": "eventDto",
"field": "beginEventDateTime",
"code": "NotNull",
"defaultMessage": "널이어서는 안됩니다"
},
{
"objectName": "eventDto",
"field": "name",
"code": "NotEmpty",
"defaultMessage": "비어 있을 수 없습니다"
},
{
"objectName": "eventDto",
"field": "beginEnrollmentDateTime",
"code": "NotNull",
"defaultMessage": "널이어서는 안됩니다"
}
],
"_links": {
"index": {
"href": "http://localhost:8080/api"
}
}
}