프로젝트를 진행하면서 프론트에 API 명세서를 간결하게 전달하기 위한 방법이 어떤게 있을까 생각을 했다.
대표적으로 Postman, Swagger는 모두 API를 개발하고 테스트하는 데 사용되는 도구이다. 그러나 이들은 주요한 차이점이 있다.
Postman : API 테스팅 도구로 누구나 들어본 적이 있을 것이다. 개발자들은 Postman을 사용하여 API 요청을 보내고 응답을 받아볼 수 있다. 하지만 Postman은 API 문서화 기능에 제약이 있으며, 대부분의 경우 수동으로 작업해야 한다.
Swagger : RESTful 웹 서비스의 설계, 구축, 문서화 및 테스트를 지원한다. Swagger UI를 사용하면 개발자들은 대화식 API 문서를 생성하고 배포할 수 있고, 실시간으로 업데이트되며 외부에 공개할 수 있는 형태로 만들어진다.
프로젝트의 협업을 위해 선택했다. 프론트에서 "명세서 첨부해서 주세요." 하면 Postman API 스펙을 캡쳐해서 주거나 Excel로 표를 만들어 주었던 과거의 개발자답지 못한 행동을 저질렀다.
따라서, Spring Rest Docs는 실제 코드와 연계하여 정확하고 최신 상태의 API 문서를 제공함으로써 개발의 생산선을 올려주기 때문에 선택하였다.
"Spring" Rest Docs인만큼 Spring 프레임워크와 강력하게 통합된다.기존 프로젝트 구조나 설정과 원활하게 연동하여 사용할 수 있어 Spring Boot를 사용하는 프로젝트라 사용하기 수월했다.
REST Docs는 API 문서에 포함(include)되는 "스니펫(snippets)"을 생성하기 위해 테스트 코드를 작성한다. 테스트 코드를 작성하지 않으면 스니펫을 얻을 수 없고, 스니펫을 얻지 못하면 API 문서에 포함시킬 수 없다. 고로, API 문서를 제공하기 위해서 반드시 테스트 코드를 작성해야 한다.
스니펫은 API의 각 부분(요청 또는 응답의 일부)을 설명하는 작은 조각의 문서다. 예를 들어 HTTP 요청의 경로, 파라미터, 헤더 등과 같은 부분들을 설명하는 스니펫이 있다. 이러한 스니펫들은 결합되어 하나의 완전한 API 문서를 구성한다.
REST Docs는 Spring MVC 테스트 프레임워크를 이용해서 테스트를 작성하고 스니펫을 생성한다. 이렇게 테스트를 기반으로 한 접근을 통해서 서비스 안정성을 보장하고, 성공한 테스트 코드는 대상 API 문서에 반영되므로 신뢰감을 제공스니펫에 오류가 있는 경우 테스트는 실패한다.
테스트 환경
plugins {
id 'org.asciidoctor.jvm.convert' version '3.3.2'
// gradle 7 부터는 asciidoctor.jvm.convert를 사용
}
configurations {
asciidoctorExtentions
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
asciidoctorExtentions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
// mockMvc 사용
testImplementation group: 'org.mockito', name: 'mockito-inline', version: '4.6.1'
}
ext {
set('snippetsDir', file("build/generated-snippets"))
}
test {
outputs.dir snippetsDir
useJUnitPlatform()
}
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExtentions'
dependsOn test // asciidoctor 작업이 test 작업에 의존.
// 테스트가 성공적으로 수행된 후에만 asciidoctor 작업이 실행.
sources{
include("**/index.adoc","**/common/*.adoc")
// 변환할 AsciiDoc 파일들을 지정
}
baseDirFollowsSourceFile()
}
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
/** build/docs/asciidoc 파일을 src/main/resources/static/docs로 복사 **/
}
build {
dependsOn copyDocument
}
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
/**
스니펫을 이용해 문서 작성 후,
build - docs - asciidoc 하위에 생기는 html 파일을 /static/docs로 복사
**/
}
Rest Docs 설정에 필요한 코드만 수정하여 넣었다.
이번 프로젝트에서 맡은 Card 도메인이다. (Entity, Repository, Service는 생략)
@RestController
@RequestMapping("/api/card")
@RequiredArgsConstructor
@Slf4j
@Builder
public class CardController {
private final CardService cardService;
private final CustomMapper customMapper;
@PostMapping("/")
@SlackNotification
public ResponseEntity<Response> createCard(@MemberId Long memberId,
@RequestBody Request request) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(customMapper.map(cardService.create(memberId, request), Response.class));
}
@GetMapping("/")
@SlackNotification
public ResponseEntity<Response> getByMemberId(@MemberId Long memberId) {
return ResponseEntity
.status(HttpStatus.OK)
.body(customMapper.map(cardService.getByMemberId(memberId), Response.class));
}
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith({RestDocumentationExtension.class})
public class MainIntegrationTest {
@Test
@DisplayName("카드 생성 Http Status Code 201")
void registerInCard() throws Exception {
//given
CardDto.Request request = registerRequest();
// when
ResultActions resultActions = mockMvc.perform(post(CARD_API_PATH + "/")
.header(HttpHeaders.AUTHORIZATION, token)
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print()); // --- (1)
// then
resultActions
.andExpect(status().isCreated())
.andDo(document("card-create", // --- (2)
preprocessRequest(prettyPrint()), // --- (3)
preprocessResponse(prettyPrint()), // --- (3)
requestHeaders( // --- (4)
headerWithName(HttpHeaders.AUTHORIZATION).description("엑세스 토큰")
),
requestFields( // --- (5)
getCardRequests()
),
responseFields( // ---(6)
getCardResponses()
)
));
}
}
protected static List<FieldDescriptor> getCardRequests() {
return List.of(
fieldWithPath("number").type(JsonFieldType.STRING).description("카드 번호"),
fieldWithPath("cardPassword").type(JsonFieldType.STRING).description("카드 비밀번호"),
fieldWithPath("validThru").type(JsonFieldType.STRING).description("카드 유효연월")
);
}
protected static List<FieldDescriptor> getCardResponses() {
return List.of(
fieldWithPath("cardId").type(JsonFieldType.NUMBER).description("카드 Id"),
fieldWithPath("number").type(JsonFieldType.STRING).description("카드 번호"),
fieldWithPath("cardPassword").type(JsonFieldType.STRING).description("카드 비밀번호"),
fieldWithPath("validThru").type(JsonFieldType.STRING).description("카드 유효연월"),
fieldWithPath("createAt").description("생성 시간")
);
}
🚨 Rest Docs는 테스트 통과 시 문서가 생성된다고 강조했다. (어떻게든 통과를 시켜야한다)
Gradle에서 설정했던 디렉토리에 스니펫이 생성되었다.
*.aodc 파일을 하나의 문서로 통합시키기 위해 index.adoc에 include:::파일명.adoc[]를 추가한다.
API 문서가 우측과 같이 출력되지 않는다. 생성된 문서는 실제로 동작하는 성공한 API에 대한 정보를 반영하는 걸 알 수 있다. 이러한 점이 신뢰감을 제공한다고 말할 수 있겠다.
Spring Rest Docs는 실제 코드와 연동되어 정확성과 일관성이 높은 API 문서 생성에 중점을 둔 도구이다.
"개발자는 협업을 해야한다. 소통이 중요하다." 라는 말은 어려웠다. 대면 회의를 자주 해야하는건지? 역할 분담을 잘 해야 하는건지? 추상적이다. Rest Docs는 개발 지원 도구이지만 이로 인해 협력 방식을 단순화하고 생산성과 효율성을 높일 수 있는게 협업의 시작이 아닐까.
링크:
https://tech.kakaopay.com/post/openapi-documentation/
https://helloworld.kurly.com/blog/spring-rest-docs-guide/