DSL 인터페이스
DSL을 제공할 수 있도록 설계된 인터페이스들에 대해서 기술합니다.
전체 구조

Spring MVC는 핸들러를 통해 HTTP 요청과 응답을 매핑합니다. 그리고 HTTP 요청과 응답을 구성하는 컴포넌트들(쿼리 파라미터, 경로변수, 바디 등)은 핸들러의 어노테이션, 파라미터, 리턴타입으로 매핑합니다. 그리고 Spring REST Docs는 각 컴포넌트들을 구성하는 필드에 대해 설명을 작성하고, 이를 통합하여 문서화 할 수 있도록 도와줍니다.
Spring REST Docs에서는 각 컴포넌트들을 구성하는 필드에 대한 설명을 AbstractDescriptor로서 표현하고, 이를 Snippet으로 통합하고 있습니다.
그리고 통합된 Snippet은 문서의 식별자(identifier)와 함께 document함수를 통해
API 호출 결과를 핸들링하여 최종적으로 asciidoc 등의 형식으로 스니펫을 생성합니다.
이 라이브러리는 DSL을 제공함과 동시에 이러한 Spring REST Docs의 구조에 맞추기 위해
AbstractDescriptor에 대응되는 ApiField, Snippet에 대응되는 ApiComponent,
그리고 하나의 API에 대해 모든 Snippet을 통합하는 ApiSpec,
작성된 DSL을 Snippet으로 변환시켜주는 SnippetGenerator 총 4가지의 인터페이스를 만들어 놓았습니다.
그리고 DSL을 제공하진 않지만, 핸들러를 구성하는 요소들을 ApiField로 변환할 수 있도록
각 요소의 정보를 담는 HandlerElement라는 인터페이스도 있습니다.
하나의 HTTP API 예시를 들어 이 4가지 인터페이스에 대해 쉽게 파악해봅시다.
HTTP Message
Request
GET /some-api?id=abc&num=5 HTTP/1.1
Content-Type: application/json
Response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 85
{
"data": "some data"
}
위와 같은 메시지로 통신되는 API가 있다고 해봅시다. 여기서 메시지를 구성하는 컴포넌트와 필드를 추출하면 아래와 같습니다.
| 컴포넌트 | 필드 |
|---|---|
| 쿼리 파라미터 | id, num |
| 응답 바디 | data |
ApiField는 위 표에 작성된 필드에 대해 문서를 작성할 수 있는 DSL 인터페이스 입니다.
그리고 이러한 ApiField타입의 프로퍼티를 각 컴포넌트 별로 통합해놓는 인터페이스가 바로 ApiComponent입니다.
그리고 생성된 ApiComponent가 어떤 컴포넌트인지에 따라 적합한 SnippetGenerator를 ApiSpec 구현체에서 구현하여 DSL 함수를 호출할 수 있게 됩니다.
확인을 위해 API를 핸들러로 매핑한 후 생성되는 ApiComponent구현체와 ApiField타입의 프로퍼티를 확인해봅시다.
public object ExampleApiResponseBody : BodyComponent(false) {
public val `data`: JsonField = JsonField("data", false, 0)
init {
addFields(
`data`
)
}
}
public object ExampleApiQueryParameter : ApiComponent<ParameterDescriptor>() {
public val id: QueryParameterField = QueryParameterField("id")
public val num: QueryParameterField = QueryParameterField("num")
init {
addFields(
`id`,
`num`
)
}
}
ExampleApiResponseBody, ExampleApiQueryParameter총 두 개의 ApiComponent구현체가 생성되었으며,
각각 필드를 ApiField 구현체 타입의 프로퍼티로 가지고 있는 것을 확인할 수 있습니다.
그리고 이렇게 생성된 ApiComponent들은 SnippetGenerator구현체의 타입 파라미터로 선언되고,
이를 ApiSpec구현체에서 구현하여 각 컴포넌트 별로 적합한 DSL 함수를 호출할 수 있게 됩니다.
한번 그 아래에 생성된 ApiSpec을 살펴봅시다.
public data class ExampleApiSpec(
override val identifier: String,
) : ApiSpec,
ResponseBodySnippetGenerator<ExampleApiResponseBody>,
QueryParameterSnippetGenerator<ExampleApiQueryParameter> {
override val snippets: MutableList<Snippet> = mutableListOf()
override fun getResponseBodyApiComponent(): ExampleApiResponseBody = ExampleApiResponseBody
override fun getQueryParameterApiComponent(): ExampleApiQueryParameter = ExampleApiQueryParameter
override fun addSnippet(generatedSnippet: Snippet) {
this.snippets.add(generatedSnippet)
}
override fun addSnippets(generatedSnippets: List<Snippet>) {
this.snippets.addAll(generatedSnippets)
}
}
해당 HTTP API는 쿼리 파라미터, 응답 바디만을 가지고 있었기 때문에 이에 상응하는 SnippetGenerator구현체를 ApiSpec구현체인 ExampleApiSpec에서 구현하고 있습니다.
SnippetGenerator 구현체는 각각 적합한 이름의 함수를 가지고 있으며, 위 예시에 등장하는 ResponseBodySnippetGenerator는 responseBody,
QueryParameterSnippetGenerator는 queryParameters라는 이름의 함수를 가집니다.
interface QueryParameterSnippetGenerator<C: ApiComponent<ParameterDescriptor>> : SnippetGenerator {
fun queryParameters(dsl: C.() -> Unit) {
...
}
...
}
interface ResponseBodySnippetGenerator<C: BodyComponent> : SnippetGenerator {
fun responseBody(dsl: C.(element: C) -> Unit) {
...
}
...
}
이 함수들은 모두 타입 파라미터로 선언된 타입을 수신객체로 가지는 함수를 파라미터로 가지고 있습니다.
이 타입 파라미터는 ApiComponent 타입이며, ApiComponent는 ApiField타입의 프로퍼티를 멤버로 가지고 있기 때문에
해당 함수 블록에서 ApiField타입의 프로퍼티를 통해 필드 DSL을 호출할 수 있게 됩니다.