HATEOAS 원칙을 따르는 REST 표현(링크 생성 및 표현)을 쉽게 생성할 수 있는 몇 가지 API 제공
HATEOAS 를 사용하기 위해 @EnableEntityLinks, @EnableHypermediaSupport 와 같은
설정들을 직접 적용해야 하지만 Spring Boot 가 HATEOAS 와 관련된 주석을 자동 설정하여 바로 사용 가능
ResourceSupport -> RepresentationModel
Resource -> EntityModel
Resources -> CollectionModel
PagedResources -> PagedModel
ResourceAssembler -> RepresentationModelAssembler
객체에서 링크도 같이 반환하기 위해 RepresentationModel 을 상속받는 클래스 작성
@JsonUnwrapped : object -> json serialize 할 때 event 객체로 감싸지 않는다
public class EventResource extends RepresentationModel {
@JsonUnwrapped
private Event event;
public EventResource(Event event) {
this.event = event;
}
...
}
EntityModel 은 RepresentationModel 하위 클래스로 content(예시 : Event) 를
호출하는 getContent 메서드에 @JsonUnwrapped 주석이 선언되어 있어서
별도의 어노테이션 작업이 필요없어 진다
public class EventResource extends EntityModel<Event> {
public EventResource(Event event) {
super(event);
}
}
테스트
클라이언트에게 전달한 응답 본문에 link 가 전달되는지 확인하는 테스트 코드
@Test
@DisplayName("정상적으로 이벤트를 생성하는 테스트")
public void createEvent() throws Exception {
... EventDto 빌더 생략
mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(jsonPath("_links.query-events").exists())
.andExpect(jsonPath("_links.self").exists())
.andExpect(jsonPath("_links.update-event").exists());
}
RESTful 서비스 문서화를 도와주는 역할
Asciidoctor 로 작성한 문서와 Spring MVC Test로 생성된 스니펫을 결합
Swagger 에서는 적용할 수 없는 테스트를 강제화 할 수 있다
Test Class 의 클래스 레벨에 @AutoConfigurationRestDocs 주석을 달아주고
mockMvc.perform 응답에 .andDo(document("식별할 이름"));
위 내용을 아래와 같이 코드로 작성하고 실행하면 target 하위에 generated-snippets 폴더가 생성되고
그 아래에 snippet(확장자 : adoc) 들이 생성된다
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName("정상적으로 이벤트를 생성하는 테스트")
public void createEvent() throws Exception {
...EventDto 빌더 생략
mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(document("create-event"));
}
json data 가 한줄로 나오며 보기가 불편한데 보기 쉽게 커스텀할 수 있다
이외에 여러가지 커스텀 참조
테스트 패키지안에 테스트에서만 사용할 configuration 클래스 작성
@TestConfiguration : 테스트에서만 사용하는 Configuration
@TestConfiguration
public class RestDocsConfiguration {
@Bean
public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
return configurer -> {
configurer.operationPreprocessors().withRequestDefaults(prettyPrint());
configurer.operationPreprocessors().withResponseDefaults(prettyPrint());
};
}
}
등록 방법으로 테스트에서 사용하는 Configuration 클래스를 import 해주면 바로 적용된다
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@Import(RestDocsConfiguration.class)
public class EventControllerTests {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@Test
@DisplayName("정상적으로 이벤트를 생성하는 테스트")
public void createEvent() throws Exception {
...EventDto 빌더 생략
mockMvc.perform(post("/api/events/")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(status().isCreated())
.andDo(document("create-event"));
}
}
다른 방법으로는 빈으로 등록하지 않고 각 테스트마다 적용하는 방법도 있다
.andDo(document("create-event", Preprocessors.preprocessRequest(Preprocessors.prettyPrint())));
document 안에 links 와 request, response 의 헤더, 필드에 대한 내용을 적어주고 테스트를 실행하면
3-4 에서 말했던 경로 아래 작성한 개수만큼 snippet 이 생성된다
아래 코드를 보면 links 를 통해서 links 테스트를 진행했는데 responseFields 에도 link 가 포함되어 있어
다시 한번 테스트를 진행해야 하므로 작성해주었다
links 반복 테스트를 무시하는 방법으로 relaxedResponseFields 를 사용할 수 있는데 장단점은 아래와 같다
장점 : 문서 일부분만 테스트, 단점 : 정확한 문서를 작성하지 못함
.andDo(document("create-event",
links(
linkWithRel("query-events").description("link to query events"),
linkWithRel("self").description("link to self"),
linkWithRel("update-event").description("link to update event")
),
requestHeaders(
headerWithName("Content-Type").description("des content type"),
headerWithName("Accept").description("des accept")
),
requestFields(
fieldWithPath("name").description("des name"),
... 생략
),
responseHeaders(
headerWithName("Location").description("des location"),
headerWithName("Content-Type").description("des content type")
),
responseFields(
fieldWithPath("id").description("des id"),
... 생략
fieldWithPath("_links.query-events.href").description("des _links.query-events"),
fieldWithPath("_links.self.href").description("des _links.self"),
fieldWithPath("_links.update-event.href").description("des _links.update-event")
)
));
target-generated-docs(경로)-index.html(파일) : asciidoctor-maven-plugin 이 생성하는 파일
target-classes-static-docs(경로)-index.html(파일) : maven-resources-plugin 이 생성하는 파일
plugin 의 순서가 중요한데 이유는 plugin 안에 goals-goal 을 보면 첫번째 플러그인은
process-asciidoc 로 설정되어 있고 두번째 플러그인은 copy-resources 로 설정되어있다
첫번째 플러그인의 작업을 통해 생성한 파일을 두번째 플러그인이 복사한다
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.8</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.outputDirectory}/static/docs
</outputDirectory>
<resources>
<resource>
<directory>
${project.build.directory}/generated-docs
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
플러그인 추가하여 메이븐 패키징 후 html 파일 생성 확인
profile 을 추가하는 코드에서 이슈 발생 아래와 같이 문제 해결하고 테스트까지 진행
profile 관련 강의 영상에서 사용한 소스코드 버전 문제로 에러 발생
eventResource.add(new Link("/docs/index.html#resources-events-create").withRel("profile"));
Link(클래스) - of(정적 메서드) 호출하여 문제 해결
eventResource.add(Link.of("/docs/index.html#resources-events-create").withRel("profile"));
docker postgres image 올리고 어플리케이션에 적용
test/resources 에 application-test.properties 생성하고 H2 DB 설정
Test 클래스의 클래스 레벨에 @ActiveProfiles("test") 설정하여
중복되는 설정만 test properties 를 반영하고 나머지는 main 에 있는 properties 설정들 그대로 적용
특별한 내용 없어서 구체적인 내용은 깃헙
다른 리소스에 대한 진입 링크를 제공하는 인덱스 생성
Index Controller 추가
@RestController
public class IndexController {
@GetMapping("/api")
public RepresentationModel index() {
var index = new RepresentationModel();
index.add(linkTo(EventController.class).withRel("/events"));
return index;
}
}
Index Test Controller 추가
//... 주석 생략
public class IndexControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void index() throws Exception {
this.mockMvc.perform(get("/api"))
.andExpect(status().isOk())
.andExpect(jsonPath("_links.events").exists());
}
}
Error Resource 클래스 추가
public class ErrorsResource extends EntityModel<Errors> {
public ErrorsResource(Errors errors) {
super(errors);
add(linkTo(methodOn(IndexController.class).index()).withRel("index"));
}
}
EventController 코드 수정
ResponseEntity -> body 에 넣었던 Errors 객체를 ErrorsResource 객체에 넣고 링크포함 하여 body 에 전달
동일하게 사용되는 코드를 메서드로 분리하는 리팩토링 작업 진행
@PostMapping
public ResponseEntity createEvent(@RequestBody @Validated 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(); //free, offline 설정
...생략
}
private ResponseEntity badRequest(Errors errors) {
return ResponseEntity.badRequest().body(new ErrorsResource(errors));
}
이벤트 테스트에 코드 추가
@Test
@DisplayName("입력 값이 잘못된 경우에 에러가 발생하는 테스트")
public void createEvent_bad_request_wrong_valid() throws Exception {
//...EventDto 빌더 생략
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
//... 생략
//추가 코드
.andExpect(jsonPath("_links.index").exists());
}
인덱스 작업 후 이벤트 테스트 실행할 때 이슈 발생
jackson 라이브러리 버전문제로 Spring Boot 2.3 버전부터 jackson 은
json 을 생성할 때 Array 부터 생성하는 걸 허용하지 않음
이벤트 적용 전 후 json 데이터
before
[
{
"field":"basePrice",
"objectName":"eventDto",
"code":"wrongValue",
...
},
{
"field":"maxPrice",
"objectName":"eventDto","code"
...
}
]
after
{
"errors":[
{
"field":"basePrice",
"objectName":"eventDto"
...
},
{
"field":"maxPrice",
"objectName":"eventDto"
}
],
"_links":{
"index":{
"href":"http://localhost:8080/api"
}
}
}