본문 바로가기
IT/Spring

Spring Rest Docs

by 최고영회 2020. 5. 22.
728x90
반응형
SMALL

이전 포스팅 중 Swagger를 이용한 API 문서화 및 테스트 자동화 (https://kimyhcj.tistory.com/354) 내용이 있다.

사실 Swagger는 테스트 용이성이 매우 높고 쉽고 빠르게 초기에 진입할 수 있기 때문에 선택했었다.

Swagger를 사용해서 API를 모두 만들고 난 후 API에 대해서 FrontEnd 개발자와 이야기해야 할 때

문서가 필요했는데 Swagger 화면으로 설명하기에는 부족함이 많았다.

 

Swagger의 Annotation 설명과 실제 구현코드가 맞지 않을 수 있다는 것.

즉, 문서화 로직이 불일치 할 수 있다는 것이 가장 큰 문제였다.

그리고 소스코드에 더덕더덕 붙어 있는 지저분한 Swagger Annotation들...

@ApiOperation(value = "Get user by Id and password") 
@ApiResponses({ 
  @ApiResponse(code = 200, message = "success"), 
  @ApiResponse(code = 404, message = "can not find user with id") }) 
@GetMapping("id/{id}/pw/{pw}") 
public ResponseEntity<User> getUserNoByIdAndPw( 
  @ApiParam(value = "user id", required = true) 
  @PathVariable(name = "id", required = true) @NotBlank String id, 
  @ApiParam(value = "encrypted password", required = true) 
  @PathVariable(name = "pw", required = true) @NotBlank String encPw ) { 
    // 생략 
    return new ResponseEntity<User>(user, user == null ? HttpStatus.NOT_FOUND : HttpStatus.OK); 
}

그래서 API 테스트 보다는 문서화에 더 중점을 두고 테스트와 동시에 문서화가 이루어지는..

다시말해 테스트가 되어야만 문서화가 이루어지는 Spring Rest Docs 로 전환했다.

사실 Swagger의 가장 큰 장점인 API Test 를 쉽게할 수 있는...것은 Postman으로 충분했다.

전환한지 오래 되었지만 정리 차원에서 포스팅을 남겨 본다.

1. pom.xml 에 restdocs dependency 추가

<!-- spring rest docs --> 
<dependency> 
  <groupId>org.springframework.restdocs</groupId> 
  <artifactId>spring-restdocs-mockmvc</artifactId> 
  <scope>test</scope> 
</dependency>

 

2. plugin에 asciidoc (파일을 컴버팅하고 target에 복사) 등 추가
  spring-restdocs-asciidoctor 의존성은 snippets 을 자동으로 구성해 준다.
  outputDirectory는 문서가 생성되는 디렉토리 경로이다.

<plugin> 
  <groupId>org.asciidoctor</groupId>
  <artifactId>asciidoctor-maven-plugin</artifactId> 
  <version>1.5.3</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>2.0.2.RELEASE</version> 
  </dependency> 
  </dependencies> 
</plugin> 
<plugin> 
  <artifactId>maven-resources-plugin</artifactId> 
  <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>

 

3. Test Code 작성

Junit 으로 Test 하며 MockMvc를 이용한다.

@Before 를 통해 Test 시 먼저 정의되어야 할 내용들을 set 한다.

private static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), StandardCharsets.UTF_8);

@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();

@Autowired
private WebApplicationContext context;

private MockMvc mockMvc;

private RestDocumentationResultHandler document;

@Before
public void setup() throws ServletException {

this.document = document("{class-name}/{method-name}", 
                 preprocessRequest(prettyPrint()),
                 preprocessResponse(prettyPrint())); 
DelegatingFilterProxy delegateProxyFilter = new DelegatingFilterProxy(); 
delegateProxyFilter.init(new MockFilterConfig(context.getServletContext(), BeanIds.SPRING_SECURITY_FILTER_CHAIN)); 
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context) 
              .apply(documentationConfiguration(this.restDocumentation)) 
              .addFilter(delegateProxyFilter) 
              .alwaysDo(document) 
              .build();
}

ㅇRestDocumentationResultHandler 를 {class-name}/{method-name} 로 설정하면

해당 테스트 클래스의 이름과 메서드 이름 기반으로 디렉토리 경로가 설정되어 snippets을 생성한다.

즉 DocConrollerTest.java 의 getDoc() 를 @Test 하면

target/generated-snippets/doc-controller-test/get-doc에 snippets리 생성된다.

prettyPrint()를 이용하여 문서가 예쁘게 만들어지게 할 수 있다. (json response를 json formatting)

alwyasDo()를 이용하여 모든 mockMvc 테스트에 대한 snippets 리 생성되도록 한다.

JWT를 Filter로 하여 Spring Security에서 사용자에 대한 인증을 체크하고 있기 때문에

MockMvc를 사용한 테스트에서도 해당 Filter가 적용될 수 있도록 DelegatingFilterProxy 를 생성하여 설정해 준다.

4. Test

API를 Test 하는 코드를 작성해 보자.

@Test 
public void getDoc() throws Exception { 
  mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/doc/{docNo}", 1) 
    .header(HttpHeaders.AUTHORIZATION, JWT_YHKIM) 
    .accept(MediaType.APPLICATION_JSON)) 
    .andDo(print()) 
    .andExpect(status().isOk()) 
    .andDo(
      document.document( 
        pathParameters(
          parameterWithName("docNo").description("문서 번호").attributes(Attributes.key("type").value("Number"))
        ), 
        getResponseOne()
      )); 
}

간단한 문서조회 Test 코드이다.

Rest API URL Path 에 속한 내용은

pathParameters, requestParameters, responseFields를 이용하여 문서를 작성하고 테스트를 완벽히 하자.

pathParameter 사용 예제

.andDo(
  document.document( 
   pathParameters(parameterWithName("docNo").description("문서 번호").attributes(Attributes.key("type").value("Number")))) 
 );

requestParameters 사용 예제

.andDo(
  document.document( 
    pathParameters(parameterWithName("docNo").description("문서 번호").attributes(Attributes.key("type").value("Number"))),
    requestParameters(parameterWithName("opinion").description("의견").attributes(Attributes.key("type").value("String")).optional()) 
  ) 
);

responseFields 사용 예제

responseFields( 
  fieldWithPath("result").type(JsonFieldType.BOOLEAN).description("성공 여부"), 
  fieldWithPath("data").type(JsonFieldType.OBJECT).description("data"), 
  fieldWithPath("data.contents").type(JsonFieldType.ARRAY).description("문서 정보").optional(), fieldWithPath("data.contents.[].no").type(JsonFieldType.NUMBER).description("문서 번호"), 
  fieldWithPath("data.contents.[].requester.name").type(JsonFieldType.STRING).description("신청인 이름").optional() 
);

optional() 을 통해 필수여부를 설정할 수 있다. (아래 6번 문서의 커스텀 참고)

Test 를 해 보면 doc-controller-test / get-doc 밑에 .adoc 파일들이 생성된다.

mockMvc building 할 때 markdown 으로 설정하면 .adoc 가 아닌 .md 파일로 만들수도 있다. 

this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
                .apply(documentationConfiguration(this.restDocumentation)
                .snippets().withTemplateFormat(TemplateFormats.markdown())) // <--- .md
                .alwaysDo(document)
                .build();

 

5. 문서화

이제 api-guide-doc.adoc ({project}/src/main/asciidoc/api-giuld-doc) 파일에

위에서 생성된 get-doc 의 http-request.adoc ... response-fileds.adoc 파일들을 넣어주면 된다.

== 문서 

=== 조회 

===== http request 
include::{snippets}/doc-controller-test/get-doc/http-request.adoc[] 
===== path parameters 
include::{snippets}/doc-controller-test/get-doc/path-parameters.adoc[] 
===== response fields 
include::{snippets}/doc-controller-test/get-doc/response-fields.adoc[] 
===== response 
include::{snippets}/doc-controller-test/get-doc/http-response.adoc[]

이제 maven build (goals - package) 해 보면 target/generated-docs/api-guilde.html 파일이 생성된다.

api-guilde.adoc 파일의 상단에는 [[overview]], 공통사항, header, header verbs, http status code 에 대한 설명을 넣어주면 더 좋다.

Test 한 코드에 대한 문서가 잘 생성된 것을 확인할 수 있다.

 

6. 문서의 커스텀

위에서 언급했던 optional()을 이용한 필수여부 포함처럼 필요한 정보들을 snippet에 추가할 수 있다.

src/main/test/resources/org/springframework/restdocs/templates 폴더에 (없으면 생성)

커스텀할 snippet template 들을 생성하면 된다.

 

아래 링크에서 default snippet 을 보고 copy 한 후 custom 하면 된다. 

https://github.com/spring-projects/spring-restdocs/tree/main/spring-restdocs-core/src/main/resources/org/springframework/restdocs/templates/asciidoctor

request-fields.snippets 의 예제는 아래와 같다. (mustache 문법을 이용한다.)

|=== 
|Name|Type|Required|Description 

{{#fields}} 
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} 
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} 
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} 
|{{#tableCellContent}}{{description}}{{/tableCellContent}} 

{{/fields}} 

|===

이제 API가 추가되거나 수정될 경우 Junit을 통해 Test 를 하고 maven package 하면 바로 문서가 생성되며

코드와 문서의 일치가 유지된다.

728x90
반응형
LIST