일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 백엔드
- terraform
- ChatGPT
- spring
- IAC
- 도서
- java
- class-transformer
- restdocs
- terraform cloud
- 유데미
- 이더리움
- 블록체인
- 글또
- gradle
- nestjs
- Database
- Mocha
- corretto
- Nestia
- typeorm
- mysql
- chai
- blockchain
- 온라인강의
- nodejs
- 리뷰
- TypeScript
- docker
- Redis
- Today
- Total
끄적끄적
Spring Rest Docs 사용하기 본문
서론
백앤드를 하다보면 가장 중요한 작업 중 하나가 API 문서 작성이다. 필자의 경우 다른 플랫폼에서는
대부분 Swagger 를 이용해 왔는데 Spring 에서는 테스트코드를 바탕으로 api docs 를 만들어 주는 프레임워크이다. 특히나 테스트 코드를 강제할 수 있다는 부분에서 매력적으로 다가왔다. 그래서 이번에 간단한 사용법에 대해서 적어볼까 한다.
시작하기
우선 이전 포스팅에서 구성한 멀티모듈 프로젝트를 바탕으로 만들어볼까 한다. 우리의 멀티모듈 프로젝트에서 restdocs 를 사용할 부분은 api 모듈 부분이다. 이 모듈의 build.gradle 에 rest docs 에 필요한 설정들을 정의해준다. 또한 포스팅에서는 WebTesetClient 를 사용해서 작성할 예정이므로 이에대한 부분또한 추가해준다.
처음에 공식문서에서 하라는 대로 했는데 html 파일이 만들어지는 경로가 html5 가 아니라 docs/asciidoc/ 으로 만들어져서 이를 수정하는 작업이 필요했다.
그 외에는 크게 다른 부분 없이 차근차근 따라가면서 설정하면 된다.
plugins {
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
configurations {
asciidoctorExt
}
ext {
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
}
// asciidoctor 작업 정의
asciidoctor {
dependsOn test
configurations 'asciidoctorExt'
inputs.dir snippetsDir
// 특정 .adoc에 다른 adoc 파일을 가져와서(include) 사용할 때 경로를 baseDir로 맞춰주는 설정
baseDirFollowsSourceFile()
}
// static/docs 폴더 비우기
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
bootJar {
enabled = true
// jar 파일에 html 파일 설정하기
dependsOn asciidoctor
from ("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
jar {
enabled = true
}
// 실제 코드에 index.html 파일 설정
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
// build 의 의존작업 명시
build {
dependsOn copyDocument
}
dependencies {
implementation project(':identity')
implementation project(':order')
// webclient 를 써야하므로 webflux 종속성도 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-web'
// rest webtestclient & asciidoc
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.6.RELEASE'
testImplementation 'org.springframework.restdocs:spring-restdocs-webtestclient'
}
예시
우선 AppController 를 아래처럼 path 도 받고 query, body 를 받도록 조금 수정해주자.
물론 실제 운영코드는 이렇게 코딩하지 않겠지만 실습을 위해서 여러가지를 한번 받아보도록 하자.
또한 CreateOrder 클래스의 경우 단순히 status 만을 가지고 있는 DTO 클래스이니 따로 명시하지는 않았다.
@RestController
public class AppController {
@GetMapping("/api/identities/{identityId}")
public Identity getIdentity(@PathVariable String identityId,
@RequestParam String provider) {
return new Identity(identityId, provider);
}
@PostMapping("/api/orders/{orderId}")
public Order createOrder(
@PathVariable String orderId,
@RequestBody CreateOrder createOrder) {
return new Order(orderId, createOrder.status);
}
@GetMapping("/health")
public String health() {
return "healthy";
}
}
그 후에는 이제 테스트 코드를 작성해보자. 우선 GetIdentityTest 를 가지고 테스트 해 볼 예정이다.
restdocs 에 대한 문법(pathParameters, queryParameters 등)은 공식문서에 자세히 나와 있으므로 그것을 참고하도록 하자.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class GetIdentityTest {
@LocalServerPort
private int port;
@Autowired
private WebTestClient webTestClient;
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
this.webTestClient = WebTestClient
.bindToServer().baseUrl("http://localhost:" + port)
.filter(documentationConfiguration(restDocumentation))
.build();
}
@DisplayName("identity 를 반환한다.")
@Test
public void sut_returns_Identity() {
String nickname = "pawmi";
String identityId = "23";
String url = "/api/identities/{identityId}?nickname=" + nickname;
EntityExchangeResult<Identity> result = this.webTestClient.get().uri(url, identityId).accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody(Identity.class)
.consumeWith(document("identities",
preprocessResponse(prettyPrint()),
pathParameters(
parameterWithName("identityId")
.description("인증 id")
),
queryParameters(
parameterWithName("age")
.optional()
.description("나이"),
parameterWithName("nickname")
.description("닉네임")
),
responseFields(
fieldWithPath("id").type(JsonFieldType.STRING).description("인증 Id"),
fieldWithPath("name").type(JsonFieldType.STRING).description("닉네임")
)
)).returnResult();
Identity body = result.getResponseBody();
assertThat(body.getId()).isEqualTo(identityId);
assertThat(body.getName()).isEqualTo(nickname);
}
}
작성을 한 뒤 테스트를 통과하면 빌드 파일에 document 에서 지정한 identities 의 이름으로 빌드 snippet 파일들이 생성되어 있는 것을 확인할 수 있다.
이를 가지고 기본 html 파일을 생성하도록 adoc 파일을 만들어줘야 한다. src/docs/asciidoc 파일에 생성하고자 하는 파일을 asciidoc 파일로 만들어주자. 현재 포스팅에서는 identity.adoc 을 만들어서 작업했다. 위의 사진에서 나오는 파일을 가지고 만들면 되며, 이 asciidoc 관련 플러그인이 intellij 에 존재하므로 사용하면 쉽게 적용할 수 있다.
[[identity]]
== identity API
=== Request
CURL:
include::{snippets}/identities/curl-request.adoc[]
Path Parameters:
include::{snippets}/identities/path-parameters.adoc[]
Query Parameters:
include::{snippets}/identities/query-parameters.adoc[]
Request HTTP Example:
include::{snippets}/identities/http-request.adoc[]
=== Response
Response Fields:
include::{snippets}/identities/response-fields.adoc[]
Response HTTP Example:
include::{snippets}/identities/http-response.adoc[]
그 후에 프로젝트를 빌드해보자. 그러면 아래 사진처럼 docs 아래에 html 파일이 생기고 이것이 resources 디렉토리에 복사되어 있는 것 또한 확인할 수 있다. 또한 앱을 실행시켜서 /docs/identity.html 로 접속하면 docs 가 잘 나오는 것을 확인할 수 있다.
여기까지 완료했다면 기본적인 restdocs 사용법을 끝났다.
비슷한 방식으로 CreateOrder 또한 만들어주자.
아래에 테스트 코드만 간단하게 적고 넘어가도록 할 것이다. asciidoc 파일은 알아서 만들어보자. (api 마다 다른 docs 를 만들어야 하므로 여기서는 create-order 로 설정했다)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class CreateOrderTest {
@LocalServerPort
private int port;
@Autowired
private WebTestClient webTestClient;
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
this.webTestClient = WebTestClient
.bindToServer().baseUrl("http://localhost:" + port)
.filter(documentationConfiguration(restDocumentation))
.build();
}
@DisplayName("생성한 order 를 반환한다.")
@Test
public void sut_returns_Order() {
CreateOrder createOrder = new CreateOrder("pending");
String orderId = "2";
String url = "/api/orders/{orderId}";
EntityExchangeResult<Order> result = this.webTestClient
.post()
.uri(url, orderId)
.bodyValue(createOrder)
.accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk().expectBody(Order.class)
.consumeWith(document("create-order",
preprocessResponse(prettyPrint()),
pathParameters(
parameterWithName("orderId")
.description("주문 id")
),
requestFields(
fieldWithPath("status").type(JsonFieldType.STRING).description("주문 상태")
),
responseFields(
fieldWithPath("id").type(JsonFieldType.STRING).description("주문 Id"),
fieldWithPath("status").type(JsonFieldType.STRING).description("주문 상태")
)
)).returnResult();
Order body = result.getResponseBody();
assertThat(body.getId()).isEqualTo(orderId);
assertThat(body.getStatus()).isEqualTo("pending");
}
}
문서 분리하기
api 마다 다른 url 로 접근해야 한다면 오히려 보기 불편할 수 있다. 그래서 모든 api 를 index.html 하나의 url 로 설정할 수 있도록 옮겨보자. 이 부분은 include 를 활용하면 간단하게 적용 가능하다.
다시 src/docs/asciidoc 디렉토리에 index.adoc 을 생성해서 아래처럼 만들어보자.
= API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3
:sectlinks:
[[common]]
== 공통 사항
API에 관계없이 아래 사항을 지켜주셔야 합니다.
=== Header
|===
| name | 설명
| `Authorization`
| API를 사용하기 위한 인증 키
|===
include::identity.adoc[]
include::order.adoc[]
그러면 이제 아래 사진과 같이 identity.adoc, order.adoc 에서 작성한 것이 index.html 에 모일 것이다.
문서 Customize
가장 유명한 커스텀으로는 필수값 표기가 있을 것 같다. optional 이 적용된 값을 false 로 지정하고 나머지는 true 로 지정할 수 있다.
그 이외에도 여러 제약조건을 걸 수 있다.
이를 optional, constraint 로 지정해서 사용할 수 있다. getIdentity 의 경우 age 를 받거나 안받을 수 있고 nickname 은 무조건 받야아만 하도록 query parameter 를 한번 고쳐보자.
또한 constraint 를 커스텀으로 제공해서 특정 조건을 명시하도록 설정할 수 있다.
우선 resources/org/springframework/restdocs/templates/asciidoctor 경로에 생성되는 기본 snippet 을 덮어줄 수 있다.
실습할 query parameter 의 경우 query-parameters.snippet 파일을 만들어서 아래처럼 코드를 지정할 수 있다.
|===
|파라미터|필수값|제약조건|설명
{{#parameters}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{ #constraints }}{{.}}{{ /constraints }}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/parameters}}
|===
그 후에 identity 테스트 코드에서 queryParameter 부분만 attribute 와 optional 을 사용하도록 고쳐보자
queryParameters(
parameterWithName("age")
.optional()
.attributes(new Attributes.Attribute("constraints", "20~29 등 범위 지정"))
.description("나이"),
parameterWithName("nickname")
.attributes(new Attributes.Attribute("constraints", "닉네임이 포함된 최근 것을 가져옴"))
.description("닉네임")
),
그 후에 다시 빌드해보면 아래처럼 커스텀한 문서 형식을 발견할 수 있을 것이다.
다른 문서 형식도 바꾸고 싶다면 쉽게 바꿀 수 있을 것이다.
결론
일단 시작하기에는 이정도로도 충분할 것 같다는 생각이 든다. 하지만 개인적으로는 docs 를 만드는 테스트와 실제 api 테스트를 분리해서 docs 를 만드는 테스트는 mockmvc 테스트로 따로 지정하는게 더 빠르고 낫지 않을까 하는 생각이 들었다.
또한 공통적인 부분을 분리한다면 좀 더 간결하게 문서를 생성하는 코드를 작성할 수 있지 않을까 싶다. 실제로 docs 를 사용해보니 다른 플랫폼에도 이런 문서 생성기가 있다면 좀 더 편하지 않을까 하는 생각이 들기도 했다.
'개발 > java & spring' 카테고리의 다른 글
Gradle 명령어로 프로젝트 만들고 실행시키기(with multimodule) (0) | 2022.10.25 |
---|---|
gradle 멀티모듈(모노레포) 구성하기 (with spring) (0) | 2022.10.25 |
Amazon corretto 설치하기 (0) | 2022.10.24 |
Mac 환경에서 IntelliJ & Android studio 이전버전 삭제하기 (0) | 2022.10.24 |