[API 문서화] restdocs-api-spec

rejs·2025년 2월 18일

API 자동화방법은 크게 2개가 있다

  • 1: Swagger
    springdoc을 사용하면 바로 api 문서를 생성해줌
    컨트롤러 및 DTO에 어노테이션을 붙여서 사용함
  • 2: Spring restdocs
    반자동이다.
    1) controller test 결과를 바탕으로 snippets를 생성(자동)
    2) snippets를 import해서 api 문서를 adoc으로 작성(수동)
    3) adoc으로 작성된 api 문서를 html로 빌드(자동)

Swagger가 쉽고 빠르게 적용이 가능하지만 어노테이션을 개발 코드에 덕지덕지 붙이는 것은 생각보다 매우 괴로운 일이다

restdocs-api-spec를 사용하여 두 방법의 하이브리드를 사용하고자 한다.

restdocs-api-spec의 구조

restdocs-api-spec는 restdocs가 생성하는 snippets를 바탕으로 openapi-spce문서를 작성해준다.

위에서 restdocs의 사용단계를 설명한바 있는데

1) controller test 결과를 바탕으로 snippets를 생성(자동)
2) snippets를 import해서 api 문서를 adoc으로 작성(수동)
3) adoc으로 작성된 api 문서를 html로 빌드(자동)

restdocs-api-spec를 사용하면 아래와 같이 사용할 수 있다

1) controller test 결과를 바탕으로 snippets를 생성(자동)
2) snippets를 바탕으로 openapi-spce에 맞는 문서를 생성(자동)
3) 2에서 생성한 문서를 바탕으로 swagger-ui 제공

3번은 swagger standalone을 쓰거나 아니면 swagger hub를 써도 좋지만, 이번 경우에는 swaggerdoc을 사용하기로 하였다.

restdocs-api-spec 적용하기

일단 snippets를 생성해줄 spring-restdocs를 적용해야한다

plugins {
	id 'com.epages.restdocs-api-spec' version "0.18.2"
}

dependencies {
	// springdoc 연계를 위해
	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
	// mockMvc를 사용하므로 
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
	testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
}

openapi3{
	server  = 'http://localhost:8080/'
	title = "Anki"
	description = "Anki 작업"
	version="1.0.0"
	format = "yaml"
}

spring3를 사용하는 경우 0.17.1버전 이상을 사용해야한다

1. ControllerTest로 api문서 만들기

MockMvc 세팅

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;

@Import(TestcontainersConfiguration.class) 
@Sql(scripts = "/data.sql", executionPhase= Sql.ExecutionPhase.BEFORE_TEST_CLASS) 
@Sql(scripts = "/drop.sql", executionPhase= Sql.ExecutionPhase.AFTER_TEST_CLASS) 
@ExtendWith(RestDocumentationExtension.class)
@ActiveProfiles("test")
@SpringBootTest
public abstract class AbstractControllerTest {
    @Autowired
    protected ObjectMapper objectMapper;

    @Autowired
    protected WebApplicationContext context;

    protected MockMvc mockMvc;

    @BeforeEach
    void setUp(final WebApplicationContext context, final RestDocumentationContextProvider restDocumentation){
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(documentationConfiguration(restDocumentation))
                .alwaysDo(MockMvcResultHandlers.print())
                .addFilters(new CharacterEncodingFilter("UTF-8", true))
                .build();
    }

}

Sql 어노테이션과 Import 어노테이션은 testcontainer와 데이터베이스 초기화를 위해 사용한다(restdocs와 관계없음)

@SpringBootTest는 WebApplicationContext이랑 objectMapper를 주입받기 위해서 사용한다.
(Mockito를 사용해서 컨트롤러에 대한 단위테스트를 수행해도 api 문서 작성에는 문제없는 것으로 안다)

앞서 말했듯이 restdocs-api-spec은 생성된 snippets을 바탕으로 swagger 문서를 작성하는데,
snippets를 작성하기 위해서 @ExtendWith(RestDocumentationExtension.class)가 꼭 필요하다.

mockMvc를 설정해주는 것도 snippets 작성을 위해서이다.

ControllerTest 예제

class DeckControllerTest extends AbstractControllerTest {

    @Test
    void getDecks() throws Exception {
        mockMvc.perform(
                get("/decks")
                        .param("languageCode", "EN")
                        .param("queryType", "By_Difficulty")
        ).andExpect(status().isOk())
                .andDo(
                        MockMvcRestDocumentationWrapper.document(
                                "{class-name}/{method-name}",
                                ResourceDocumentation.resource(
                                        ResourceSnippetParameters.builder()
                                                .tag("articles")
                                                .summary("Article을 모두 보기")
                                                .queryParameters(
                                                        parameterWithName("languageCode").description("언어코드"),
                                                        parameterWithName("queryType").description("의미에 따른 분류인가 / 난이도에 따른 분류인가")
                                                )
                                                .responseFields(
                                                        BaseDocs.combine(
                                                                BaseDocs.basePageResponse(),
                                                                CardDocs.deckDto(BaseDocs.basePageResponsePrefix)
                                                        )
                                                )
                                                .build()
                                )
                                )
                        )
        ;
    }
}

andDo 부분이 중요하다.
spring rest docs는 MockMvcRestDocumentation.document를 사용하는데,
restdocs-api-spec은 MockMvcRestDocumentationWrapper.document를 사용해주어야한다.

MockMvcRestDocumentationWrapper.document의 사용법은 아래의 링크를 참고할 것

BaseDocsCardDocs같은 것은 Schema들을 재사용하기 위해서 만든 class들인데, andWithPrefix가 안되서 아래와 같이 만들 수 밖에 없었다. 참고하길 바란다.

public class BaseDocs {
    private static FieldDescriptor[] _basePageResponse = new FieldDescriptor[]{
            fieldWithPath("size").description("content의 크기"),
            fieldWithPath("content").description("실제 데이터들"),
            fieldWithPath("page").description("현재 페이지의 번호"),
            fieldWithPath("pageSize").description("전체 페이지의 크기")
    };

    public static FieldDescriptor[] basePageResponse(){
        return _basePageResponse;
    }
    public static String basePageResponsePrefix = "content[].";

    public static FieldDescriptor[] combine(FieldDescriptor[] a, FieldDescriptor[] b){
        List<FieldDescriptor> ret = new ArrayList<>();
        Collections.addAll(ret,a);
        Collections.addAll(ret,b);
        return ret.toArray(FieldDescriptor[]::new);
    }
}
public class CardDocs {
    public static FieldDescriptor[] cardDto(String prefix){
        return new FieldDescriptor[]{
                fieldWithPath(prefix+"size").description("content의 크기"),
                fieldWithPath(prefix+"content").description("실제 데이터들"),
                fieldWithPath(prefix+"page").description("현재 페이지의 번호"),
                fieldWithPath(prefix+"pageSize").description("전체 페이지의 크기")
        };
    }

    public static FieldDescriptor[] deckDto(String prefix){
        return new FieldDescriptor[]{
                fieldWithPath(prefix+"category").description("카드 분류"),
                fieldWithPath(prefix+"languageCode").description("언어코드"),
                fieldWithPath(prefix+"cardCounts").description("덱에 포함된 카드 개수"),
        };
    }
}

여기까지 왔다면 ./gradlew openapi3 명령어를 실행하여 build/generated-snippetsbuild/api-spec에 파일들이 생성되었는지 확인해보자 (gradle기준)

2. springdoc 연계하기

기본생성되는 springdoc 문서 제거하기

springdoc:
  api-docs:
    enabled: false // api spce 기본 작성을 중단함
  swagger-ui:
    url: /docs/openapi3.yaml // 이 위치에 있는 swagger 파일을 사용함 

swagger-ui/url은 {host}/docs/openapi3.yaml를 사용한다.
localhost라면 localhost:8080/docs/openapi3.yaml에 우리가 원하는 swagger 설정파일이 들어있어야한다.

springdoc.api-docs.enabled=false 옵션을 주면 swagger를 위한 bean까지 모두 제거되기 때문에 수동으로 주입해주어야한다

@Configuration
public class SwaggerConfig implements WebMvcConfigurer {
    @Bean
    SpringDocConfiguration springDocConfiguration(){
        return new SpringDocConfiguration();
    }

    @Bean
    SpringDocConfigProperties springDocConfigProperties() {
        return new SpringDocConfigProperties();
    }

    @Bean
    ObjectMapperProvider objectMapperProvider(SpringDocConfigProperties springDocConfigProperties){
        return new ObjectMapperProvider(springDocConfigProperties);
    }

    @Bean
    SpringDocUIConfiguration SpringDocUIConfiguration(Optional<SwaggerUiConfigProperties> optionalSwaggerUiConfigProperties){
        return new SpringDocUIConfiguration(optionalSwaggerUiConfigProperties);
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/docs/**")
                .addResourceLocations("classpath:/docs/");
    }
}

resources/docs에 openapi3.yaml 파일을 넣은뒤 서버를 실행시켜서 localhost:8080/docs/openapi3.yaml에 접근할 수 있는지 확인해보자

접근 가능하다면 http://localhost:8080/swagger-ui.html에 접속해서 openapi3에 맞게 swagger ui가 생성되었는 지 확인하자

openapi3.yaml을 자동으로 복사하기

tasks.register('docs', Copy) {
	doFirst {
		delete file('src/main/resources/docs/openapi3.yaml')
	}
	from file('build/api-spec/openapi3.yaml')
	into file('src/main/resources/docs/.')
	dependsOn tasks.named('openapi3')
	dependsOn tasks.named('processResources')
	doLast{
		delete file('build/generated-snippets')
	}
}

0개의 댓글