Errors를 Json으로 Serialize할 시 400 대신 500 HTTP 상태 코드가 반환되는 문제

bestKimEver·2022년 12월 29일
0
post-custom-banner

상황

백기선 님의 스프링 REST API 강의 실습 중 Spring 2.1.0에서 2.6.3으로 업그레이드 하는 과정에서 일부 테스트 케이스 응답으로 400 HTTP 상태 코드가 반환되어야 하는 것이 500 Internal Server Error 코드로 반환되는 문제 새로이 발생했다.

  • 실패한 테스트(400 대신 500이 뜸):
    • create fail tests: empty input, invalid input(logical error)
    • update fail tests: missing value, invalid input(logical 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(); 코드부터 제대로 실행되지 않는 것을 확인했다

관련 코드(일부 발췌)

  • Controller
    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));
    }
  • ErrorsResource (customized EntityModel)
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"));
    }
}
  • ErrorSerializer
@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
  • Response
{
    "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"
        }
    }
}
profile
이제 3년차 개발새발자. 제가 보려고 정리해놓는 글이기 때문에 다소 미흡한 내용이 많습니다.
post-custom-banner

0개의 댓글