본문 바로가기

Study/스프링 기반 REST API 개발

03) 이벤트 조회 및 수정 REST API 개발

 

1. ErrorsResource, InvalidDefinitionException

에러가 발생하면 메인페이지로 돌아가게 할 수 있도록 index 링크 정보를 전달하려고 한다. 현재는 에러만 넘겨주는 상태에서 ErrorsResource를 만들어서 링크의 정보도 같이 넘겨주려고 한다.

@PostMapping
public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) {
   if (errors.hasErrors()) {
      return ResponseEntity.badRequest().body(errors.getAllErrors());
   }

   eventValidator.validate(eventDto, errors);
   if (errors.hasErrors()) {
      return ResponseEntity.badRequest().body(errors.getAllErrors());
   }

   Event event = modelMapper.map(eventDto, Event.class);
   event.update();

   Event newEvent = eventRepository.save(event);
   WebMvcLinkBuilder selfLink = linkTo(EventController.class).slash(newEvent.getId());
   URI createUri = selfLink.toUri();

   EventResource eventResource = new EventResource(event);
   eventResource.add(linkTo(EventController.class).withRel("query-events"));
   eventResource.add(selfLink.withRel("update-event"));
   eventResource.add((Link.of("/docs/index.html#resources-events-create").withRel("profile")));

   return ResponseEntity.created(createUri).body(eventResource);
}

 

ErrorsResource를 만들어 원하는 링크 정보를 넣어준다.

public class ErrorsResource extends EntityModel<Errors> {

   public static EntityModel<Errors> modelOf(Errors errors) {
      EntityModel<Errors> errorsModel = EntityModel.of(errors);
      errorsModel.add(linkTo(methodOn(IndexController.class).index()).withRel("index"));
      return errorsModel;
   }
}

 

그런 다음 기존에 에러를 넘겨주던걸 ErrorsResource로 만들어서 넘겨준다.

@PostMapping
public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) {
   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 newEvent = eventRepository.save(event);
   WebMvcLinkBuilder selfLink = linkTo(EventController.class).slash(newEvent.getId());
   URI createUri = selfLink.toUri();

   EventResource eventResource = new EventResource(event);
   eventResource.add(linkTo(EventController.class).withRel("query-events"));
   eventResource.add(selfLink.withRel("update-event"));
   eventResource.add((Link.of("/docs/index.html#resources-events-create").withRel("profile")));

   return ResponseEntity.created(createUri).body(eventResource);
}

private static ResponseEntity badRequest(Errors errors) {
   return ResponseEntity.badRequest().body(ErrorsResource.modelOf(errors));
}

 

하지만 이런 에러가 발생했다.

 

jakarta.servlet.ServletException: Request processing failed: org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.springframework.validation.DefaultMessageCodesResolver

 

Caused by: org.springframework.http.converter.HttpMessageConversionException

 

Caused by: cohttp://m.fasterxml.jackson.databind.exc.InvalidDefinitionExceptionNo serializer found for class org.springframework.validation.DefaultMessageCodesResolver and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: org.springframework.hateoas.EntityModel["content"]->org.springframework.validation.BeanPropertyBindingResult["messageCodesResolver"])

 

스택 트레이스를 살펴보면, Errors 객체를 JSON으로 변환하려고 할  때 DefaultMessageCodesResolver에 대한 직렬화 도구가 없어서 문제가 발생했다고 한다. Spring은 응답을 생성할 때 Jackson 라이브러리를 사용하여 Java 객체를 JSON으로 변환한다. 그런데 이 변환 과정에서 DefaultMessageCodesResolver 클래스를 JSON으로 직렬화할 수 없다는 에러가 발생했다. DefaultMessageCodesResolver에 적절한 직렬화 방법이 없거나, 해당 클래스를 JSON으로 변환할 수 없는 속성을 포함하고 있기 때문일 수도 있다고 한다.

 

그래서 기존에 정의했던 ObjectMapper에 설정을 정의해 준다. Errors 인스턴스를 JSON으로 변경해 주던 ErrorsSerializer를 Errors클래스에 직렬화 하도록 ObjectMapper에 설정해 주면 된다. SimpleModule는 Jackson 라이브러리에서 제공하는 클래스로, 커스텀 직렬화 방법이나 역직렬화 방법을 정의하고 등록하는 데 사용된다. 

@Bean
public ObjectMapper objectMapper() {
   ObjectMapper objectMapper = new ObjectMapper();
   objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);

   objectMapper.registerModule(new JavaTimeModule());

   SimpleModule module = new SimpleModule();
   module.addSerializer(Errors.class, new ErrorsSerializer());
   objectMapper.registerModule(module);

   return objectMapper;
}

 

기존에 직렬화/역직렬화에 대한 문제는 해결되었지만 다른 에러가 발생했다.

java.lang.AssertionError: Status expected:<400> but was:<500>
Expected :400
Actual   :500
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)

 

다행히 해당 문제는 커뮤니티에서 금방 찾을 수 있었다. 문제의 내용은 이렇다. Jackson 라이브러리가 JSON 배열을 생성할 때 해당 배열의 이름을 필요로 하기 때문이다. 이전에는 배열 이름 없이도 배열을 생성할 수 있었지만, 최근 버전에서는 배열 이름을 명시해주어야 한다.  그래서 기존에 ErrorsSerializer에서 

gen.writeFieldName("errors") 

 

를 상단에 정의를 해주고 기존에 값을 검증하는 코드를 바꿔주면 된다.

 

 

2. Page

페이지의 정보를 가져오는 코드이다. Pageable 파라미터는 클라이언트가 요청할 페이지 번호와 페이지 크기, 정렬 방식 등의 정보를 담고 있다. JPA를 통해 Pageable 객체를 인자로 받아 페이징 된 Event 객체들을 Page 객체로 반환한다. 그렇게 만들어진 page 객체를 JSON 형태로 직렬화돼서 클라이언트에게 전송된다.

@GetMapping
public ResponseEntity queryEvents(Pageable pageable) {
	Page<Event> page = eventRepository.findAll(pageable);

	return ResponseEntity.ok(page);
}

 

아래는 위의 요청에 대한 body 값이다. 

하지만 body에는 우리가 원하는 링크 정보가 없다. 여기서 PagedResourcesAssembler를 활용하면 이러한 부분을 처리할 수 있다. PagedResourcesAssembler는 Spring Data의 Page 인스턴스를 Spring HATEOAS의 PagedModel 인스턴스로 변환해 준다. 이렇게 변환된 PagedModel은 HATEOAS 원칙에 따라 표현될 수 있는 모델로, 각 이벤트에 대한 링크 정보와 함께, 페이징 정보도 포함해 보여준다.

@GetMapping
public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler) {
	Page<Event> page = eventRepository.findAll(pageable);
      
	var pagedModel = assembler.toModel(page);

	return ResponseEntity.ok(pagedModel);
}

 

이렇게 하면 body 값은 content -> _embedded.eventList로 변경되고 아래에 링크의 정보를 포함해서 보여준다.

 

하지만 하나의 이벤트 안에는 링크의 정보가 없다. 그래서 이전에 만들어줬던 EventResource를 활용해서 각자의 링크 정보 self를 넣어준다.

 

@GetMapping
public ResponseEntity queryEvents(Pageable pageable, PagedResourcesAssembler<Event> assembler) {
	Page<Event> page = eventRepository.findAll(pageable);
    
	var pagedModel = assembler.toModel(page, EventResource::new);

	return ResponseEntity.ok(pagedModel);
}

 

이렇게 되면 각 이벤트에 대해 링크정보가 들어가게 된다.

'Study > 스프링 기반 REST API 개발' 카테고리의 다른 글

02) HATEOAS와 Self-Describtive Message  (1) 2023.11.30
01) 이벤트 생성 API 개발  (0) 2023.11.25