API 자동화방법은 크게 2개가 있다
springdoc을 사용하면 바로 api 문서를 생성해줌Swagger가 쉽고 빠르게 적용이 가능하지만 어노테이션을 개발 코드에 덕지덕지 붙이는 것은 생각보다 매우 괴로운 일이다
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을 사용하기로 하였다.
일단 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버전 이상을 사용해야한다
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 작성을 위해서이다.
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의 사용법은 아래의 링크를 참고할 것
- https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#documenting-your-api
spring restdocs 공식 문서를 참조하면 좋...긴 한데 이게 묘하게 다른 부분이 많다...
BaseDocs나 CardDocs같은 것은 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-snippets와 build/api-spec에 파일들이 생성되었는지 확인해보자 (gradle기준)
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가 생성되었는 지 확인하자
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')
}
}