RestApi ๋งŒ๋“ค๊ธฐ - API์ธ๋ฑ์Šค ์ง€์  ๋งŒ๋“ค๊ธฐ (8)

๋ฐ˜์‘ํ˜•

* ์—ฌ๊ธฐ์„œ ์ธ๋ฑ์Šค๋ž€?
๋ฉ”์ธ ํŽ˜์ด์ง€๋ฅผ ๋œปํ•ฉ๋‹ˆ๋‹ค.
๋ฉ”์ธํŽ˜์ด์ง€๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜์ž.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class IndexControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @Test
  void index() throws Exception {
    mockMvc.perform(get("/delivery"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("_links.delivery").exists());
  }

}

์ฝ”๋“œ๊ฐ€ DeliveryTest์™€ ์ค‘๋ณต๋œ ๋ถ€๋ถ„์ด ๋ณด์ด์ง€๋งŒ...
์ผ๋‹จ ๊ทธ๋Œ€๋กœ ์ง„ํ–‰ํ•˜์˜€๋‹ค.(์‚ฌ์‹ค ๋ชจ๋ฆ„)
์•„๋ฌดํŠผ
ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š”
get์œผ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ๊ทธ๊ฒƒ์„ ํ”„๋ฆฐํŠธํ•˜๊ตฌ
์ƒํƒœ๊ฐ’์ด 200์ธ์ง€ ํ™•์ธํ•˜์ž.
๋งˆ์ง€๋ง‰์œผ๋กœ ์œ„ ๋งํฌ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.

ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋Š”

๋‹น์—ฐํžˆ 404๊ฐ€ ๋‚˜์™”๋‹ค.

๋‚˜๋Š” IndexController๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์—
๊ทธ๊ณณ๋ถ€ํ„ฐ ๋งŒ๋“ค์ž.

@Controller
public class IndexController {

  @GetMapping("/delivery")
  public ResponseEntity<?> index() {
    return ResponseEntity.ok().build();
  }

}

์ด๋ ‡๊ฒŒ ๋งŒ๋“ค๋ฉด ์ž˜ ๋ ๊นŒ?
์ƒ๊ฐํ•ด๋ณธ๋‹ค.

์ด๋ฒˆ์—๋Š” link๊ฐ€ ์—†๋‹ค๊ณ  ํ•œ๋‹ค.

๋‹ค์‹œ 

  • ResourceSupport => RepresentationModel

  • Resource => EntityModel

  • Resources => CollectionModel

  • PagedResources => PagedMode

์ด๊ฒƒ์„ ๋ณต์Šตํ•ด๋ณด์ž.
RepresentationModel๋ฅผ ์ด์šฉํ•ด์„œ ๋ฆฌ์†Œ์Šค๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
pagedMode๋Š” ๋˜์ง€ ์•Š๊ณ , ๋‚˜๋จธ์ง€ ๋‘๊ฐœ๋Š” ์ƒ์†์ผ๋•Œ๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค.
๊ทธ๋Ÿฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

public ResponseEntity<?> index() {
    var resource = new RepresentationModel<>();
    resource.add(linkTo(IndexController.class).withRel("delivery"));
    return ResponseEntity.ok().body(resource);
  }

build์—์„œ body๋กœ ๋ฐ”๊พผ์ด์œ ๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค.
์‹คํ–‰์„ ์‹œํ‚ค๋ฉด..

์ •์ƒ์ ์œผ๋กœ ๋™์ž‘๋œ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์ด๊ฒƒ์€ ์–ธ์ œ ์‚ฌ์šฉ์ด ๋˜์–ด์งˆ๊นŒ?
์ƒ๊ฐํ•ด๋ณด๋ฉด ์—๋ŸฌํŽ˜์ด์ง€์—์„œ ์‚ฌ์šฉ๋˜์–ด์ง„๋‹ค.

์–ด๋”˜์ง€๋Š” ๋ชจ๋ฅด๊ฒ ์œผ๋‚˜. ์ด๋Ÿฐ์‹์œผ๋กœ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ๋ฉ”์ธํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ฒŒ ๋˜์–ด์žˆ๋‹ค.
๊ทธ๋ž˜์„œ ์šฐ๋ฆฌ๋„ ์—๋ŸฌํŽ˜์ด์ง€์— ์ถ”๊ฐ€ํ•ด๋ณด์ž.

@Test
  void badRequest_wrong_input() throws Exception {
    DeliveryDto delivery = DeliveryDto.builder()
        .item("book")
        .user("klom")
        .deliveryEndTime(LocalDateTime.now())
        .deliveryTime(LocalDateTime.now().plusDays(10))
        .itemPrice(0)
        .build();

    mockMvc.perform(post("/api/delivery/")
        .contentType(MediaType.APPLICATION_JSON_VALUE)
        .content(objectMapper.writeValueAsString(delivery))
    )
        .andDo(print())
        .andExpect(status().isBadRequest())
        .andExpect(jsonPath("$.[0].objectName").exists())
        .andExpect(jsonPath("$.[0].field").exists())
        .andExpect(jsonPath("$.[0].rejectedValue").exists())
        .andExpect(jsonPath("$.[0].links.index").exists())
    ;
  }

์ž˜๋ชป๋œ ๋งํฌ์— ๋“ค์–ด๊ฐ€๊ฒŒ ๋˜๋ฉด ์ด ๋งํฌ๊ฐ€ ์‹คํ–‰๋˜๊ฒŒ ๋งŒ๋“ค์—ˆ๋‹ค.
์ด๊ฑธ ์‹คํ–‰ํ•ด๋ณด๋ฉด.

 ์ธ๋ฑ์Šค๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ๋‚˜์˜จ๋‹ค.

๊ทธ ์ด์œ ๋Š”

if (errors.hasErrors()) {
   return ResponseEntity.badRequest().body(errors);
}

์ด๊ฑฐ ๋•Œ๋ฌธ์ด๋‹ค.
์ด๊ฒƒ์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ฆฌ์†Œ์Šค ์ฆ‰ ๋งํฌ ์ •๋ณด๋ฅผ ๋„ฃ์–ด์ค˜์•ผ ๋œ๋‹ค.
๊ทธ ์ •๋ณด๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.
๋งˆ์นจ

public class DeliveryModel extends EntityModel<Delivery> {
  public DeliveryModel(Delivery delivery, Link... links) {
    super(delivery, links);
    add(linkTo(DeliveryController.class).withRel("query-events"));
    add(linkTo(DeliveryController.class).slash(delivery.getId()).withRel("update-events"));
    add(linkTo(DeliveryController.class).slash(delivery.getId()).withSelfRel());
    add(Link.of("http://localhost:8080/docs/index.html#resources-index").withRel("profile"));
  }
}

์ด๋ ‡๊ฒŒ ๋งŒ๋“ ๊ฒŒ ์žˆ์–ด์„œ ์ด๊ฒƒ์„ ์ฐธ๊ณ ํ•ด์„œ ๋งŒ๋“ค๋ฉด ๋ ๊ฒƒ ๊ฐ™๋‹ค.

public class ErrorModel extends EntityModel<Errors> {

  public ErrorModel(Errors content, Link... links) {
    super(content, links);
    add(linkTo(methodOn(IndexController.class).index()).withRel("index"));
  }
}

๋งŒ๋“ ๋’ค,

 if (errors.hasErrors()) {
     return ResponseEntity.badRequest().body(new ErrorModel(errors));
}

์ด๋ ‡๊ฒŒ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.
๋งŒ์•ฝ ์ค‘๋ณต๋œ๊ฒŒ ์กด์žฌํ•˜๋ฉด ๋ฆฌํŽ™ํ† ๋งํ•ด์„œ ๋ณ€๊ฒฝ์‹œ์ผœ์ค˜๋„ ๋œ๋‹ค.
๊ทธ๊ฑด ์•Œ์•„์„œ...
๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

์ด๋Ÿฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”๋ฐ..
์ด์œ ๋ฅผ ํ™•์ธํ•ด๋ณด๋‹ˆ ์˜ค๋ธŒ์ ํŠธ๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์—†๋‹ค.
ํ•„๋“œ ์ด๋ฆ„์„ ๋„ฃ์–ด์•ผ ๋œ๋‹ค๋ผ๊ณ  ๋‚˜์™€ ์žˆ๋‹ค.

์Šคํ”„๋ง2.3์ด ๋„˜์–ด์˜ค๋ฉด์„œ ๋ฐฐ์—ด๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ•œ๋‹ค.

 

[์ธ๋ฑ์Šค ๋งŒ๋“ค๊ธฐ] ์—์„œ ErrorsResource ๋ถ€๋ถ„ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค. - ์ธํ”„๋Ÿฐ | ์งˆ๋ฌธ & ๋‹ต๋ณ€

์ข‹์€ ์งˆ๋ฌธ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์Šคํ”„๋ง ๋ถ€ํŠธ 2.3์œผ๋กœ ์˜ฌ๋ผ๊ฐ€๋ฉด์„œ Jackson ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋”์ด์ƒ Array๋ถ€ํ„ฐ ๋งŒ๋“œ๋Š”๊ฑธ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. 2020-10-02 22:09:06.151  WARN 14548 --- [           main] .w.s.m.s.DefaultHandlerExcep

www.inflearn.com

๊ทธ๋ž˜์„œ 

jsonGenerator.writeFieldName("errors");
jsonGenerator.writeStartArray();

์ด๋Ÿฐ์‹์œผ๋กœ ์ถ”๊ฐ€ํ•˜๊ตฌ.

.andExpect(jsonPath("errors[0].objectName").exists())
.andExpect(jsonPath("errors[0].field").exists())
.andExpect(jsonPath("errors[0].rejectedValue").exists())
.andExpect(jsonPath("_links.index").exists())

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ด๋ ‡๊ฒŒ ์ˆ˜์ •ํ–ˆ๋‹ค.
๊ทธ๋ฆฌ๊ณ  ์‹คํ–‰์‹œ์ผœ๋ณด์ž.

์ž˜ ๋‚˜์˜จ๋‹ค.
๊ฒธ์‚ฌ๊ฒธ์‚ฌ

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

์ด ์ฝ”๋“œ๋„ ์ˆ˜์ •์‹œ์ผœ์คฌ๋‹ค.
๊ฐ•์˜๋Š” 13๋ถ„์งœ๋ฆฌ์ง€๋งŒ... ์ด๊ฑธ 1์‹œ๊ฐ„ ๋„˜๊ฒŒ ํ–ˆ๋„ค ใ…Ž;

๋Œ“๊ธ€

Designed by JB FACTORY