Spring REST Docs 개요
- Spring REST Docs는 테스트 코드 기반으로 RESTful 서비스를 문서화 할 수 있게 도와주는 도구입니다.
- 기본적으로 Asciidoc을 사용하며 작성된 테스트 코드에 의해 html 파일을 생성해줍니다.
- 테스트로 자동 생성된 스니펫을 사용하여 문서를 생성합니다.
Spring REST Docs 채택 이유
- Swagger는 API 동작을 테스트 하기에는 좋으나 명료한 문서 제공용으로는 부족합니다.
- REST API 문서를 깔끔하게 제공하고 싶어서 swagger를 사용하다가 Spring REST Docs로 전환하게 되었습니다.
Spring REST Docs 적용하기
의존성 추가
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
RestDocsConfig
- 스니펫 결과를 보기 좋게 출력하기 위한 빈을 정의합니다.
@TestConfiguration
public class RestDocsConfig {
@Bean
public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
return configurer -> configurer.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint());
}
}
적용 전
{"name":"kg","exchangeUnitName":"g","exchangeQuantity":1000.0,"_links":{"self":{"href":"http://localhost:8080/units/kg"},"units-read":{"href":"http://localhost:8080/units"},"profile":{"href":"/docs/index.html#resources-units-create"}}}
적용 후
{
"name" : "kg",
"exchangeUnitName" : "g",
"exchangeQuantity" : 1000.0,
"_links" : {
"self" : {
"href" : "http://localhost:8080/units/kg"
},
"units-read" : {
"href" : "http://localhost:8080/units"
},
"profile" : {
"href" : "/docs/index.html#resources-units-create"
}
}
}
샘플 테스트 코드
- @AutoConfigureRestDocs 애노테이션을 추가해야 합니다.
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ActiveProfiles("test")
@Import({RestDocsConfig.class, EmbeddedRedisConfig.class})
@Ignore
public class ControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
public void When_단위_저장_Then_정상_리턴() throws Exception {
// Given
UnitRequest unitRequest = UnitRequest.builder()
.name("kg")
.exchangeUnitName("g")
.exchangeQuantity(1000D)
.build();
// When
final ResultActions actions = this.mockMvc.perform(post("/units")
.param("userId", "1001")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(this.objectMapper.writeValueAsString(unitRequest)));
// Then
actions.andDo(print())
.andExpect(status().isCreated())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE))
.andExpect(header().exists(HttpHeaders.LOCATION))
.andExpect(jsonPath("name").value(unitRequest.getName()))
.andExpect(jsonPath("exchangeUnitName").value(unitRequest.getExchangeUnitName()))
.andExpect(jsonPath("exchangeQuantity").value(unitRequest.getExchangeQuantity()))
.andExpect(jsonPath("_links.self").exists())
.andExpect(jsonPath("_links.units-read").exists())
.andExpect(jsonPath("_links.profile").exists())
.andDo(document("units-create",
links(
linkWithRel("self").description("현재 API"),
linkWithRel("units-read").description("단위 조회 API"),
linkWithRel("profile").description("프로파일 링크")
),
requestHeaders(
headerWithName(HttpHeaders.ACCEPT).description("Accept 헤더"),
headerWithName(HttpHeaders.CONTENT_TYPE).description("Content type 헤더")
),
requestFields(
fieldWithPath("name").description("단위 이름"),
fieldWithPath("exchangeUnitName").description("환산 단위"),
fieldWithPath("exchangeQuantity").description("환산 단위의 수량")
),
responseHeaders(
headerWithName(HttpHeaders.CONTENT_TYPE).description("Content type 헤더")
),
responseFields(
fieldWithPath("name").description("단위 이름"),
fieldWithPath("exchangeUnitName").description("환산 단위 이름"),
fieldWithPath("exchangeQuantity").description("환산 단위 수량"),
fieldWithPath("_links.self.href").description("현재 API"),
fieldWithPath("_links.units-read.href").description("단위 조회 API"),
fieldWithPath("_links.profile.href").description("프로파일 링크")
)
));
}
}
index.adoc
= REST API Guide
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
:operation-curl-request-title: Example request
:operation-http-response-title: Example response
[[overview]]
= 개요
[[overview-http-verbs]]
== HTTP 동사
본 REST API에서 사용하는 HTTP 동사(verbs)는 가능한한 표준 HTTP와 REST 규약을 따릅니다.
|===
| 동사 | 용례
| `GET`
| 리소스를 가져올 때 사용
| `POST`
| 새 리소스를 만들 때 사용
| `PUT`
| 기존 리소스를 수정할 때 사용
| `PATCH`
| 기존 리소스의 일부를 수정할 때 사용
| `DELETE`
| 기존 리소스를 삭제할 떄 사용
|===
[[overview-http-status-codes]]
== HTTP 상태 코드
본 REST API에서 사용하는 HTTP 상태 코드는 가능한한 표준 HTTP와 REST 규약을 따릅니다.
|===
| 상태 코드 | 용례
| `200 OK`
| 요청을 성공적으로 처리함
| `201 Created`
| 새 리소스를 성공적으로 생성함. 응답의 `Location` 헤더에 해당 리소스의 URI가 담겨있다.
| `204 No Content`
| 기존 리소스를 성공적으로 수정함.
| `400 Bad Request`
| 잘못된 요청을 보낸 경우. 응답 본문에 더 오류에 대한 정보가 담겨있다.
| `404 Not Found`
| 요청한 리소스가 없음.
|===
[[overview-errors]]
== 오류
에러 응답이 발생했을 때 (상태 코드 >= 400), 본문에 해당 문제를 기술한 JSON 객체가 담겨있다. 에러 객체는 다음의 구조를 따른다.
include::{snippets}/errors/response-fields.adoc[]
예를 들어, 잘못된 요청으로 레시피를 만들려고 했을 때 다음과 같은 `400 Bad Request` 응답을 받는다.
include::{snippets}/errors/http-response.adoc[]
[[overview-hypermedia]]
== 하이퍼미디어
본 REST API는 하이퍼미디어와 사용하며 응답에 담겨있는 리소스는 다른 리소스에 대한 링크를 가지고 있다.
응답은 http://stateless.co/hal_specification.html[Hypertext Application from resource to resource. Language (HAL)] 형식을 따른다.
링크는 `_links`라는 키로 제공한다. 본 API의 사용자(클라이언트)는 URI를 직접 생성하지 않아야 하며, 리소스에서 제공하는 링크를 사용해야 한다.
[[resources]]
= 리소스
[[resources-index]]
== 인덱스
인덱스는 서비스 진입점을 제공한다.
[[resources-index-access]]
=== 인덱스 조회
`GET` 요청을 사용하여 인덱스에 접근할 수 있다.
operation::index[snippets='request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-units]]
== 단위
단위 리소스는 단위를 만들거나 조회할 때 사용한다.
[[resources-units-create]]
=== 단위 생성
`POST` 요청을 사용해서 새 단위를 만들 수 있다.
operation::units-create[snippets='request-fields,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-units-read]]
=== 단위 조회
`GET` 요청을 사용해서 기존 단위 하나를 조회할 수 있다.
operation::units-read[snippets='path-parameters,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-materials]]
== 재료
재료 리소스는 재료를 만들거나 조회할 때 사용한다.
[[resources-materials-create]]
=== 재료 생성
`POST` 요청을 사용해서 새 재료를 만들 수 있다.
operation::materials-create[snippets='request-fields,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-materials-read]]
=== 재료 조회
`GET` 요청을 사용해서 기존 재료 하나를 조회할 수 있다.
operation::materials-read[snippets='path-parameters,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-materials-query]]
=== 재료 조회
`GET` 요청을 사용해서 기존 재료 하나를 조회할 수 있다.
operation::materials-query[snippets='request-parameters,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-recipes]]
== 레시피
레시피 리소스는 레시피를 만들거나 조회할 때 사용한다.
[[resources-recipes-create]]
=== 레시피 생성
`POST` 요청을 사용해서 새 레시피를 만들 수 있다.
operation::recipes-create[snippets='request-fields,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-recipes-read]]
=== 레시피 조회
`GET` 요청을 사용해서 기존 레시피 하나를 조회할 수 있다.
operation::recipes-read[snippets='path-parameters,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-recipes-update]]
=== 레시피 수정
`PUT` 요청을 사용해서 기존 레시피를 수정할 수 있다.
operation::recipes-update[snippets='request-fields,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-recipes-delete]]
=== 레시피 삭제
`DELETE` 요청을 사용해서 기존 레시피를 삭제할 수 있다.
operation::recipes-delete[snippets='path-parameters,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-recipes-query]]
=== 레시피 리스트 조회
`GET` 요청을 사용하여 서비스의 모든 레시피를 조회할 수 있다.
operation::recipes-query[snippets='request-parameters,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-recipes-count]]
=== 레시피 건 수 조회
`GET` 요청을 사용하여 서비스의 모든 레시피의 건 수를 조회할 수 있다.
operation::recipes-count[snippets='request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-recipes-readCount]]
=== 레시피 조회 수 증가
`PUT` 요청을 사용해서 레시피의 조회 수를 증가시킬 수 있다.
operation::recipes-readCount[snippets='path-parameters,request-headers,http-request,response-headers,curl-request,http-response,links']
[[resources-recipes-popular]]
=== 인기 레시피 리스트 조회
`GET` 요청을 사용하여 서비스의 모든 인기 레시피를 조회할 수 있다.
operation::recipes-popular[snippets='request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-members]]
== 회원
회원 리소스는 회원를 만들거나 조회할 때 사용한다.
[[resources-members-create]]
=== 회원 생성
`POST` 요청을 사용해서 새 레시피를 만들 수 있다.
operation::members-create[snippets='request-fields,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-members-read]]
=== 회원 조회
`GET` 요청을 사용해서 기존 회원 하나를 조회할 수 있다.
operation::members-read[snippets='path-parameters,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
[[resources-members-update]]
=== 회원 수정
`PUT` 요청을 사용해서 기존 회원을 수정할 수 있다.
operation::members-update[snippets='request-fields,request-headers,http-request,response-fields,response-headers,curl-request,http-response,links']
완성된 REST Docs 문서
