꿀똥벌레
꿀똥벌레 개발 블로그
꿀똥벌레
전체 방문자
오늘
어제
  • 분류 전체보기 (90)
    • JAVA (17)
    • SPRING (14)
    • Elasticsearch (4)
    • GRADLE (2)
    • HTML, CSS (0)
    • JAVASCRIPT (0)
    • GIT (1)
    • Vue.js (1)
    • server (1)
    • Python (0)
    • IT리뷰 (0)
    • 인프라 (6)
    • IOS (21)
    • 디자인패턴 (20)
    • Kafka (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • Index Template
  • spring integration
  • maxConnTotal
  • 엘라스틱서치
  • spring
  • ES
  • SWIFT
  • persistent connection
  • 스프링 인테그레이션
  • mappings
  • maxConnPerRoute
  • 인덱스 템플릿
  • persistence connection
  • elasticsearch
  • Index
  • springintegration
  • KEEPALIVE
  • connectionRequestTimeout
  • java
  • 스프링 인티그레이션

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
꿀똥벌레

꿀똥벌레 개발 블로그

SPRING

Spring Rest Docs 사용법

2021. 5. 2. 21:28

Spring Rest Doc??

  • 기존에 사용하던 Swagger는 소스코드의 작동과는 연관이 거의 없기 때문에, 로직이 변경 되더라도 api 명세 어노테이션 등을 변경해주지 않으면 최신화가 되지 않는 문제가 있다.
  • Spring Rest Docs 를 이용하면 반드시 테스트코드가 통과 해야 문서가 생성된다.(asciiDoc 이 생성) 따라서 api 문서의 정확성이 보장된다.

asciiDoc??

markdown 과 같이 특정 표현식을 통해 문서를 작성하는 도구이다. 개인적인 생각으론 markdown 에 비해 사용성은 불편하지만, include와 같은 기능을 통해 api명세를 만들기엔 좋은 것 같다.

(markdown이 include를 지원하게 되면 markdown 이 더 좋을듯...)

RestDocs를 사용하기 위한 의존 추가

build.gradle

plugins {
    ...
    //asciiDoc -> html 변환을 위한 플러그인
    id "org.asciidoctor.convert" version "1.5.9.2"
    ...
}

...

ext {
    ...
    //스니펫이 생성될 기본 경로 변수 설정
    snippetsDir = file('build/generated-snippets')
    ...
}
...
//asciidoctor 실행을 위한 설정
asciidoctor {
  inputs.dir snippetsDir
  dependsOn test
}
...
dependencies {
    ...
    //asciidoctor 디펜던시 추가
    asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.5.RELEASE'
  //test compile 시에 mockMvc 설정을 추가
  testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.5.RELEASE'
    ...
}
...
task copyDocument(type: Copy) {
    //asciidoctor 먼저 실행 후 해당 테스크 실행
  dependsOn asciidoctor

    //경로 설정
  from file("build/asciidoc/html5/")
  into file("src/main/resources/static/docs")
}
//빌드시 태스크 먼저 실행 하도록 설정
build{
  dependsOn copyDocument
}
...
  • asciidoctor: asciidoc(.adoc) 파일을 웹에서 볼 수 있도록 html파일로 변환 해주는 플러그인 이다.
  • 스니펫: restDocs 성공시 http-request.adoc, response-body.adoc과 같은 파일들이 각 테스트별 폴더 하위에 생성되는데(정확히는 테스트 실행 객체의 identifier로 넣어준 문자열) 저러한 .adoc파일들을 스니펫 이라고 한다.
  • 스니펫들을 조합하여 전체적인 Index.adoc 을 만들고, 그 것을 asciidoctor로 html로 파싱하여 문서로 제공한다.

학습테스트용 소스

테스트 코드는 MockMvc를 이용해 작성되었다. RestAssured 객체 또는 다른 스프링 테스트 클래스도 사용 가능하다.

@RestController
@RequestMapping("/api/member")
public class MemberController {
  @GetMapping("{memberId}")
  public ResponseEntity getMember(@PathVariable int memberId) {
    Member member = new Member();
    member.setMemberId(memberId);
    member.setName("꿀똥벌레");
    member.setAge(29);
    return new ResponseEntity<>(member, HttpStatus.OK);
  }

  @PostMapping
  public ResponseEntity postMember(@RequestBody Member member) {
    return new ResponseEntity<>(member, HttpStatus.OK);
  }
}
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest({MemberController.class})
@ExtendWith(RestDocumentationExtension.class)
public class MemberControllerTest {
  private MockMvc mockMvc;

  @MockBean
  MemberService memberService;

  @BeforeEach
  public void setUp(WebApplicationContext webApplicationContext,
                    RestDocumentationContextProvider restDocumentation) {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
        .apply(documentationConfiguration(restDocumentation))
        .build();
  }

  @Test
  public void findMemberById() throws Exception {
    Member member = new Member();
    member.setMemberId(1);
    member.setName("꿀똥벌레");
    member.setAge(29);

    when(memberService.getMemberById(1)).thenReturn(member);

    mockMvc.perform(get("/api/member/{memberId}", 1)
        .accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andDo(document("index",
            pathParameters(
                parameterWithName("memberId").description("멤버 id")
            ),
            responseFields(
                fieldWithPath("memberId").description("멤버 이름"),
                fieldWithPath("name").description("멤버 이름"),
                fieldWithPath("age").description("멤버 이름")
            )));
  }

}
  • build.gradle 에서 설정한 스니펫 디렉토리인 build/generated-snippets 와 andDo(document("") 로 설정한 index로 인해 build/generated-snippets/index 하위에 스니펫들이 생성 된다.
  • @WebMvcTest 를 이용해 테스트를 작성하면 mvc클래스만 로딩 되므로 service 의존성 주입에서 에러가 발생한다. 주입 에러가 발생되지 않도록 @MockBean을 사용한다.
  • junit5의 기능으로 @ExtendWith(RestDocumentationExtension.class) 를 사용하면 @BeforEach의 WebApplicationContext, RestDocumentationContextProvider를 받아올 수 있다.

스프링부트에서 @AutoConfigurationRestDocs 어노테이션을 테스트 클래스에 적용하면 아래를 자동으로 적용해 준다.

@Before
public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
            .apply(documentationConfiguration(this.restDocumentation))
            .build();
}

MockMvc와 RestDocs 커스터마이징

import static capital.scalable.restdocs.response.ResponseModifyingPreprocessors.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;

@TestConfiguration
public class RestDocsConfiguration {
  @Bean
  public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
    return configurer -> configurer.snippets()
        .withDefaults(
            CliDocumentation.curlRequest(),
            HttpDocumentation.httpRequest(),
            HttpDocumentation.httpResponse(),
            PayloadDocumentation.requestBody(),
            PayloadDocumentation.responseBody());
  }

  @Bean
  public RestDocumentationResultHandler restDocumentationResultHandler(final Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) {
    return document("{class-name}/{method-name}",
        preprocessRequest(prettyPrint()),
        preprocessResponse(replaceBinaryContent(),
            limitJsonArrayLength(jackson2ObjectMapperBuilder.build()),
            replaceMultipartBinaryContent(),
            prettyPrint()));
  }
}

@WebMvcTest({MemberController.class})
@Import(RestDocsConfiguration.class)
@AutoConfigureRestDocs
public class MemberControllerTest {
  @Autowired
  private MockMvc mockMvc;
    ...
}

테스트가 성공하면 생성되는 스니펫

build/generated-snippets/.. 에 생성된다.

curl-request.adoc

[source,bash]
----
$ curl 'http://localhost:8080/api/member/1' -i -X GET \
    -H 'Accept: application/json'
----

http-request.adoc

[source,http,options="nowrap"]
----
GET /api/member/1 HTTP/1.1
Accept: application/json
Host: localhost:8080

----

등등이 있다.

MockMvc RestDoc 커스터마이징 설정

Configuration 클래스

import static capital.scalable.restdocs.response.ResponseModifyingPreprocessors.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;

@TestConfiguration
public class RestDocsConfiguration {
  @Bean
  public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
    return configurer -> configurer.snippets()
        .withDefaults(
            CliDocumentation.curlRequest(),//curl스니펫
            HttpDocumentation.httpRequest(),//http-request스니펫
            HttpDocumentation.httpResponse(),//http-response스니펫
            PayloadDocumentation.requestBody(),//request-body스니펫
            PayloadDocumentation.responseBody());//response-body스니펫
  }

  @Bean
  public RestDocumentationResultHandler restDocumentationResultHandler(final Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder) {
    return document("{class-name}/{method-name}",
        preprocessRequest(prettyPrint()),
        preprocessResponse(replaceBinaryContent(),
            limitJsonArrayLength(jackson2ObjectMapperBuilder.build()),
            replaceMultipartBinaryContent(),
            prettyPrint()));
  }
}
  • RestDocsMockMvcConfigurationCustomizer 를 빈으로 생성하면, 스프링에서 MockMvcRestDocumentationConfigurer 를 커스터마이징 하는데 사용된다. 단@AutoConfigureRestDocs 어노테이션이 제공하지 않는 기능에 대해서만 추가적으로 사용된다.
  • configurer.snippets() 로 스니펫 설정 오브젝트를 가져온다.
  • withDefaults(): 디폴트로 생성되는 스니펫들을 설정한다.(http-requeset.adoc등..)
  • CliDocumentation.curlRequest(param) param으로 설정에 맞는 map을 넣어주면 추가 설정이 가능하다.
  • MockMvcRestDocumentation.document(): rest api 문서화 하는 팩토리 메서드 이다.
  • {class-name}/{method-name}경로에 스니펫들이 생성된다(스네이크 표기법)
  • prettyPrint: json 정렬되어 표기
  • testImplementation("capital.scalable:spring-auto-restdocs-core:2.0.11") 의존성 추가로 사용할 수 있다.
  • limitJsonArrayLength: 제이슨 배열 표기의 경우 3개까지 표기되도록 설정
  • replaceBinaryContent, replaceMultipartBinaryContent: binary와 multipart 콘텐츠를 replace??

테스트 클래스

@WebMvcTest({MemberController.class})
@Import(RestDocsConfiguration.class)
@AutoConfigureRestDocs
public class MemberControllerTest {
  @Autowired
  private MockMvc mockMvc;

  @MockBean
  MemberService memberService;

  @Autowired
  RestDocumentationResultHandler restDocumentationResultHandler;

  @Test
  public void findMemberById() throws Exception {
    //Given
    Member member = new Member();
    member.setMemberId(1);
    member.setName("꿀똥벌레");
    member.setAge(29);

    //When
    when(memberService.getMemberById(1)).thenReturn(member);

    //Then
    mockMvc.perform(get("/api/member/{memberId}", 1)
        .accept(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andDo(restDocumentationResultHandler.document(
            pathParameters(
                parameterWithName("memberId").description("멤버 id")
            ),
            responseFields(
                fieldWithPath("memberId").description("멤버 이름"),
                fieldWithPath("name").description("멤버 이름"),
                fieldWithPath("age").description("멤버 이름")
            )
        ));
  }
}

커스텀 스니펫 사용법

추상 클래스인 TemplateSnippet 상속받은 클래스를 정의한다.

public class CustomSnippet extends TemplatedSnippet {
  public CustomSnippet(Map<String, Object> attributes) {
    super("custom", attributes);
  }

  @Override
  protected Map<String, Object> createModel(Operation operation) {
    Map<String, Object> model = new HashMap<>();
    model.put("넣을데이터", "데이터 입니다.");
    return model;
  }
}
  • TemplateSnippet을 상속 받는 경우 createModel을 반드시 구현 해야한다.
  • //HttpRequestSnippet의 createModel 의 메소드 @Override protected Map<String, Object> createModel(Operation operation) { Map<String, Object> model = new HashMap<>(); model.put("method", operation.getRequest().getMethod()); model.put("path", getPath(operation.getRequest())); model.put("headers", getHeaders(operation.getRequest())); model.put("requestBody", getRequestBody(operation.getRequest())); return model; }
  • createModel에서 return 하는 map은 스니펫 파일을 렌더링 할 데이터로 사용된다.
  • {{넣을데이터}} → 데이터 입니다. 로 렌더링 됨.
  • super("custom", attributes)로 선언됨에 따라 custom.snippet 파일을 템플릿으로 사용한다.
  • classpath 하위의 org/springframework/restdocs/templates/custom.snippet 파일을 사용한다.

사용 소스 (학습용으로 급하게 만든 소스이므로 다듬을 필요가 있음)

@TestConfiguration
public class RestDocsConfiguration {
  @Bean
  public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
    return configurer -> configurer.snippets()
        .withDefaults(
            CliDocumentation.curlRequest(),
            HttpDocumentation.httpRequest(),
            HttpDocumentation.httpResponse(),
            PayloadDocumentation.requestBody(),
            PayloadDocumentation.responseBody(),
            new CustomSnippet(null));
  }
...
}

제약조건 표기 추가

java validation annotation을 api문서에 추가 하려 한다.

@Data
public class Member {
  @NotNull
  private int memberId;

  @NotNull
  @Size(max=10)
  private String name;

  private int age;
}
private static class ConstrainedFields {  
private final ConstraintDescriptions constraintDescriptions;

ConstrainedFields(Class<?> input) {
  this.constraintDescriptions = new ConstraintDescriptions(input);
}

    private FieldDescriptor withPath(String path) {
      return fieldWithPath(path)
      .attributes(key("constraints")
      .value(StringUtils.collectionToDelimitedString(
              this.constraintDescriptions.descriptionsForProperty(path), "\n")));
    }
}
  • input파라미터로 받은 클래스의 제약조건을 가져와서, 메세지로 변환시킨다.
  • 메시지는 /src/test/resources/org/springframework/restdocs/constraints/ConstraintDescriptions.properties
    파일을 만들고, 각 제약조건에 해당하는 메세지를 재정의 할 수 있다.
javax.validation.constraints.NotNull.description=NOT NULL  
javax.validation.constraints.Size.description=SIZE ${min} ~ ${max}

테스트 코드

//Given
Member member = new Member();
member.setMemberId(1);
member.setName("꿀똥벌레");
member.setAge(29);

//When  
when(memberService.getMemberById(1)).thenReturn(member);

ConstrainedFields field = new ConstrainedFields(Member.class);  
//Then  
mockMvc.perform(get("/api/member/{memberId}", 1)  
  .accept(MediaType.APPLICATION\_JSON))  
  .andExpect(status().isOk())  
  .andDo(restDocumentationResultHandler.document(  
  pathParameters(  
      parameterWithName("memberId").description("멤버 id")  
  ),  
  responseFields(
  //기본 PayloadDocumentation.fieldWithPath를 사용하지 않고 새로 정의한 ConstrainedFields
  //클래스의 withPath 를 사용하였다.
    field.withPath("memberId").description("멤버 아이디"),  
    field.withPath("name").description("멤버 이름"),  
    field.withPath("age").description("나이")  
  )  
));

추가한 constraints 속성을 사용하도록 response-fields.snippet 을 재정의
/src/test/resources/org/springframework/restdocs/templates/response-fields.snippet

===== Response Fields
|===
|필드명|타입|필수여부|설명|제약조건

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

|===

'SPRING' 카테고리의 다른 글

Spring Event  (0) 2021.05.27
Spring 메소드 실행 실패시 재실행  (0) 2021.05.24
Spring Batch  (0) 2021.04.28
Spring RestTemplate  (0) 2021.04.20
스프링 비동기 타임아웃 설정 변경  (0) 2020.11.23
    꿀똥벌레
    꿀똥벌레
    개발자 꿀똥벌레 입니다.

    티스토리툴바