토리맘의 한글라이즈 프로젝트 logo 토리맘의 한글라이즈 프로젝트

스프링 REST Docs 공식 레퍼런스를 한글로 번역한 문서입니다.

전체 목차는 여기에 있습니다.

목차


이번 섹션에서는 스프링 REST Docs로 API를 문서화하는 방법에 대해 더 자세히 설명한다.


3.1. Hypermedia

스프링 Rest Docs를 사용하면 하이퍼미디어 기반 API에서의 링크도 문서화할 수 있다. 다음 예제는 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("index", links( // (1)
			linkWithRel("alpha").description("Link to the alpha resource"), // (2) 
			linkWithRel("bravo").description("Link to the bravo resource")))); // (3) 
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
	.expectStatus().isOk().expectBody()
	.consumeWith(document("index",links( // (1)
			linkWithRel("alpha").description("Link to the alpha resource"), // (2)
			linkWithRel("bravo").description("Link to the bravo resource")))); // (3)
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("index", links( // (1)
			linkWithRel("alpha").description("Link to the alpha resource"), // (2)
			linkWithRel("bravo").description("Link to the bravo resource")))) // (3)
	.get("/").then().assertThat().statusCode(is(200));

(1) 응답에 있는 링크를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 스태틱 메소드 links를 사용한다.
(2) relalpha인 링크가 있는지 검증한다. org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 스태틱 메소드 linkWithRel을 사용한다.
(3) relbravo인 링크가 있는지 검증한다.

(1) 응답에 있는 링크를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 스태틱 메소드 links를 사용한다.
(2) relalpha인 링크가 있는지 검증한다. org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 스태틱 메소드 linkWithRel을 사용한다.
(3) relbravo인 링크가 있는지 검증한다.

(1) 응답에 있는 링크를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 스태틱 메소드 links를 사용한다.
(2) relalpha인 링크가 있는지 검증한다. org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 스태틱 메소드 linkWithRel을 사용한다.
(3) relbravo인 링크가 있는지 검증한다.

코드를 실행하면 links.adoc이란 스니펫을 만들며, 이 스니펫은 리소스 링크를 설명하는 테이블을 가지고 있다.

응답에서 title이 있는 링크는 descriptor 설명을 생략할 수 있으며, 이땐 title을 사용한다. title이 없는 링크의 설명을 생략하면 실패한다.

링크를 문서화할 땐, 응답에 있는 모든 링크를 작성하지 않으면 테스트는 실패한다. 마찬가지로 문서화한 링크가 응답에 없을 땐, 해당 링크를 선택 사항으로 마킹하지 않았다면 테스트는 실패한다.

링크를 문서화하고 싶지 않다면 무시하도록 마킹해도 된다. 이렇게하면 위에서 언급한 테스트 실패를 방지하고, 만들어진 스니펫에서도 제외할 수 있다.

모든 링크를 문서화하지 않아도 테스트가 실패하지 않도록 완화된 모드로 링크를 문서화할 수도 있다. 이렇게 하려면 org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 relaxedLinks 메소드를 사용해라. 일부 링크만 중요한 특정 시나리오를 문서화하기 유용하다.

스프링 Rest Docs는 기본적으로 두 가지 형태의 링크를 처리한다:

Atom이나 HAL 형식 링크를 사용하는데 컨텐츠 타입이 다르다면, links에 빌트인 LinkExtractor 구현체 중 하나를 제공하면 된다. 다음 예제는 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
.andDo(document("index", links(halLinks(), // (1)
		linkWithRel("alpha").description("Link to the alpha resource"),
		linkWithRel("bravo").description("Link to the bravo resource"))));
.consumeWith(document("index",links(halLinks(), // (1)
		linkWithRel("alpha").description("Link to the alpha resource"),
		linkWithRel("bravo").description("Link to the bravo resource"))));
.filter(document("index", links(halLinks(), // (1)
		linkWithRel("alpha").description("Link to the alpha resource"),
		linkWithRel("bravo").description("Link to the bravo resource"))))

(1) HAL 형식 링크를 사용한다고 알려준다. org.springframework.restdocs.hypermedia.HypermediaDocumentation에 있는 스태틱 메소드 halLinks를 사용한다.

API가 Atom도 HAL도 아닌 다른 형식으로 링크를 표현하고 있다면, LinkExtractor 인터페이스를 직접 구현해서 응답에서 링크를 추출하면 된다.

HAL을 사용할 땐 self, curies같이 모든 응답에 들어있는 링크를 문서화하는 대신, 개요 섹션에 한 번만 문서화하고 나머지 API 문서에선 공통 링크를 무시하고 싶을 수 있다. 이럴 때 스니펫 재사용 기능을 활용하면 미리 특정 링크를 무시하게끔 스니펫을 설정해두고, 필요할 때 링크 descriptor를 추가할 수 있다. 다음 예제를 참고해라:

public static LinksSnippet links(LinkDescriptor... descriptors) {
	return HypermediaDocumentation.links(linkWithRel("self").ignored().optional(),
			linkWithRel("curies").ignored()).and(descriptors);
}

3.2. Request and Response Payloads

앞서 설명한 하이퍼미디어 전용 기능 외에도, 일반적인 요청/응답 페이로드 문서도 작성할 수 있다.

기본적으로 스프링 Rest Docs는 요청과 응답 바디를 위한 스니펫을 자동으로 만들어준다. 각 스니펫 이름은 request-body.adocresponse-body.adoc이다.

3.2.1. Request and Response Fields

요청, 응답 페이로드 문서를 좀 더 자세히 작성하고 싶다면, 페이로드 필드를 문서화할 수 있다.

아래 페이로드를 생각해 보자:

{
	"contact": {
		"name": "Jane Doe",
		"email": "jane.doe@example.com"
	}
}

이 예제에 있는 필드는 다음과 같이 문서화할 수 있다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk())
		.andDo(document("index",
				responseFields( // (1)
						fieldWithPath("contact.email")
								.description("The user's email address"), // (2)
				fieldWithPath("contact.name").description("The user's name")))); // (3)
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("user",
		responseFields( // (1)
			fieldWithPath("contact.email").description("The user's email address"), // (2)
			fieldWithPath("contact.name").description("The user's name")))); // (3)
RestAssured.given(this.spec).accept("application/json")
	.filter(document("user", responseFields( // (1)
			fieldWithPath("contact.name").description("The user's name"), // (2)
			fieldWithPath("contact.email").description("The user's email address")))) // (3)
	.when().get("/user/5")
	.then().assertThat().statusCode(is(200));

(1) 응답 페이로드에 있는 필드를 설명하는 스니펫을 만들도록 설정한다. 요청을 문서화할 땐 requestFields를 사용하면 된다. 두 스태틱 메소드는 모두 org.springframework.restdocs.payload.PayloadDocumentation에 있다.
(2) contact.email 패스에 필드가 있는지 검증한다. org.springframework.restdocs.payload.PayloadDocumentation에 있는 스태틱 메소드 fieldWithPath를 사용한다.
(3) contact.name 패스에 필드가 있는지 검증한다.

결과로 만들어지는 스니펫엔 필드를 설명하는 테이블이 추가된다. 요청 문서에서 해당 스니펫 이름은 request-fields.adoc, 응답은 response-fields.adoc이다.

필드를 문서화할 땐, 페이로드에 있는 모든 필드를 작성하지 않으면 테스트는 실패한다. 마찬가지로 문서화한 필드가 페이로드에 없을 땐, 해당 필드를 선택 사항으로 마킹하지 않았다면 테스트는 실패한다.

문서에 모든 필드를 상세하게 적고 싶지 않다면 하위 패스를 하나로 묶어서 문서화하는 것도 가능하다. 다음은 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk())
		.andDo(document("index",
				responseFields( 
						subsectionWithPath("contact")
								.description("The user's contact details")))); // (1)
this.webTestClient.get().uri("user/5").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("user",
		responseFields(
			subsectionWithPath("contact").description("The user's contact details")))); // (1)
RestAssured.given(this.spec).accept("application/json")
	.filter(document("user", responseFields(
			subsectionWithPath("contact").description("The user's contact details")))) // (1)
	.when().get("/user/5")
	.then().assertThat().statusCode(is(200));

(1) contact 패스 하위 섹션을 문서화한다. 이렇게 하면 contact.emailcontact.name도 문서화한다고 보면 된다. org.springframework.restdocs.payload.PayloadDocumentation에 있는 스태틱 메소드 subsectionWithPath를 사용한다.

subsectionWithPath는 특정 페이로드 섹션에 대한 개요를 제공하는 식으로 활용할 수 있다. 그런 다음 하위 섹션은 별도로 더 자세히 문서화해도 된다. 요청, 응답 페이로드 하위 섹션 문서 작성하기를 참고해라.

필드나 하위 섹션을 아예 문서화하고 싶지 않다면 무시하도록 마킹해도 된다. 이렇게하면 위에서 언급한 테스트 실패를 방지하고, 만들어진 스니펫에서도 제외할 수 있다.

모든 필드를 문서화하지 않아도 테스트가 실패하지 않도록 완화된 모드로 필드를 문서화할 수도 있다. 이렇게 하려면 org.springframework.restdocs.payload.PayloadDocumentation에 있는 메소드 relaxedRequestFields, relaxedResponseFields를 사용해라. 페이로드 일부만 중요한 특정 시나리오를 문서화하기 유용하다.

스프링 Rest Docs는 디폴트로 문서화하는 페이로드가 JSON이라고 가정한다. XML 페이로드를 문서화하려면 반드시 요청이나 응답 컨텐츠 타입이 application/xml과 호환돼야 한다.

Fields in JSON Payloads

이번 섹션에선 JSON 페이로드 필드를 다루는 방법을 설명한다.

JSON Field Paths

JSON 필드 패스는 점 표기법(dot notation)이나 괄호 표기법(bracket notation)을 사용한다. 점 표기법은 패스 안에서 ‘.’으로 키를 구분한다 (예를 들어 a.b). 괄호 표기법은 각 키를 꺽쇠 괄호와 작은 따옴표로 감싼다 (예를 들어 ['a']['b']). 두 표기법 모두 배열은 []로 식별한다. 점 표기법이 좀 더 간결하긴 하지만, 괄호 표기법에서도 키 이름 안에선 .을 사용해도 된다. 두 가지 표기법을 같이 사용해도 된다 (예를 들어 a['b']).

아래 JSON 페이로드를 생각해 보자:

{
	"a":{
		"b":[
			{
				"c":"one"
			},
			{
				"c":"two"
			},
			{
				"d":"three"
			}
		],
		"e.dot" : "four"
	}
}

위 JSON 페이로드에는 다음과 같은 패스가 존재한다:

Path Value
a b를 가지고 있는 객체
a.b 객체 세 개가 들어있는 배열
['a']['b'] 객체 세 개가 들어있는 배열
a['b'] 객체 세 개가 들어있는 배열
['a'].b 객체 세 개가 들어있는 배열
a.b[] 객체 세 개가 들어있는 배열
a.b[].c 문자열 one, two가 들어있는 배열
a.b[].d 문자열 three
a['e.dot'] 문자열 four
['a']['e.dot'] 문자열 four

루트가 배열인 페이로드도 문서화할 수 있다. [] 패스가 전체 배열을 가리킨다. 배열 엔트리 내 필드는 괄호나 점 표기법으로 구분하면 된다. 예를 들어 아래 배열에서 [].id는 모든 객체의 id 필드를 가리킨다:

[
	{
		"id":1
	},
	{
		"id":2
	}
]

이름이 다른 필드를 한 번에 매칭하고 싶으면 와일드카드 *를 사용할 수 있다. 예를 들어 아래 JSON에서 모든 사용자의 role을 문서화할 땐 users.*.role을 사용하면 된다:

{
	"users":{
		"ab12cd34":{
			"role": "Administrator"
		},
		"12ab34cd":{
			"role": "Guest"
		}
	}
}
JSON Field Types

필드를 문서화할 때 스프링 Rest Docs는 페이로드를 확인해서 필드 타입을 결정한다. 세 가지 필드 타입을 지원한다:

Type Description
array 필드에 사용한 값이 모두 배열일 때
boolean 필드에 사용한 값이 모두 boolean일 때 (true, false)
object 필드에 사용한 값이 모두 객체일 때
number 필드에 사용한 값이 모두 숫자일 때
null 필드에 사용한 값이 모두 null일 때
string 필드에 사용한 값이 모두 문자열일 때
varies 페이로드 내에서 필드를 각기 다른 타입으로 여러 번 사용하는 경우

FieldDescriptortype(Object) 메소드로 직접 타입을 설정해도 된다. 문서화할 땐 넘겨준 Object에서 toString 메소드를 호출한다. 전형적으로는 JsonFieldType enum 값 중 하나를 사용한다. 다음 예제는 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
.andDo(document("index",
		responseFields(
				fieldWithPath("contact.email").type(JsonFieldType.STRING) // (1)
						.description("The user's email address"))));
.consumeWith(document("user",
	responseFields(
		fieldWithPath("contact.email")
			.type(JsonFieldType.STRING) // (1)
			.description("The user's email address"))));
.filter(document("user", responseFields(
		fieldWithPath("contact.email")
				.type(JsonFieldType.STRING) // (1)
				.description("The user's email address"))))

(1) 필드 타입을 String으로 설정한다.

XML payloads

이번 섹션에선 XML 페이로드를 다루는 방법을 설명한다.

XML Field Paths

XML 필드 패스는 XPath로 표현한다. 자식 노드로 내려갈 땐 /를 사용한다.

XML Field Types

XML 페이로드를 문서화할 땐 반드시 FieldDescriptor에 있는 type(Object) 메소드로 필드 타입을 지정해야 한다. 지정한 타입에서 toString 메소드를 호출한 결과로 문서화한다.

Reusing Field Descriptors

범용적인 스니펫 재사용 기능과 더불어, 기존 descriptor에 프리픽스를 추가해서 스니펫을 만들 수도 있다. 이렇게 하면 요청이나 응답 페이로드에서 반복되는 descriptor를 한 번만 생성해 재활용할 수 있다.

책 정보를 반환하는 엔드포인트를 생각해 보자:

{
	"title": "Pride and Prejudice",
	"author": "Jane Austen"
}

title, author의 패스는 각각 titleauthor다.

이제 책 정보 배열을 반환하는 엔드포인트로 넘어가 보자:

[{
	"title": "Pride and Prejudice",
	"author": "Jane Austen"
},
{
	"title": "To Kill a Mockingbird",
	"author": "Harper Lee"
}]

title, author의 패스는 각각 [].title[].author다. 책 한 권과 배열의 유일한 차이점은 이제 필드 패스에 []. 프리픽스가 추가됐다는 점이다.

책 정보를 문서화하는 descriptor는 다음과 같이 만들 수 있다:

FieldDescriptor[] book = new FieldDescriptor[] {
		fieldWithPath("title").description("Title of the book"),
		fieldWithPath("author").description("Author of the book") };

이제 이걸 사용해서 책 한 권에 대한 API를 문서화하면 된다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/books/1").accept(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk()).andDo(document("book", responseFields(book))); // (1)
this.webTestClient.get().uri("/books/1").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("book",
		responseFields(book))); // (1)
RestAssured.given(this.spec).accept("application/json")
	.filter(document("book", responseFields(book))) // (1)
	.when().get("/books/1")
	.then().assertThat().statusCode(is(200));

(1) 기존 descriptor를 사용해서 titleauthor를 문서화한다.

같은 descriptor로 책 배열도 문서화할 수 있다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/books").accept(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk())
		.andDo(document("book",
				responseFields(
						fieldWithPath("[]").description("An array of books")) // (1)
								.andWithPrefix("[].", book))); // (2)
this.webTestClient.get().uri("/books").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("books",
		responseFields(
			fieldWithPath("[]")
				.description("An array of books")) // (1)
				.andWithPrefix("[].", book))); // (2)
RestAssured.given(this.spec).accept("application/json")
	.filter(document("books", responseFields(
		fieldWithPath("[]").description("An array of books")) // (1)
		.andWithPrefix("[].", book))) // (2)
	.when().get("/books")
	.then().assertThat().statusCode(is(200));

(1) 배열을 문서화한다.
(2) 기존 descriptor에 []. 프리픽스를 달아 [].title[].author를 문서화한다.

3.2.2. Documenting a Subsection of a Request or Response Payload

페이로드가 크거나 구조가 복잡하다면 페이로드를 섹션별로 문서화하는 것도 좋다. Rest Docs를 사용하면 페이로드의 하위 섹션을 추출해서 따로 문서화할 수 있다.

Documenting a Subsection of a Request or Response Body

아래 JSON 응답 바디를 생각해 보자:

{
	"weather": {
		"wind": {
			"speed": 15.3,
			"direction": 287.0
		},
		"temperature": {
			"high": 21.2,
			"low": 14.8
		}
	}
}

temperature 객체를 문서화하는 스니펫은 다음과 같이 만들 수 있다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk()).andDo(document("location",
				responseBody(beneathPath("weather.temperature")))); // (1)
this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("temperature",
		responseBody(beneathPath("weather.temperature")))); // (1)
RestAssured.given(this.spec).accept("application/json")
	.filter(document("location", responseBody(beneathPath("weather.temperature")))) // (1)
	.when().get("/locations/1")
	.then().assertThat().statusCode(is(200));

(1) 응답 바디의 하위 섹션만 가지고 있는 스니펫을 만든다. org.springframework.restdocs.payload.PayloadDocumentation에 있는 스태틱 메소드 responseBodybeneathPath를 사용한다. 요청 바디를 위한 스니펫은 responseBody 대신 requestBody를 사용하면 된다.

결과로 만들어지는 스니펫은 다음 컨텐츠를 가지고 있다:

{
	"temperature": {
		"high": 21.2,
		"low": 14.8
	}
}

스니펫 이름은 하위 섹션 식별자로 구분한다. 기본적으로 beneath-${path}를 식별자로 사용한다. 예를 들어 이전 코드는 response-body-beneath-weather.temperature.adoc이라는 스니펫을 만든다. 이 식별자는 다음과 같이 withSubsectionId(String) 메소드로 커스텀할 수 있다:

responseBody(beneathPath("weather.temperature").withSubsectionId("temp"));

이제 request-body-temp.adoc이란 스니펫을 만든다.

Documenting the Fields of a Subsection of a Request or Response

요청/응답 바디 하위 섹션을 문서화할 수 있듯, 특정 섹션에 있는 필드만 문서화하는 것도 가능하다. temperature 객체의 필드(high, low)를 문서화하는 스니펫은 다음과 같이 만들 수 있다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/locations/1").accept(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk())
		.andDo(document("location",
				responseFields(beneathPath("weather.temperature"), // (1)
						fieldWithPath("high").description(
								"The forecast high in degrees celcius"), // (2) 
				fieldWithPath("low")
						.description("The forecast low in degrees celcius"))));
this.webTestClient.get().uri("/locations/1").accept(MediaType.APPLICATION_JSON)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("temperature",
		responseFields(beneathPath("weather.temperature"), // (1)
			fieldWithPath("high").description("The forecast high in degrees celcius"), // (2) 
			fieldWithPath("low").description("The forecast low in degrees celcius"))));
RestAssured.given(this.spec).accept("application/json")
	.filter(document("location", responseFields(beneathPath("weather.temperature"), // (1)
		fieldWithPath("high").description("The forecast high in degrees celcius"), // (2) 
		fieldWithPath("low").description("The forecast low in degrees celcius"))))
	.when().get("/locations/1")
	.then().assertThat().statusCode(is(200));

(1) 응답 페이로드에서 weather.temperature 패스 밑에 있는 섹션 필드를 설명하는 스니펫을 만든다. org.springframework.restdocs.payload.PayloadDocumentation에 있는 스태틱 메소드 beneathPath를 사용한다.
(2) highlow 필드를 문서화한다.

결과로 만들어지는 스니펫엔 weather.temperaturehigh, low 필드를 설명하는 테이블이 있다. 스니펫 이름은 하위 섹션 식별자로 구분한다. 기본적으로 beneath-${path}를 식별자로 사용한다. 예를 들어 이전 코드는 response-fields-beneath-weather.temperature.adoc이란 스니펫을 만든다.


3.3. Request Parameters

요청 파라미터는 requestParameters로 문서화할 수 있다. 요청 파라미터는 GET 요청의 쿼리 스트링으로 추가한다. 다음은 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/users?page=2&per_page=100")) // (1)
	.andExpect(status().isOk())
	.andDo(document("users", requestParameters( // (2)
			parameterWithName("page").description("The page to retrieve"), // (3)
			parameterWithName("per_page").description("Entries per page") // (4)
	)));
this.webTestClient.get().uri("/users?page=2&per_page=100") // (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("users", requestParameters( // (2)
			parameterWithName("page").description("The page to retrieve"), // (3)
			parameterWithName("per_page").description("Entries per page") // (4)
	)));
RestAssured.given(this.spec)
	.filter(document("users", requestParameters( // (1)
			parameterWithName("page").description("The page to retrieve"), // (2)
			parameterWithName("per_page").description("Entries per page")))) // (3)
	.when().get("/users?page=2&per_page=100") // (4)
	.then().assertThat().statusCode(is(200));

(1) 쿼리 스트링에 두 파라미터 page, per_page를 사용해서 GET 요청을 수행한다.
(2) 요청 파라미터를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 requestParameters를 사용한다.
(3) page 파라미터를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 parameterWithName을 사용한다.
(4) per_page 파라미터를 문서화한다.

(1) 쿼리 스트링에 두 파라미터 page, per_page를 사용해서 GET 요청을 수행한다.
(2) 요청 파라미터를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 requestParameters를 사용한다.
(3) page 파라미터를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 parameterWithName을 사용한다.
(4) per_page 파라미터를 문서화한다.

(1) 요청 파라미터를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 requestParameters를 사용한다.
(2) page 파라미터를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 parameterWithName을 사용한다.
(3) per_page 파라미터를 문서화한다.
(4) 쿼리 스트링에 두 파라미터 page, per_page를 사용해서 GET 요청을 수행한다.

요청 파라미터를 POST 요청 바디에 폼 데이터로 넣어도 된다. 다음은 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(post("/users").param("username", "Tester")) // (1)
	.andExpect(status().isCreated())
	.andDo(document("create-user", requestParameters(
			parameterWithName("username").description("The user's username")
	)));
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("username", "Tester");
this.webTestClient.post().uri("/users").body(BodyInserters.fromFormData(formData)) // (1)
	.exchange().expectStatus().isCreated().expectBody()
	.consumeWith(document("create-user", requestParameters(
		parameterWithName("username").description("The user's username")
)));
RestAssured.given(this.spec)
	.filter(document("create-user", requestParameters(
			parameterWithName("username").description("The user's username"))))
	.formParam("username", "Tester") // (1)
	.when().post("/users") // (2)
	.then().assertThat().statusCode(is(200));

(1) username 파라미터 하나를 사용해 POST 요청을 수행한다.

(1) username 파라미터 하나를 사용해 POST 요청을 수행한다

(1) username 파라미터를 설정한다.
(1) POST 요청을 수행한다.

어떤 방법으로 테스트하든, request-parameters.adoc이란 스니펫이 만들어지며, 이 스니펫은 리소스가 지원하는 파라미터를 설명하는 테이블을 가지고 있다. 여기서도 마찬가지로 문서화한 요청 파라미터가 실제 요청에 없을 땐, 해당 파라미터를 선택 사항으로 마킹하지 않았다면 테스트는 실패한다.

요청 파라미터를 문서화하고 싶지 않다면 무시하도록 마킹해도 된다. 이렇게하면 위에서 언급한 테스트 실패를 방지하고, 만들어진 스니펫에서도 제외할 수 있다.

모든 요청 파라미터를 문서화하지 않아도 테스트가 실패하지 않도록 완화된 모드로 파라미터를 문서화할 수도 있다. 이렇게 하려면 org.springframework.restdocs.request.RequestDocumentation에 있는 relaxedRequestParameters 메소드를 사용해라. 일부 요청 파라미터만 중요한 특정 시나리오를 문서화하기 유용하다.


3.4. Path Parameters

요청의 패스 파라미터는 pathParameters로 문서화할 수 있다. 다음은 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) // (1)
	.andExpect(status().isOk())
	.andDo(document("locations", pathParameters( // (2)
			parameterWithName("latitude").description("The location's latitude"), // (3)
			parameterWithName("longitude").description("The location's longitude") // (4)
	)));
this.webTestClient.get().uri("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("locations",
		pathParameters( // (2)
			parameterWithName("latitude").description("The location's latitude"), // (3)
			parameterWithName("longitude").description("The location's longitude")))); // (4)
RestAssured.given(this.spec)
	.filter(document("locations", pathParameters( // (1)
			parameterWithName("latitude").description("The location's latitude"), // (2)
			parameterWithName("longitude").description("The location's longitude")))) // (3)
	.when().get("/locations/{latitude}/{longitude}", 51.5072, 0.1275) // (4)
	.then().assertThat().statusCode(is(200));

(1) 두 패스 파라미터 latitude, longitude를 사용해서 GET 요청을 수행한다.
(2) 요청 패스 파라미터를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 pathParameters를 사용한다.
(3) latitude 파라미터를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 parameterWithName을 사용한다.
(4) longitude 파라미터를 문서화한다.

(1) 두 패스 파라미터 latitude, longitude를 사용해서 GET 요청을 수행한다.
(2) 요청 패스 파라미터를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 pathParameters를 사용한다.
(3) latitude 파라미터를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 parameterWithName을 사용한다.
(4) longitude 파라미터를 문서화한다.

(1) 요청 패스 파라미터를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 pathParameters를 사용한다. (2) latitude 파라미터를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 parameterWithName을 사용한다.
(3) longitude 파라미터를 문서화한다
(4) 두 패스 파라미터 latitude, longitude를 사용해서 GET 요청을 수행한다.

path-parameters.adoc이란 스니펫이 만들어지며, 이 스니펫은 리소스가 지원하는 패스 파라미터를 설명하는 테이블을 가지고 있다.

MockMvc로 패스 파라미터를 문서화한다면, MockMvcRequestBuilders 대신 RestDocumentationRequestBuilders에 있는 메소드 중 하나로 요청을 빌드해야 한다.

패스 파라미터를 문서화할 땐, 요청에 있는 모든 패스 파라미터를 작성하지 않으면 테스트는 실패한다. 마찬가지로 문서화한 패스 파라미터가 요청에 없을 땐, 해당 패스 파라미터를 선택 사항으로 마킹하지 않았다면 테스트는 실패한다.

모든 파라미터를 문서화하지 않아도 테스트가 실패하지 않도록 완화된 모드로 패스 파라미터를 문서화할 수도 있다. 이렇게 하려면 org.springframework.restdocs.request.RequestDocumentation에 있는 relaxedPathParameters 메소드를 사용해라. 일부 패스 파라미터만 중요한 특정 시나리오를 문서화하기 유용하다.

패스 파라미터를 문서화하고 싶지 않다면 무시하도록 마킹해도 된다. 이렇게하면 위에서 언급한 테스트 실패를 방지하고, 만들어진 스니펫에서도 제외할 수 있다.


3.5. Request Parts

멀티파트 요청의 part는 requestParts로 문서화할 수 있다. 다음은 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(multipart("/upload").file("file", "example".getBytes())) // (1)
	.andExpect(status().isOk())
	.andDo(document("upload", requestParts( // (2)
			partWithName("file").description("The file to upload")) // (3)
));
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
multipartData.add("file", "example".getBytes());
this.webTestClient.post().uri("/upload").body(BodyInserters.fromMultipartData(multipartData)) // (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("upload", requestParts( // (2)
		partWithName("file").description("The file to upload")) // (3)
));
RestAssured.given(this.spec)
	.filter(document("users", requestParts( // (1)
			partWithName("file").description("The file to upload")))) // (2)
	.multiPart("file", "example") // (3)
	.when().post("/upload") // (4)
	.then().statusCode(is(200));

(1) file이란 이름을 가진 part 하나로 POST 요청을 수행한다.
(2) 요청의 part를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 requestParts를 사용한다.
(3) file part를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 partWithName을 사용한다.

(1) file이란 이름을 가진 part 하나로 POST 요청을 수행한다.
(2) 요청의 part를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 requestParts를 사용한다.
(3) file part를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 partWithName을 사용한다.

(1) 요청의 part를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 requestParts를 사용한다.
(2) file part를 문서화한다. org.springframework.restdocs.request.RequestDocumentation에 있는 스태틱 메소드 partWithName을 사용한다.
(3) 요청에 file이란 이름을 가진 part를 설정한다.
(4) /uploadPOST 요청을 보낸다.

request-parts.adoc이란 스니펫이 만들어지며, 이 스니펫은 리소스가 지원하는 요청 part를 설명하는 테이블을 가지고 있다.

요청 part를 문서화할 땐, 요청에 있는 모든 part를 작성하지 않으면 테스트는 실패한다. 마찬가지로 문서화한 part가 요청에 없을 땐, 해당 part를 선택 사항으로 마킹하지 않았다면 테스트는 실패한다.

모든 part를 문서화하지 않아도 테스트가 실패하지 않도록 완화된 모드로 요청 part를 문서화할 수도 있다. 이렇게 하려면 org.springframework.restdocs.request.RequestDocumentation에 있는 relaxedRequestParts 메소드를 사용해라. 일부 요청 part만 중요한 특정 시나리오를 문서화하기 유용하다.

요청 part를 문서화하고 싶지 않다면 무시하도록 마킹해도 된다. 이렇게하면 위에서 언급한 테스트 실패를 방지하고, 만들어진 스니펫에서도 제외할 수 있다.


3.6. Request Part Payloads

요청 part의 바디와 해당 필드도 지원하므로, 요청 part의 페이로드도 요청 페이로드와 거의 동일한 방식으로 문서화할 수 있다.

3.6.1. Documenting a Request Part’s Body

요청 part 바디를 가진 스니펫은 다음과 같이 만들 수 있다:

MockMvc WebTestClient REST Assured
MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png",
		"<<png data>>".getBytes());
MockMultipartFile metadata = new MockMultipartFile("metadata", "",
		"application/json", "{ \"version\": \"1.0\"}".getBytes());

this.mockMvc.perform(fileUpload("/images").file(image).file(metadata)
			.accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("image-upload", requestPartBody("metadata"))); // (1)
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
Resource imageResource = new ByteArrayResource("<<png data>>".getBytes()) {

	@Override
	public String getFilename() {
		return "image.png";
	}

};
multipartData.add("image", imageResource);
multipartData.add("metadata", Collections.singletonMap("version",  "1.0"));

this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData))
	.accept(MediaType.APPLICATION_JSON).exchange()
	.expectStatus().isOk().expectBody()
	.consumeWith(document("image-upload",
			requestPartBody("metadata"))); // (1)
Map<String, String> metadata = new HashMap<>();
metadata.put("version", "1.0");
RestAssured.given(this.spec).accept("application/json")
	.filter(document("image-upload", requestPartBody("metadata"))) // (1)
	.when().multiPart("image", new File("image.png"), "image/png")
			.multiPart("metadata", metadata).post("images")
	.then().assertThat().statusCode(is(200));

(1) metadata란 이름을 가진 요청 part 바디를 스니펫으로 만들도록 설정한다. PayloadDocumentation에 있는 스태틱 메소드 requestPartBody를 사용한다.

결과로 만들어지는 스니펫은 request-part-${part-name}-body.adoc이며, part의 바디를 가지고 있다. 예를 들어 metadata란 이름의 part를 문서화하면 request-part-metadata-body.adoc이란 스니펫을 만든다.

3.6.2. Documenting a Request Part’s Fields

MockMvc WebTestClient REST Assured
MockMultipartFile image = new MockMultipartFile("image", "image.png", "image/png",
		"<<png data>>".getBytes());
MockMultipartFile metadata = new MockMultipartFile("metadata", "",
		"application/json", "{ \"version\": \"1.0\"}".getBytes());

this.mockMvc.perform(fileUpload("/images").file(image).file(metadata)
			.accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("image-upload", requestPartFields("metadata", // (1)
			fieldWithPath("version").description("The version of the image")))); // (2)
MultiValueMap<String, Object> multipartData = new LinkedMultiValueMap<>();
Resource imageResource = new ByteArrayResource("<<png data>>".getBytes()) {

	@Override
	public String getFilename() {
		return "image.png";
	}

};
multipartData.add("image", imageResource);
multipartData.add("metadata", Collections.singletonMap("version",  "1.0"));
this.webTestClient.post().uri("/images").body(BodyInserters.fromMultipartData(multipartData))
	.accept(MediaType.APPLICATION_JSON).exchange()
	.expectStatus().isOk().expectBody()
	.consumeWith(document("image-upload",
		requestPartFields("metadata", // (1)
			fieldWithPath("version").description("The version of the image")))); // (2)
Map<String, String> metadata = new HashMap<>();
metadata.put("version", "1.0");
RestAssured.given(this.spec).accept("application/json")
	.filter(document("image-upload", requestPartFields("metadata", // (1)
			fieldWithPath("version").description("The version of the image")))) // (2)
	.when().multiPart("image", new File("image.png"), "image/png")
			.multiPart("metadata", metadata).post("images")
	.then().assertThat().statusCode(is(200));

(1) metadata라는 요청 part 페이로드에 있는 필드들을 설명하는 스니펫을 만들도록 설정한다. PayloadDocumentation에 있는 스태틱 메소드 requestPartFields를 사용한다.
(2) version 패스에 필드가 있는지 검증한다. org.springframework.restdocs.payload.PayloadDocumentation에 있는 스태틱 메소드 fieldWithPath를 사용한다.

결과로 만들어지는 스니펫은 part 필드를 설명하는 테이블을 가지고 있다. 스니펫 이름은 request-part-${part-name}-fields.adoc이 된다. 예를 들어 metadata란 part를 문서화하면 request-part-metadata-fields.adoc이란 스니펫이 생긴다.

필드를 문서화할 땐, part 페이로드에 있는 모든 필드를 작성하지 않으면 테스트는 실패한다. 마찬가지로 문서화한 필드가 part 페이로드에 없을 땐, 해당 필드를 선택 사항으로 마킹하지 않았다면 테스트는 실패한다. 계층 구조를 쓰는 페이로드는 필드 하나만 작성해도 하위 필드도 문서화한 것으로 처리한다.

필드를 문서화하고 싶지 않다면 무시하도록 마킹해도 된다. 이렇게하면 위에서 언급한 테스트 실패를 방지하고, 만들어진 스니펫에서도 제외할 수 있다.

모든 필드를 문서화하지 않아도 테스트가 실패하지 않도록 완화된 모드로 필드를 문서화할 수도 있다. 이렇게 하려면 org.springframework.restdocs.payload.PayloadDocumentation에 있는 relaxedRequestPartFields 메소드를 사용해라. 일부 part 페이로드만 중요한 특정 시나리오를 문서화하기 유용하다.

필드에 대한 설명이나 XML을 쓰는 페이로드 문서화 등에 대한 자세한 정보는 요청과 응답 페이로드 문서 작성하기 섹션을 참고해라.


3.7. HTTP Headers

요청, 응답 헤더는 requestHeaders, responseHeaders로 문서화할 수 있다. 다음은 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
this.mockMvc
	.perform(get("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=")) // (1)
	.andExpect(status().isOk())
	.andDo(document("headers",
			requestHeaders( // (2)
					headerWithName("Authorization").description(
							"Basic auth credentials")), // (3)
			responseHeaders( // (4)
					headerWithName("X-RateLimit-Limit").description(
							"The total number of requests permitted per period"),
					headerWithName("X-RateLimit-Remaining").description(
							"Remaining requests permitted in current period"),
					headerWithName("X-RateLimit-Reset").description(
							"Time at which the rate limit period will reset"))));
this.webTestClient
	.get().uri("/people").header("Authorization", "Basic dXNlcjpzZWNyZXQ=") // (1)
	.exchange().expectStatus().isOk().expectBody()
	.consumeWith(document("headers",
		requestHeaders( // (2)
			headerWithName("Authorization").description("Basic auth credentials")), // (3)
		responseHeaders( // (4)
			headerWithName("X-RateLimit-Limit")
				.description("The total number of requests permitted per period"),
			headerWithName("X-RateLimit-Remaining")
				.description("Remaining requests permitted in current period"),
			headerWithName("X-RateLimit-Reset")
				.description("Time at which the rate limit period will reset"))));
RestAssured.given(this.spec)
	.filter(document("headers",
			requestHeaders( // (1)
					headerWithName("Authorization").description(
							"Basic auth credentials")), // (2)
			responseHeaders( // (3)
					headerWithName("X-RateLimit-Limit").description(
							"The total number of requests permitted per period"),
					headerWithName("X-RateLimit-Remaining").description(
							"Remaining requests permitted in current period"),
					headerWithName("X-RateLimit-Reset").description(
							"Time at which the rate limit period will reset"))))
	.header("Authorization", "Basic dXNlcjpzZWNyZXQ=") // (4)
	.when().get("/people")
	.then().assertThat().statusCode(is(200));

(1) basic 인증을 위한 Authorization 헤더를 사용해서 GET 요청을 수행한다.
(2) 요청 헤더를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 requestHeaders를 사용한다.
(3) Authorization 헤더를 문서화한다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 headerWithName을 사용한다.
(4) 응답 헤더를 설명하는 스니펫을 만든다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 responseHeaders를 사용한다.

(1) basic 인증을 위한 Authorization 헤더를 사용해서 GET 요청을 수행한다.
(2) 요청 헤더를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 requestHeaders를 사용한다.
(3) Authorization 헤더를 문서화한다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 headerWithName을 사용한다.
(4) 응답 헤더를 설명하는 스니펫을 만든다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 responseHeaders를 사용한다.

(1) 요청 헤더를 설명하는 스니펫을 만들도록 설정한다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 requestHeaders를 사용한다.
(2) Authorization 헤더를 문서화한다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 headerWithName을 사용한다.
(3) 응답 헤더를 설명하는 스니펫을 만든다. org.springframework.restdocs.headers.HeaderDocumentation에 있는 스태틱 메소드 responseHeaders를 사용한다.
(4) 요청에 basic 인증을 위한 Authorization 헤더를 설정한다.

결과로 request-headers.adocresponse-headers.adoc 스니펫이 만들어진다. 두 스니펫 모두 헤더를 설명하는 테이블을 가지고 있다.

HTTP 헤더를 문서화할 땐 요청, 응답에 있는 모든 헤더를 작성하지 않으면 테스트는 실패한다.


3.8. Reusing Snippets

보통 문서화하는 API에는 여러 리소스에서 사용하는 공통 기능이 있기 마련이다. 이런 리소스를 문서화할 땐 공통 요소로 설정한 Snippet을 재사용하면 중복 코드를 피할 수 있다.

먼저 공통 요소를 설명하는 Snippet을 만들어야 한다. 다음은 그 방법을 보여준다:

protected final LinksSnippet pagingLinks = links(
		linkWithRel("first").optional().description("The first page of results"),
		linkWithRel("last").optional().description("The last page of results"),
		linkWithRel("next").optional().description("The next page of results"),
		linkWithRel("prev").optional().description("The previous page of results"));

그 다음엔 이 스니펫을 사용해서 리소스에 특화된 descriptor를 별도로 추가하면 된다. 다음 예제를 참고해라:

MockMvc WebTestClient REST Assured
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("example", this.pagingLinks.and( // (1)
			linkWithRel("alpha").description("Link to the alpha resource"),
			linkWithRel("bravo").description("Link to the bravo resource"))));
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON).exchange()
	.expectStatus().isOk().expectBody()
	.consumeWith(document("example", this.pagingLinks.and( // (1)
		linkWithRel("alpha").description("Link to the alpha resource"),
		linkWithRel("bravo").description("Link to the bravo resource"))));
RestAssured.given(this.spec)
	.accept("application/json")
	.filter(document("example", this.pagingLinks.and( // (1)
			linkWithRel("alpha").description("Link to the alpha resource"),
			linkWithRel("bravo").description("Link to the bravo resource"))))
	.get("/").then().assertThat().statusCode(is(200));

(1) pagingLinks Snippet을 재사용하고, 문서화할 리소스에 특화된 descriptor를 and로 추가한다.

이 예제를 실행하면 relfirst, last, next, previous, alpha, bravo인 링크를 전부 문서화한다.


3.9. Documenting Constraints

스프링 Rest Docs는 여러 가지 클래스로 제약 조건 문서화를 돕는다. 클래스 제약 조건 설명은 ConstraintDescriptions 인스턴스로 접근할 수 있다. 다음은 그 방법을 보여준다:

public void example() {
	ConstraintDescriptions userConstraints = new ConstraintDescriptions(UserInput.class); // (1)
	List<String> descriptions = userConstraints.descriptionsForProperty("name"); // (2)
}

static class UserInput {

	@NotNull
	@Size(min = 1)
	String name;

	@NotNull
	@Size(min = 8)
	String password;

}

(1) UserInput 클래스를 위한 ConstraintDescriptions 인스턴스를 생성한다.
(2) name 프로퍼티의 제약조건 설명을 가져온다. 이 리스트엔 두 가지 설명이 담겨있다: NotNull 제약 조건과 Size 제약 조건.

이 기능을 활용한 사례는 스프링 HATEOAS 샘플에 있는 ApiDocumentation 클래스를 보면 알 수 있다.

3.9.1. Finding Constraints

기본적으로 제약 조건은 Bean Validation Validator를 사용해 찾는다. 현재는 프로퍼티 제약조건만 지원한다. 커스텀 ValidatorConstraintResolver 인스턴스로 ConstraintDescriptions를 만들면 이때 사용할 Validator를 커스텀할 수 있다. 제약 조건 resolution을 완전히 제어하고 싶으면 ConstraintResolver를 직접 구현하면 된다.

3.9.2. Describing Constraints

Bean Validation 2.0에 있는 모든 제약 조건은 디폴트로 설명을 제공한다:

Hibernate Validator에 있는 아래 제약 조건에 대한 기본 설명도 함께 지원한다:

디폴트 설명을 재정의하거나 다른 설명을 사용하고 싶다면, base name이 org.springframework.restdocs.constraints.ConstraintDescriptions인 리소스 번들을 만들면 된다. 리소스 번들을 사용하는 예시는 스프링 HATEOAS 기반 샘플에서 확인할 수 있다.

리소스 번들에 있는 각 키에 .description을 더한 게 제약 조건의 풀 네임이다. 예를 들어 표준 @NotNull 제약 조건의 키는 javax.validation.constraints.NotNull.description이다.

설명 부분에선 제약 조건의 속성을 가리키는 프로퍼티 플레이스홀더를 사용할 수 있다. 예를 들어 @Min 제약 조건의 기본 설명 Must be at least ${value}에선 제약 조건의 value 속성을 참조하고 있다.

제약 조건 설명 처리를 좀 더 제어하고 싶으면, 커스텀 ResourceBundleConstraintDescriptionResolverConstraintDescriptions를 만들면 된다. 완전히 제어하려면 커스텀 ConstraintDescriptionResolver 구현체로 ConstraintDescriptions를 만들어라.

3.9.3. Using Constraint Descriptions in Generated Snippets

제약 조건에 대한 설명이 있다면 생성할 스니펫에서 원하는대로 사용할 수 있다. 예를 들어 필드를 설명하면서 제약 조건을 함께 설명하고 싶을 수 있다. 아니면 요청 필드 스니펫 추가 정보에 제약 조건을 넣을 수도 있다. 스프링 HATEOAS 기반 샘플에 있는 ApiDocumentation 클래스는 후자의 접근 방식을 보여준다.


3.10. Default Snippets

요청과 응답을 문서화하면 자동으로 여러 가지 스니펫이 만들어 진다.

Snippet Description
curl-request.adoc 문서화하는 MockMvc 호출과 동일한 curl 명령어가 있다.
httpie-request.adoc 문서화하는 MockMvc 호출과 동일한 HTTPie 명령어가 있다.
http-request.adoc 문서화하는 MockMvc 호출과 동일한 HTTP 요청이 있다.
http-response.adoc 반환된 HTTP 응답이 있다.
request-body.adoc 전송한 요청 바디가 있다.
response-body.adoc 반환된 응답 바디가 있다.

디폴트로 생성할 스니펫을 설정해도 된다. 자세한 정보는 설정 섹션을 참고해라.


3.11. Using Parameterized Output Directories

MockMvc나 REST Assured를 사용할 땐, document에서 쓸 출력 디렉토리를 파라미터로 만들 수 있다. WebTestClient를 사용할 때는 불가능하다.

다음 파라미터를 지원한다:

Parameter Description
{methodName} 테스트 메소드의 원래 이름.
{method-name} 캐밥 케이스로 표기한 테스트 메소드 이름.
{method_name} 스네이크 케이스로 표기한 테스트 메소드 이름.
{ClassName} 테스트 클래스의 원래 이름.
{class-name} 캐밥 케이스로 표기한 테스트 클래스의 simple name.
{class_name} 스네이크 케이스로 표기한 테스트 클래스의 simple name.
{step} 현재 테스트에서 서비스를 호출한 횟수.

예를 들어 테스트 클래스가 GettingStartedDocumentation이고 테스트 메소드는 creatingANote라면, document("{class-name}/{method-name}")getting-started-documentation/creating-a-note란 디렉토리에 스니펫을 생성한다.

출력 디렉토리를 파라미터로 만드는 건 특히 @Before 메소드와 함께 쓸 때 유용하다. 초기 세팅 메소드에서 문서를 한 번만 설정하고, 클래스 내 모든 테스트에 재사용할 수 있기 때문이다. 다음은 그 방법을 보여준다:

MockMvc REST Assured
@Before
public void setUp() {
	this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
			.apply(documentationConfiguration(this.restDocumentation))
			.alwaysDo(document("{method-name}/{step}/")).build();
}
@Before
public void setUp() {
	this.spec = new RequestSpecBuilder()
			.addFilter(documentationConfiguration(this.restDocumentation))
			.addFilter(document("{method-name}/{step}")).build();
}

이렇게 설정해 두면, 다른 설정이 없어도 테스트하는 서비스를 호출할 때마다 디폴트 스니펫을 만든다. 사용 사례는 각 샘플 어플리케이션에 있는 GettingStartedDocumentation 클래스를 살펴봐라.


3.12. Customizing the Output

이번 섹션에선 스프링 REST Docs 결과물을 커스텀하는 방법을 설명한다.

3.12.1. Customizing the Generated Snippets

스프링 Rest Docs는 Mustache 템플릿으로 스니펫을 만든다. 스프링 Rest Docs는 생성하는 모든 스니펫마다 디폴트 템플릿을 제공한다. 스니펫 내용을 커스텀하려면 자체 템플릿을 제공하면 된다.

템플릿은 org.springframework.restdocs.templates 하위 패키지 클래스패스에서 로드한다. 하위 패키지명은 사용 중인 템플릿 포맷의 ID로 결정한다. 디폴트 템플릿 포맷 Asciidoctor는 ID가 asciidoctor이므로, org.springframework.restdocs.templates.asciidoctor에서 스니펫을 로드한다. 각 템플릿 이름은 생성할 스니펫 이름에서 따온다. 예를 들어 curl-request.adoc 스니펫 템플릿을 재정의하려면, src/test/resources/org/springframework/restdocs/templates/asciidoctor 아래 curl-request.snippet이란 템플릿을 생성해라.

3.12.2. Including Extra Information

스니펫에는 두 가지 방법으로 추가 정보를 넣을 수 있다:

이렇게 추가한 속성은 템플릿을 렌더링할 때 접근할 수 있다. 커스텀 스니펫 템플릿을 함께 사용하면 스니펫에 추가 정보를 넣을 수 있다.

구체적으로 예를 들자면, 요청 필드 문서에 제약 조건 컬럼과 제목을 추가할 수 있다. 먼저 문서화할 각 필드에 constraints 속성을 주고, title 속성도 추가한다. 다음은 그 방법을 보여준다:

MockMvc WebTestClient REST Assured
.andDo(document("create-user", requestFields(
		attributes(key("title").value("Fields for user creation")), // (1)
		fieldWithPath("name").description("The user's name")
				.attributes(key("constraints")
						.value("Must not be null. Must not be empty")), // (2)
		fieldWithPath("email").description("The user's email address")
				.attributes(key("constraints")
						.value("Must be a valid email address"))))); // (3)
consumeWith(document("create-user",
	requestFields(
		attributes(key("title").value("Fields for user creation")), // (1)
		fieldWithPath("name")
			.description("The user's name")
			.attributes(key("constraints").value("Must not be null. Must not be empty")), // (2)
		fieldWithPath("email")
			.description("The user's email address")
			.attributes(key("constraints").value("Must be a valid email address"))))); // (3)
.filter(document("create-user", requestFields(
		attributes(key("title").value("Fields for user creation")), // (1)
		fieldWithPath("name").description("The user's name")
				.attributes(key("constraints")
						.value("Must not be null. Must not be empty")), // (2)
		fieldWithPath("email").description("The user's email address")
				.attributes(key("constraints")
						.value("Must be a valid email address"))))) // (3)

(1) 요청 필드 스니펫에 title 속성을 설정한다.
(2) name 필드에 constraints 속성을 설정한다.
(3) email 필드에 constraints 속성을 설정한다.

다음 할 일은 만들어진 스니펫 테이블에 필드 제약 조건과 제목을 추가하는 커스텀 템플릿 request-fields.snippet을 제공하는 거다. 다음 예제를 참고해라:

.{{title}} // (1)
|===
|Path|Type|Description|Constraints // (2)

{{#fields}}
|{{path}}
|{{type}}
|{{description}}
|{{constraints}} // (3)

{{/fields}}
|===

(1) 테이블에 제목을 추가한다.
(2) 새 컬럼 “Constraints”를 추가한다.
(3) 각 테이블 행에 descriptor의 constraints 속성을 넣는다.


전체 목차는 여기에 있습니다.

<< >>

TOP