연습코드는 깃허브에 있다.
기존에 있던 API 문서가 Swagger
또는 컨플루언스에 저장을 해두었었다.
바꾸려는 이유부터 설명하도록 하겠다.
우선 컨플루언스부터 얘기하자면 그냥 하나의 문서로 API를 공유하고 있었기 때문에,
깜빡하고 고치지 않은, 그러면서 최신화 된 문서도 약간씩 다른 데이터셋을 가지고 있는것들을 많이 봤다.
실제로 내가 고치고 나서도 유지가 안된것도 있었다. 😅
그래서 API문서를 따로 수정하지 않고 문서도 자동화할 수 있지 않은가 에 대해서 생각해봤다.
우선 스웨거도 기존에 사용하고 있었다고 했는데, 왜 바꾸려고 하냐면
우선 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값에 대한 형식을 적어주었다.
그렇게 해서 테스트코드를 돌리게 되면 성공하게 되고,
이런것이 생기게 된다.
그 다음으로는 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
을 불러와서 해당하는 양식을 넣어주었다.
유저에 대한 요청, 응답은 깃허브에서 볼 수 있고, 설정한대로 잘 동작하는걸 볼 수 있다.
이것을 사용하려면 일단 팀원들이 테스트 코드를 어느정도 작성할 줄 아는 팀원들이 있어야 가능하고,
결국 나는 이것을 업무에 도입 시킴으로써 유지보수 측면에서나, 코드에 대한 신뢰성 둘 다 잡을 수 있을거라고 판단했다.