본문 바로가기
Spring

Rest Docs의 템플릿 커스터마이징

by AlbertIm 2024. 9. 11.

시작

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이며 따라서 snippetorg.springframework.restdocs.templates.asciidoctor에서 로드됩니다.

 

각 템플릿은 생성하는 snippet의 이름을 따릅니다. 예를 들어 curl-request.adoc snippet의 템플릿을 오버라이드하려면 src/test/resources/org/springframework/restdocs/templates/asciidoctorcurl-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

댓글