API 명세서는 애플리케이션의 동작, 소통을 위해 빠질 수 없는 부분이다.
API 명세서(문서)를 통해 API의 기능, 사용방법, 제한사항 등을 명확하게 이해할 수 있다.
여러 작업자가 함께 일한다면(ex. 프론트엔드 - 백엔드) API 문서를 통해 API를 올바르게 호출하고 필요한 데이터를 교환할 수 있다.
제대로된 명세서가 있다면 빠른 소통이 가능해진다.
그러나, 기능이 추가/변경될 때 API 명세서도 지속적으로 업데이트 되지 않는다면 제대로 된 소통이 어려워진다.
실제로 지금 구현하던 애플리케이션에서도 수정이 생겼을 때마다 명세서를 수정하다가, 어느순간 기능 구현만 신경쓰고 문서화를 놓치게 되었다. 그래서 문서화를 수동으로 작성하기 보다는 자동화하는 것이 낫겠다는 생각을 했다.
문서 자동화를 할 수 있는 툴은 여러가지가 있다. 그 중 대표적인 것이 Swagger와 RestDocs이다.
나는 그 중 RestDocs를 선택했는데, 이유는 크게 두가지이다.
RestDocs는 테스트 기반으로 문서화 작업이 이루어지므로 더 신뢰할 수 있다고 생각했다.
그리고, 테스트 코드에 관련 설정이 들어가므로 프로덕션 코드의 수정이 없다는 점이 좋다고 느꼈다.
어떤 툴을 사용할지는 각각의 장단점이 있으므로 본인이(혹은 팀이) 중요하게 생각하는 부분에 따라 결정하면 될 것 같다.
RestDocs는 공식문서가 정말정말정말 친절하게 되어있었다. 그래서 이번 글은 공식문서를 천천히 따라해보며 각 설정이 어떤 역할들을 하는지 알아보고자한다. (직접 공식문서를 따라해보는 것도 학습에 많은 도움이 될 듯 하다.)
1. build.gradle 파일 설정
아래 내용은 공식문서의 설정파일과 각 설명을 정리해놓은 것이다. 내용을 보면 Asciidoctor라는 단어가 계속 등장하는데, Asciidoctor는 AsciiDoc문서 형식을 HTML, PDF 등 여러 형식으로 변환하는 Ruby기반 텍스트 프로세서라고 한다. AsciiDoc는 사람이 읽을 수 있는 일반 텍스트 형식으로, 문서를 작성하기 위한 마크업 언어를 의미한다. 세부 내용이 궁금하다면 링크(공식문서)를 참고하면 된다.
plugins { // (1) Asciidoctor 플러그인을 적용한다
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
configurations {
asciidoctorExt // (2) Asciidoctor를 확장하는 종속성에 대한 구성을 선언한다
}
dependencies {
// (3) spring-restdocs-asciidoctor 의존을 추가한다. 스니펫의 속성을 .adoc에서 자동으로 구성할 수 있다.
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
// (4) RestDocs의 RestAssured 모듈을 추가하는 설정이다.
// RestAssured를 사용해 테스트를 작성할 때 자동으로 API 문서를 생성할 수 있다.
// 만약 mockmvc를 쓴다면, restdocs:spring-restdocs-mockmvc를 추가한다.
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
}
ext { // (5) 생성된 스니펫이 저장될 위치를 설정한다.
snippetsDir = file('build/generated-snippets')
}
test { // (6) 테스트 작업이 실행될 때 snippetsDir에 출력이 기록된다는것을 알리는 설정이다.
outputs.dir snippetsDir
}
asciidoctor { // (7) Asciidoctor 작업을 구성한다.
inputs.dir snippetsDir // (8) Asciidoctor의 작업이 snippetsDir에서 값을 가져온다는것을 지정한다.
configurations 'asciidoctorExt' // (9) Asciidoctor의 작업이 asciidoctorExt의 구성을 사용하도록 지정한다.
dependsOn test // (10) Asciidoctor의 작업이 test 작업에 의존되도록 한다. (문서 생성 전에 테스트가 실행되도록 한다.)
}
여기에 나는 몇 가지 추가 설정을 해주었는데, 그 설정은 다음과 같다.
asciidoctor.doFirst { // 이전에 진행된 내용이 있다면 중복을 방지하기 위해 삭제한다.
delete file('src/main/resources/static/docs')
}
asciidoctor { // 문서를 생성할 때 아래 설정한 경로로 저장되도록 한다.
outputDir file('src/main/resources/static/docs')
}
bootJar { // 프로젝트를 Jar 파일로 생성할 때 생성된 RestDocs가 "static/docs" 경로로 들어가도록 한다.
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
2. 테스트 설정
JUnit5 테스트 설정은 다음과 같다. RestDocumentationExtension을 적용한다. 나는 컨트롤러 클래스들은 통합테스트로, 아래의 IntegrationTest를 모두 상속받고 있다. 그래서 IntegrationTest에서 한번에 설정을 적용했다.
@ExtendWith(RestDocumentationExtension.class)
public abstract class IntegrationTest {
...
}
3. RestAssured를 구성하는 방법을 설정한다.
초기 설정에서는 Json을 예쁘게 출력하는 기능만 설정해주었다. 문서 생성 시 Json 요청/응답이 너무 길어지면 읽기가 힘들어져서, 필드에 따라 개행이 적용될 수 있도록 했다.
@ExtendWith(RestDocumentationExtension.class)
public abstract class IntegrationTest {
// 상속받은 클래스의 각 테스트 메서드에서 사용하기 위해 protected로 설정했다.
// HTTP 요청을 보내기 위한 사전 설정 정보를 정의하는데 사용된다. (헤더, 본문, 인증 정보 등)
protected RequestSpecification spec;
@BeforeEach
void setUp(RestDocumentationContextProvider restDocumentation) {
// 원하는 설정을 적용하기 위해 정의한 필터이다.
// documentationConfiguration(restDocumentation)부분을 통해 RestDocs 설정을 초기화한다.
Filter RestAssuredConfig = documentationConfiguration(restDocumentation)
.operationPreprocessors() // 요청과 응답을 처리하기 전에 적용할 preprocessor를 설정한다. 요청/응답을 문서화하기 전에 수정/가공할 수 있다.
.withRequestDefaults(prettyPrint()) // Json이 길어지면 가로로 길어져 읽기 어려우므로, 보기 좋게 형식화 될 수 있도록 추가했다.
.withResponseDefaults(prettyPrint());
// RequestSpecBuilder는 RestAssured 클래스 중 하나로, HTTP 요청의 사양(spec)을 설정한다.
// addFilter에서 위에서 생성한 필터를 추가한다.
this.spec = new RequestSpecBuilder().addFilter(RestAssuredConfig)
.build();
}
...
}
4. Restful Service에 적용(테스트 코드에 적용)
이번 포스팅은 문서화에 대한 내용이므로 RestAssured 테스트 방법에 대한 내용은 다루지 않았다.
.filter() 에는 RestDocumentationFilter를 지정해주었는데, 이 필터는 테스트 중의 요청과 응답을 캡쳐해서 문서화하는 역할을 한다.
class MemberApiControllerTest extends IntegrationTest {
@DisplayName("회원가입에 성공하면 201 응답과 Location 헤더에 리소스 저장 경로를 받는다.")
@Test
void signup() {
MemberSignUpRequest memberSignUpRequest = new MemberSignUpRequest("회원이름", "user@email.com", "1234");
RestAssured.given(spec).log().all() // spec을 적용한다.
.filter(SIGN_UP.getFilter()) // 적용될 RestDocumentationFilter 설정
.contentType(ContentType.JSON)
.body(memberSignUpRequest)
.when()
.post("/members")
.then().log().all()
.statusCode(201)
.header("Location", "/members/1");
}
}
나는 구현한 API 목록이 많아서 RestDocumentationFilter를 각각 작성하기 보다는 한군데에서 모아서 관리하는게 좋겠다고 생각했고, Fixture와 Enum을 고민하다가 Enum 방식을 사용했다.
document()는 API 문서를 생성하는데 사용되는데, 첫번째 인자인 "signup"은 생성될 문서의 파일 이름을 지정한다. 두번째 인자는 요청, 응답들의 속성을 나타낸다. 나는 자세한 설명을 위해 requestFields, responseHeaders, requestCookies, responseCookies, responseHeaders, pathParameters 등의 설정을 모두 추가해주었다. 각 필드의 이름과 설명을 정의할 수 있다.
public enum RestDocsFilter {
SIGN_UP(
document("signup",
requestFields(
fieldWithPath("name").description("회원 이름"),
fieldWithPath("email").description("회원 이메일"),
fieldWithPath("password").description("회원 비밀번호")
),
responseHeaders(
headerWithName("Location").description("리소스 URI")
)
)
),
...
;
private RestDocumentationFilter filter;
RestDocsFilter(RestDocumentationFilter filter) {
this.filter = filter;
}
public RestDocumentationFilter getFilter() {
return filter;
}
}
이제 문서화할 준비는 끝이 났다. 테스트가 정상적으로 통과하면 지정된 디렉토리에 기본적으로 6개의 스니펫이 자동으로 생성된다.
만약 위의 filter에서 cookie, header등 다른 속성도 추가했다면 해당 스니펫도 자동으로 생성된다.
5. 문서 작성 (스니펫 사용)
생성된 스니펫을 사용하기 위해 소스파일을 생성해야한다. .adoc(AsciiDoc) 확장자를 가진 파일을 생성하면 된다.
src/docs/asciidoc/*.adoc 위치에 생성했다.
생성한 파일의 형태는 아래와 같다. 예쁜(?) 출력을 위해 몇가지 설정을 추가했다.
= // 애플리케이션 명세 제목을 나타낸다.
:toc: left // 목차(Table of Contents)를 왼쪽에 생성한다.
:toclevels: 2 // 목차 제목 수준을 설정한다. 2단계까지(==, ===) 포함시켰다.
:toc-title: API 목록 // 목차의 제목을 나타낸다.
:sectnums: // 섹션 번호를 자동으로 생성하도록 한다.
:source-highlighter: // highlightjs 소스코드에 highlightjs를 사용하도록 설정한다. 코드를 조금 더 예쁘게 만들어준다.
== 회원 // 회원이라는 제목의 섹션을 시작한다. (2단계 제목)
=== 회원가입 // 회원가입이라는 회원의 하위 섹션을 시작한다. (3단계 제목)
// 이 부분이 실제 들어갈 내용을 나타낸다. signup 이름의 API 작업을 참조하고, [] 부분에 사용할 스니펫을 지정한다.
operation::signup[snippets="http-request,request-fields,http-response,response-headers"]
Jar 파일을 빌드하면 설정에 따라 docs/index.html 파일이 생성되고, 자동으로 생성된 문서는 아래와 같다.
원하는대로 깔끔한 API 명세서가 완성되었다!
명확한 명세서를 위해서는 성공, 실패에 대한 모든 테스트 케이스가 있어야하고 직접 설정을 해주어야한다는 복잡함(귀찮음)이 있지만, 대신 테스트도 더 꼼꼼하게 짤 수 있고 문서도 최신화 할 수 있어서 좋은 것 같다. :)
+ 학습한 내용을 정리한 글입니다. 혹시 잘못된 부분이나 설명이 있다면 말씀 부탁드립니다. ☺️
'우아한테크코스 > 레벨 2 - Spring' 카테고리의 다른 글
[Spring] RestClient, MockRestServiceServer로 단위 테스트하기 (1) | 2024.06.02 |
---|---|
[Spring, JUnit5] 테스트 격리: DB 초기화 (InitializingBean, BeforeEachCallback) (0) | 2024.05.25 |
[JPA] @Embedded, @Embeddable 개념과 사용법 (0) | 2024.05.16 |
[Spring] HandlerMethodArgumentResolver 사용하기 (0) | 2024.05.09 |
[Spring] DTO 검증하고 결과 알려주기 with @Valid, @ExceptionHandler (0) | 2024.05.03 |