REST API(11) HATEOAS 적용(2) - 인덱스 만들기
구현 Package 및 Class
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 테스트 결과(성공)
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.class
의index()
메서드에 대한 링크("/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값 안으로 들어가기 때문에 위와 같이 코드를 다시 작성해준다.
결과 (성공)
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.