테스트 코드로 문서화를 - Spring Rest Docs

Lee Seung Jae·2022년 2월 21일
0
post-custom-banner

연습코드는 깃허브에 있다.

Spring Rest Docs

기존에 있던 API 문서가 Swagger 또는 컨플루언스에 저장을 해두었었다.

바꾸려는 이유부터 설명하도록 하겠다.

전환하려는 이유

우선 컨플루언스부터 얘기하자면 그냥 하나의 문서로 API를 공유하고 있었기 때문에,

깜빡하고 고치지 않은, 그러면서 최신화 된 문서도 약간씩 다른 데이터셋을 가지고 있는것들을 많이 봤다.

실제로 내가 고치고 나서도 유지가 안된것도 있었다. 😅

그래서 API문서를 따로 수정하지 않고 문서도 자동화할 수 있지 않은가 에 대해서 생각해봤다.

Spring Rest Docs 채택

우선 스웨거도 기존에 사용하고 있었다고 했는데, 왜 바꾸려고 하냐면

우선 Swagger는 어노테이션을 굉장히 많이 사용한다.

그래서 문서화에 관련된 코드들을 어쩔 수 없이 추가한다.

어노테이션을 덕지덕지 붙이면 가독성도 떨어지고,

컨트롤러는 컨트롤러 + API문서 라는 역할 2개를 하게 된다.

각각의 장단점을 보자.

Spring Rest Docs

장점단점
테스트 기반으로 수행세부 설정이 어렵다
제품 코드에 영향을 주지 않는다엔드포인트에 따른 코드가 많다
문서에 대한 신뢰성이 높다추가적인 기능을 제공해주진 않는다
문서 본연의 기능에 충실하다문서가 딱딱하다 (오히려 난 이래서 좋았다)

Swagger

장점단점
문서가 알록달록하다제품 코드의 가독성이 떨어지게 된다
API 테스트 기능을 제공문서의 신뢰도가 떨어짐 -> 실제 서버를 대상으로 동작한다
객체들에 대한 정보를 제공라이브러리의 무게가 무겁다
의존성 추가만 해도 기본 UI를 제공해줌많은 어노테이션이 필요
테스트 코드가 없어도 사용이 가능하다

예제

build.gradle

plugins {
    id 'org.springframework.boot' version '2.6.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'org.asciidoctor.convert' version '1.5.8'
    id 'java'
}

group = 'io.github'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
maven {
url 'https://search.maven.org/'
}
}

ext {
snippetsDir = file("build/generated-snippets") // 스니펫 디렉토리를 build/generated-snippets 로 설정
}

dependencies {
implementation (
'org.springframework.boot:spring-boot-starter-data-jpa',
'org.springframework.boot:spring-boot-starter-web',
'com.h2database:h2',
'org.projectlombok:lombok'
)
annotationProcessor 'org.projectlombok:lombok'

testImplementation (
        'org.springframework.boot:spring-boot-starter-test',
        'org.springframework.restdocs:spring-restdocs-mockmvc' //mockMvc rest docs의존성
)

}

tasks.named('test') { //test 작업 수행할 때 스니펫 dir 생성해주고 돌림
outputs.dir snippetsDir
useJUnitPlatform()
}

tasks.named('asciidoctor') {
inputs.dir snippetsDir
dependsOn test
}


> 설정하면서 얻은 에러

원래 `repositories` 안에

maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }

이 두개가 들어가 있었는데,

저장소를 바꾸려고 했더니 전부 못불러오는 에러가 발생했다.

**확인해보니 해당 스프링부트 버전이 저장소에 없어서 못불러오는 이유 😱**

버전을 낮추어 해결하였다~ 아무튼 해결하고...

테스트 의존성에 `spring-restdocs-mockmvc`를 추가해준다.

### 코드

> RestDocsDependencyImports.java

```java
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) // RestDocumentation 관련 설정 추가
public class RestDocsDependencyImports {

    protected MockMvc mockMvc;

    @BeforeEach
    void setUp(WebApplicationContext webApplicationContext,
        RestDocumentationContextProvider provider) {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
            .apply(documentationConfiguration(provider).snippets())
            .alwaysDo(print())
            .build();
    }

    protected OperationResponsePreprocessor getResponsePreprocessor() {
    //응답에 대한 json 문장을 예쁘게 줄맞춰 준다.
        return preprocessResponse(prettyPrint());
    }

    protected OperationRequestPreprocessor getRequestPreprocessor() {
    //요청에 대한 json 문장을 예쁘게 줄맞춰 준다.
        return preprocessRequest(prettyPrint());
    }

}

대략적으로 구성해서 전역의 추상클래스로 두고 이 추상클래스를 상속해서 나머지를 구현해주려고 했다.

@RestController
public class HomeController {

    @GetMapping("/")
    public ResponseEntity<Map<String, String>> hello() {
        return ResponseEntity.ok(Map.of("key", "hello"));
    }

}

컨트롤러는 간단하게 key라는 키에 hello라는 값을 담아준걸 반환하게 했다.

@WebMvcTest(HomeController.class)
class HomeControllerTest extends RestDocsDependencyImports {

    @Test
    void hello() throws Exception {
        mockMvc.perform(get("/"))  // request uri
            .andExpect(status().isOk()) //상태값이 200인지
            .andDo(
                document("{method-name}", getRequestPreprocessor(), getResponsePreprocessor(),
                    responseFields(
                        fieldWithPath("key").type(JsonFieldType.STRING).description("키 값"))
                )
            );
    }

}

document부분이 바로 rest-docs부분인데,

인자는 앞부터 디렉토리(build/generated-snippets), request 전처리기, response 전처리기, 스니펫

으로 진행된다.

나는 "{method-name}" 으로 했는데 저렇게 넣어주게 되면 해당 테스트 코드 메소드 따라서 디렉토리가 생성된다.

그리고 앞전에서 설정해준 prettyPrint()로 요청, 응답 전부 전처리를 진행했고,

response-fields 라는 스니펫을 만들기 위해서 Response값에 대한 형식을 적어주었다.

그렇게 해서 테스트코드를 돌리게 되면 성공하게 되고,

이런것이 생기게 된다.

스크린샷 2022-01-31 오후 11 42 51

그 다음으로는 asciiDoc의 문법을 좀 알아야 하는 필요성이 있는데,

그것은 여기 서 확인하도록 하자.

그 다음 src/docs/example.adoc 을 만들어주고

:snippets: ../../../build/generated-snippets
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:

== Request

=== Request URL
....
GET /
Content-Type: application/json;charset=UTF-8
....

=== Request HTTP Example

include::{snippets}/hello/http-request.adoc[]

== Response

=== Response HTTP Example

include::{snippets}/hello/http-response.adoc[]

include::{snippets}/hello/response-fields.adoc[]

이걸 사용해서 불러왔다.

대략적인 구조들은 문서보면 바로 이해가 되는것들이 많다.

= 는 마크다운에서의 #과 같다.

갯수에 따라 제목, 부제목, ... 순으로 가게 된다.

include로는 build/generated-snippets/hello/*.adoc 을 불러와서 해당하는 양식을 넣어주었다.

결과

스크린샷 2022-01-31 오후 11 52 47

유저에 대한 요청, 응답은 깃허브에서 볼 수 있고, 설정한대로 잘 동작하는걸 볼 수 있다.

이것을 사용하려면 일단 팀원들이 테스트 코드를 어느정도 작성할 줄 아는 팀원들이 있어야 가능하고,

결국 나는 이것을 업무에 도입 시킴으로써 유지보수 측면에서나, 코드에 대한 신뢰성 둘 다 잡을 수 있을거라고 판단했다.

profile
💻 많이 짜보고 많이 경험해보자 https://lsj8367.tistory.com/ 블로그 주소 옮김
post-custom-banner

0개의 댓글