포스트

REST API(11) HATEOAS 적용(2) - 인덱스 만들기

구현 Package 및 Class

image


API 인덱스 만들기

우리는 웹을 이용할 때, 처음 접근 시에는 Url로 접근해도 그 이후의 행위들은 따로 또 Url을 직접 입력 하는 것 없이, 모두 클릭이나, 값을 입력하는 것을 통해 일어난다.
이와 같이 우리는 이벤트를 조회, 생성, 수정 등을 하기 위해서는 API의 진입점이 필요하다.
이번에는 그 진입점에 해당하는 인덱스를 만들어 보겠다.


IndexControllerTest

먼저 test에 index 패키지를 만들고, IndexControllerTest.java 클래스를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package me.hantomas.restapi.index;

import me.hantomas.restapi.common.RestDocsConfiguration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;


import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class IndexControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void index() throws Exception {
        this.mockMvc.perform(get("/api/"))
                .do(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("_links.events").exists());
    }
}


IndexController

그리고 똑같이 index패키지를 만들고 IndexController.java 클래스를 생성하여 이벤트에 대한 링크를 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package me.hantomas.restapi.index;

import me.hantomas.restapi.events.EventController;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;

@RestController
public class IndexController {
    @GetMapping("/api")
    public ResourceSupport index(){
        var index = new ResourceSupport();
        index.add(linkTo(EventController.class).withRel("events"));
        return index;
    }
}

  • EventController클래스에 대한 링크(@RequestMapping("/api/events")을 통해 /api/events"를 요청하는)를"events"관계로 링크를 추가한다.
    👉”_links”:{“events”:{“href”:”http://localhost:8080/api/events”}}

IndexControllerTest 테스트 결과(성공)

image


ErrorResource

인덱스 링크는 입력 값이 잘못되어 에러가 발생한 경우에 인덱스로 가는 링크를 제공할 때 사용할 수 있다.
common패키지 아래에, 이를 위한 ErrorResource.java클래스를 구현하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package me.hantomas.restapi.common;

import me.hantomas.restapi.index.IndexController;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.validation.Errors;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

public class ErrorsResource extends Resource<Errors> {

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

  • 에러가 있을때 IndexController.classindex()메서드에 대한 링크("/api"로 GET 요청을 하는)를 "index"라는 관계로 생성한다.
    👉 “_links”:{“index”:{“href”:”http://localhost:8080/api”}}

EventController 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//import 생략

@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
    private final EventRepository eventRepository;

    private final ModelMapper modelMapper;

    private final EventValidator eventValidator;

    public EventController(EventRepository eventRepository, ModelMapper modelMapper, EventValidator eventValidator) {
        this.eventRepository = eventRepository;
        this.modelMapper = modelMapper;
        this.eventValidator = eventValidator;
    }
    @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 = this.eventRepository.save(event);

        ControllerLinkBuilder selfLinkBuilder = linkTo(EventController.class).slash(newEvent.getId());
        URI createdUri = selfLinkBuilder.toUri();
        EventResource eventResource = new EventResource(newEvent);
        eventResource.add(linkTo(EventController.class).withRel("query-events"));
        eventResource.add(selfLinkBuilder.withRel("update-event"));
        eventResource.add(new Link("/docs/index.html#resources-events-create").withRel("profile"));
        return ResponseEntity.created(createdUri).body(eventResource);
    }

    private  ResponseEntity badRequest(Errors errors){ // 추가
        return ResponseEntity.badRequest().body(new ErrorsResource(errors));
    }
}

  • return ResponseEntity.badRequest().body(new ErrorsResource(errors)); 에러 발생시 공통적으로 쓰이는 코드이기에 리팩토링해준다.

EventControllerTests 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// import 생략

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
@ActiveProfiles("test")
public class EventControllerTests {
    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @Test
    @TestDescription("입력값이 잘못된 경우에 에러가 발생하는 테스트")
    public void createEvent_Bad_Request_Wrong_Input() throws Exception{
        EventDto eventDto = EventDto.builder()
                .name("Spring")
                .description("REST API Development With Spring")
                .beginEnrollmentDateTime(LocalDateTime.of(2024,02,22,14,21))
                .closeEnrollmentDateTime(LocalDateTime.of(2024,02,21,14,21))
                .beginEventDateTime(LocalDateTime.of(2024,02,20,14,21))
                .endEventDateTime(LocalDateTime.of(2024,02,19,14,21))
                .basePrice(10000)
                .maxPrice(200)
                .limitOfEnrollment(100)
                .location("강남역 D2 스타트업 팩토리")
                .build();

        this.mockMvc.perform(post("/api/events")
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .content(this.objectMapper.writeValueAsString(eventDto))
                )
                .andDo(print())
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("content[0].objectName").exists()) // 수정 $[0] -> content[0]
                .andExpect(jsonPath("content[0].defaultMessage").exists()) // 수정
                .andExpect(jsonPath("content[0].code").exists()) // 수정
                .andExpect(jsonPath("_links.index").exists()) // 추가
        ;
    }
}

  • "_links.index"값이 Unwrapped되지 않고 content값 안으로 들어가기 때문에 위와 같이 코드를 다시 작성해준다.

결과 (성공)

image

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.