시작
Rest Assured와 Spring REST Docs를 사용하면 REST API 문서를 자동으로 생성할 수 있습니다. 하지만 실제 프로젝트에 적용해 보니 기본적으로 제공되는 Rest Docs의 템플릿을 그대로 사용하기에는 부족한 부분이 있었습니다. 프로젝트에 맞게 문서를 좀 더 깔끔하게 관리하려면 커스터마이징이 필요했습니다. 이 포스트에서는 Rest Docs의 템플릿을 어떻게 커스터마이징 했는지 그리고 제가 직면했던 문제와 해결 방법을 공유하고자 합니다.
본문
1. 프로젝트 설정
build.gradle에 필요한 의존성입니다.
plugins {
// AsciiDoc 파일을 HTML로 변환하는 플러그인
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
configurations {
// AsciiDoc 확장 구성
asciidoctorExt
}
dependencies {
// Rest Assured 라이브러리 (API 테스트 용도)
testImplementation 'io.rest-assured:rest-assured:5.5.0'
// Spring REST Docs와 Rest Assured 통합
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
// Spring REST Docs와 AsciiDoc 통합
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}
ext {
// 문서 스니펫이 생성될 디렉토리 설정
snippetsDir = file('build/generated-snippets')
}
test {
// 테스트 결과로 생성된 스니펫 디렉토리를 출력
outputs.dir snippetsDir
}
asciidoctor {
// AsciiDoc 입력 디렉토리로 스니펫 경로 지정
inputs.dir snippetsDir
// AsciiDoc 확장 구성 사용
configurations 'asciidoctorExt'
// 테스트가 성공한 후 AsciiDoc 작업이 실행되도록 설정
dependsOn test
}
bootJar {
// bootJar 작업이 asciidoctor 작업 후에 실행되도록 설정
dependsOn asciidoctor
// AsciiDoc으로 생성된 출력 파일들을 Jar 파일에 포함시킴
from ("${asciidoctor.outputDir}") {
// 생성된 문서를 'static/docs' 경로로 복사
into 'static/docs'
}
}
처음에 제가 아래 코드를 보고 약간의 오해가 있었습니다.
// AsciiDoc으로 생성된 출력 파일들을 Jar 파일에 포함시킴
from ("${asciidoctor.outputDir}") {
// 생성된 문서를 'static/docs' 경로로 복사
into 'static/docs'
}
이 코드가 빌드 과정에서 AsciiDoc으로 생성된 문서가 build
디렉터리 내의 리소스 파일에 저장될 것이라고 생각했습니다. 그래서 빌드 후에 build
폴더 안에서 문서를 찾을 수 있을 거라고 예상했습니다. 그러나 실제로는 이 코드가 Jar
파일 안에 문서를 포함시키는 방식이었습니다. 즉 Spring Boot의 정적 리소스 경로인 static/docs
디렉터리에 파일이 저장되는 것이었죠.
따라서 애플리케이션을 빌드하고 실행한 후에는 /docs/index.html
경로를 통해 AsciiDoc으로 생성된 문서를 확인할 수 있습니다. 이 경로는 Spring의 정적 리소스 관리 규칙을 따르며 static
디렉터리에 있는 파일들이 /docs
경로로 자동으로 매핑됩니다.
2. RestDocs와 Rest Assured 설정
테스트 코드에서 RestDocs와 Rest Assured를 설정하는 방법에 대해 설명합니다.
@Import(TestcontainersConfiguration.class) // Testcontainers 설정을 가져옴 (Optional)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 랜덤 포트로 SpringBootTest 실행
@ExtendWith(RestDocumentationExtension.class) // RestDocs 확장을 위한 설정
public abstract class TodoAcceptanceTest { // 모든 인수 테스트의 상위 클래스
@LocalServerPort
public int port; // Spring Boot가 할당한 포트를 저장
@Autowired
protected DatabaseCleaner databaseCleanup; // 테스트 전 데이터베이스 초기화를 위한 클래스
protected RequestSpecification spec; // Rest Assured 요청 사양 설정
// 요청 본문을 보기 좋게 출력하는 Preprocessor 설정
public static @NotNull OperationRequestPreprocessor prettyPrintRequest() {
return Preprocessors.preprocessRequest(Preprocessors.prettyPrint());
}
// 응답 본문을 보기 좋게 출력하는 Preprocessor 설정
public static @NotNull OperationResponsePreprocessor prettyPrintResponse() {
return Preprocessors.preprocessResponse(Preprocessors.prettyPrint());
}
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
// Rest Assured의 요청 사양을 설정하고 RestDocs와 연동
this.spec = new RequestSpecBuilder()
.addFilter(documentationConfiguration(restDocumentation))
.build();
// 지정된 포트를 사용하도록 설정
spec.port(port);
RestAssured.port = port;
// 각 테스트 전에 데이터베이스를 초기화
databaseCleanup.execute();
}
}
Rest Assured를 사용하면 데이터가 Rollback 되지 않습니다. 또한 모든 인수 테스트마다 새로운 Spring Context와 데이터베이스를 실행하는 것은 부담이 될 수 있습니다. 따라서 매번 데이터베이스를 지우는 방법을 선택했습니다.
@Component
public class DatabaseCleaner {
// 테이블 이름을 저장할 리스트
private final List<String> tableNames = new ArrayList<>();
// JPA의 EntityManager를 사용하여 데이터베이스와 상호작용
@PersistenceContext
private EntityManager entityManager;
@PostConstruct
public void init() {
// 애플리케이션 실행 시점에 테이블 이름을 가져와 리스트에 저장
var tables = entityManager.getMetamodel().getEntities().stream()
// 각 엔티티 클래스의 @Table 어노테이션에서 테이블 이름 추출
.map(e -> e.getJavaType().getAnnotation(Table.class).name())
.toList();
// 테이블 이름을 리스트에 추가
tableNames.addAll(tables);
}
@Transactional
public void execute() {
// 현재 영속성 컨텍스트에 있는 변경 내용을 데이터베이스에 반영
entityManager.flush();
// 외래 키 체크 비활성화
entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate();
// 모든 테이블의 데이터를 삭제 (테이블 자체가 아니라 데이터만 비움)
for (String tableName : tableNames) {
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
// 외래 키 체크 다시 활성화
entityManager.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate();
}
}
각 엔티티에 @Table(name = ":name")
어노테이션을 추가하여 더욱 안전하게 테이블을 찾는 방법을 사용했습니다. 주의할 점은 DB sequence
는 초기화하지 않는다는 점입니다.
3. 문서화 커스터마이징
Spring REST Docs는 Mustache 템플릿을 사용하여 snippet
을 생성합니다. 템플릿은 클래스패스의 org.springframework.restdocs.templates
서브패키지에서 로드됩니다.
서브패키지의 이름은 사용 중인 템플릿 형식의 ID에 의해 결정됩니다. 기본 템플릿 형식인 Asciidoctor의 경우 ID는 asciidoctor
이며 따라서 snippet
은 org.springframework.restdocs.templates.asciidoctor
에서 로드됩니다.
각 템플릿은 생성하는 snippet
의 이름을 따릅니다. 예를 들어 curl-request.adoc
snippet
의 템플릿을 오버라이드하려면 src/test/resources/org/springframework/restdocs/templates/asciidoctor
에 curl-request.snippet
이라는 이름의 템플릿을 생성하면 됩니다. 상위 폴더인 src/test/resources/org/springframework/restdocs/templates/
에 생성하면 하위 서브패키지의 모든 템플릿을 오버라이드할 수 있습니다.
이 포스트에서는 총 4개의 snippet
을 커스터마이징 합니다.
path-parameters.snippet
[source,http,options="nowrap"]
----
{{path}}
----
{{#parameters.0}}
|===
|파라미터|설명
{{#parameters}}
|{{#tableCellContent}}`+{{name}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/parameters}}
|===
{{/parameters.0}}
query-parameters.snippet
|===
|파라미터|설명
{{#parameters}}
|{{name}}
|{{description}}
{{/parameters}}
|===
request-fields.snippet
[cols="1,1,1,2,3", options="header"]
|===
|필드명|타입|필수|설명|제약조건
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}선택{{/optional}}{{^optional}}필수{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
|{{#tableCellContent}}{{constraints}}{{/tableCellContent}}
{{/fields}}
|===
response-fileds.snippet
|===
|필드|타입|설명
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}
|===
4. 테스트 코드에 적용
인수 테스트 Steps: 테스트에서 그룹 프로젝트를 생성하는 방법을 보여주는 코드입니다
public static ExtractableResponse<Response> 그룹_프로젝트_생성(
long groupId, String name, String accessToken, RequestSpecification spec
) {
var body = new HashMap<>();
body.put("name", name);
this.spec.filter(createGroupProjectDocumentation());
return given(spec).log().all()
.auth().oauth2(accessToken)
.body(body)
.contentType("application/json")
.when()
.post("/groups/{groupId}/projects", groupId)
.then().log().all()
.extract();
}
Document 설정: 다음은 문서화 설정을 위한 메서드입니다
public static @NotNull RestDocumentationFilter createGroupProjectDocumentation() {
return document(
"groups/create-project", // 문서화 파일 이름
prettyPrintRequest(), // 요청 본문을 보기 좋게 출력하는 설정
prettyPrintResponse(), // 응답 본문을 보기 좋게 출력하는 설정
pathParameters(
parameterWithName("groupId").description("그룹 ID")
),
requestFields(
fieldWithPath("name").description("프로젝트 이름").attributes(
key("constraints").value(ValidationMessages.PROJECT_NAME_MESSAGE))
),
responseFields(
fieldWithPath("id").description("프로젝트 ID")
)
);
}
5. index.adoc 설정
이제 index.adoc
파일을 설정하여 생성된 API 문서를 통합합니다.
=== 그룹 프로젝트 생성 API
==== URL 경로
include::{snippets}/groups/create-project/path-parameters.adoc[]
==== 요청
include::{snippets}/groups/create-project/http-request.adoc[]
==== 요청 필드 상세
include::{snippets}/groups/create-project/request-fields.adoc[]
==== 응답
include::{snippets}/groups/create-project/http-response.adoc[]
==== 응답 필드 상세
include::{snippets}/groups/create-project/response-fields.adoc[]
6. 결과
마무리
Rest Docs의 템플릿 커스터마이징을 사용하여 API 문서를 더욱 원하는 형식으로 작성할 수 있었습니다. Rest Docs는 Swagger에 비해 기능적인 면에서는 부족할 수 있지만 코드의 깔끔함을 유지할 수 있는 장점이 있습니다.
참고자료
'Spring' 카테고리의 다른 글
GitHub Repository에 Coveralls 배지 적용하기 (0) | 2024.09.11 |
---|
댓글