DevOps

Swagger와 RestDocs의 장점을 합친 효과적인 API 문서화 (OpenApi Spec)

KAispread 2024. 2. 28. 18:43
728x90
반응형
반응형

개요

API 서버를 개발하다보면 협업을 위해 API 문서화를 진행하게됩니다. Spring 기반의 프로젝트에서 API 문서화 도구로 Swagger와 RestDocs를 가장 많이 사용하게 되는데요, 각 문서화 도구의 장단점은 다음과 같습니다.

구분 장점 단점
Swagger 아름다운 문서
 문서에서 API Test 가능
 Swagger 어노테이션이 비즈니스 코드와 섞임
 테스트 코드가 강제되지 않음
Rest Docs  문서화를 위해 테스트코드가 강제됨
테스트 기반으로 문서화되므로 비즈니스 코드가 깔끔해짐
아름답지 않은 문서
문서에서 API Test 불가능

 

저는 API 문서화를 위해 두 기술을 모두 사용해봤는데요, 확실히 각각의 장단점이 뚜렷했습니다. 그러던 도중, 카카오페이 기술 블로그에서 이 둘의 장점을 합친 문서화 방법을 정리한 포스팅을 발견하게 되었고 이후에는 이 방법으로 문서화를 진행했는데요, 이번 포스팅에서는 OpenApi Spec을 활용한 API 문서화 방법에 대해 정리해보려고 합니다.

 

 

OpenApi Specification

Swagger 팀이 SmartBear Software에 합류하면서 Swagger Spec.이 OpenApi Spec.으로 명칭이 바뀌었고 오늘날에는 RESTful API 스펙에 대한 표준으로서 활용되고 있다고 합니다.

OAS 문서 예시

OAS는 yaml 혹은 Json 형식으로 작성되고 Swagger-UI가 이를 해석해서 시각화해주는데요 위 사진은 제가 작년에 진행했던 프로젝트의 OAS 파일 예시입니다. OpenApi Spec의 더 자세한 내용에 대해 잘 정리된 포스팅이 있어 아래 링크로 공유해드리겠습니다.

https://devocean.sk.com/blog/techBoardDetail.do?ID=165186

Swagger-UI가 OAS 파일을 해석하여 API 문서를 시각화해주기 때문에 우리는 OAS 파일을 생성할 경로만 찾으면 됩니다. 다행히 epages라는 독일 기업에서 RestDocs Test를 통해 OAS를 만들어주는 오픈소스를 제공해주고 있습니다. 이 오픈소스를 사용하여 RestDocs Test로 OAS 파일을 생성하고, Swagger-UI가 이를 참조할 수 있도록 해주면 되는 것입니다.

 

GitHub - ePages-de/restdocs-api-spec: Adds API specification support to Spring REST Docs

Adds API specification support to Spring REST Docs - ePages-de/restdocs-api-spec

github.com

 

 

목차

개발 환경은 Java 17, Spring Boot 3.2.x 를 사용하였고 목차는 다음과 같습니다.

  1. Swagger-UI standalone 정적 파일 세팅
  2. build.gradle 수정
  3. SwaggerController 생성
  4. MockMvcRestDocs 테스트 코드 작성

 

Swagger-UI standalone 정적 파일 세팅

https://github.com/swagger-api/swagger-ui/releases/tag/v5.11.3

 

Release Swagger UI v5.11.3 Released! · swagger-api/swagger-ui

5.11.3 (2024-02-07) Bug Fixes spec: render response body for non-200 responses (#9555) (a88bed5), closes #9556

github.com

위 링크에서 Swagger-UI standalone 파일을 다운로드해주세요. 

 

압축을 풀고, dist 폴더만 프로젝트의 /resource/static 경로에 복사해줍니다.

 

// Resource Handler 설정
@Configuration
public class StaticRoutingConfig implements WebMvcConfigurer {

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

        registry.addResourceHandler("swagger-ui.html")
            .addResourceLocations("classpath:/static/dist/swagger-ui/");
    }
}

해당 resource를 참조할 수 있도록 정적 파일 경로에 맞게 Configuration 파일을 작성합니다.

 

다음 작업들을 진행합니다.

  • /resource/static/swagger-ui 에 새로운 패키지 생성
  • index.html 에서 swagger-ui.html으로 이름 변경
  • 불필요한 파일 삭제
    • oauth2-redirect.html
    • swagger-ui.js
    • swagger-ui-es-bundle-core.js
    • swagger-ui-es-bundle.js

 

추가적으로, swagger-initializer.js 파일에서 swagger 페이지가 참조할 OAS 파일의 url을 변경해주어야합니다.

 

모든 작업을 마치면 다음과 같은 형태가 됩니다.

 

 

build.gradle 수정

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

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
		testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

// #1 Set Swagger-ui description
openapi3 {
    setServers([
            {
                url = "http://localhost:8080"
                description = "local server"
            },
            {
                url = "다른 서버 IP"
                description = "Develop Server"
            },
    ])
    setTitle("Test service API docs")
    setDescription("OpenApi Specification 3.0 기반의 API 문서입니다")
    setVersion("0.0.1")
    setFormat("yaml") // or json
}

// #2 Add Gradle Task for generate .yaml file for API Doc
tasks.register('copyOasToSwagger', Copy) {
    delete "src/main/resources/static/swagger-ui/openapi3.yaml"
    from "${buildDir}/api-spec/openapi3.yaml"
    into "src/main/resources/static/swagger-ui/"
    dependsOn "openapi3"
}

build.gradle에 epages 오픈소스 의존성과 Task 들을 추가해줍니다.

  1. Swagger-UI에 표현될 문서 제목, 설명, 파일 포맷 등을 설정합니다.
    • setServers 배열에 여러 서버 IP를 추가할 수 있습니다. 보통 Local 서버와 개발 서버, 운영 서버를 번갈아가며 테스트 해보기위해 사용합니다.
  2. openapi3.yaml 파일 이름으로 OAS 파일을 생성해주기위한 Task를 정의합니다.
    • copyOasToSwagger라는 이름의 gradle task를 실행시키면 /resource/static/swagger-ui 경로에 OAS 문서가 생성됩니다.

 

build.gradle을 수정하고 빌드시키면 다음과 같이 copyOasToSwagger Task가 생성된 것을 확인하실 수 있습니다. copyOasToSwagger Task를 실행시켜보겠습니다.

 

openapi: 3.0.1
info:
  title: Test service API docs
  description: OpenApi Specification 3.0 기반의 API 문서입니다
  version: 0.0.1
servers:
- url: http://localhost:8080
  description: LOCAL server
tags: []
paths: {}
components:
  schemas: {}

Task를 실행하면 /resource/static/swagger-ui 에 다음과 같이 openapi3.yml 문서가 생깁니다. 해당 openapi.yml 문서가 Swagger-ui 를 통해 API 문서로 보여지는 것입니다.

 

 

SwaggerController 생성

@Controller
public class SwaggerUiController {

    @GetMapping("/swagger-ui")
    public String swaggerUi() {
        return "redirect:/static/dist/swagger-ui.html";
    }

}

swagger-ui.html 리소스에 접근하기 쉽도록 controller 하나를 만들어줍니다.

 

spring:
   mvc:
      static-path-pattern: /static/**

mvc가 인지하는 정적 파일 경로를 다음과 같이 수정해줍니다.

 

Spring 애플리케이션을 구동하고 localhost:8080/swagger-ui 경로에 접속해보면?

다음과 같이 openapi3.yml 파일을 참조하는 Swagger 페이지가 보여지는 것을 확인할 수 있습니다.

 

 

MockMvcRestDocs 테스트 코드 작성

간단한 회원가입 API에 대해 테스트 코드를 작성해보겠습니다. MockMvc 테스트와 RestAssured 테스트 둘 다 지원합니다.

우선, Controller Layer 테스트 설정을 다음과 같이 하나의 PresentationTestSupprot라는 하나의 클래스로 묶어줍니다.

@ExtendWith(RestDocumentationExtension.class)
@WebMvcTest({
    SignupController.class
})
public abstract class PresentationTestSupport {

    @Autowired
    protected ObjectMapper mapper;

    @MockBean
    protected SignupService signupService;

    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();
    }

    protected String toJson(Object data) throws JsonProcessingException {
        mapper.registerModule(new JavaTimeModule());
        return mapper.writeValueAsString(data);
    }
}

 

이후 PresentationTestSupport 클래스를 상속받는 테스트 클래스에 회원가입 API 테스트 코드를 작성합니다.

class SignupControllerTest extends PresentationTestSupport {

    @DisplayName("회원가입에 성공한다.")
    @Test
    void signup_success() throws Exception {
        // given
        SignupRequest request = SignupRequest.builder()
            .userId("userId123")
            .password("password123")
            .name("name123")
            .regNo("980112-1234567")
            .build();

        given(signupService.signup(any(SignupRequest.class)))
            .willReturn(1L);

        // when
        ResultActions perform = mockMvc.perform(RestDocumentationRequestBuilders.post("/szs/signup")
            .with(csrf())
            .content(toJson(request))
            .contentType("application/json"));

        // then
        perform
            .andExpectAll(
                status().isOk(),
                jsonPath("$.statusCode").value("OK"),
                jsonPath("$.message").value("회원가입 성공"),
                jsonPath("$.data").value(1L)
            );

        verify(signupService).signup(any(SignupRequest.class));

        perform
            .andDo(
                MockMvcRestDocumentationWrapper.document("회원가입",
                    ResourceSnippetParameters.builder()
                        .tag("회원 관련 API")
                        .summary("회원가입 API 입니다.")
                        .description(
                            """
                                회원 정보를 통해 회원가입을 진행합니다.
                            """),
                    requestFields(
                        fieldWithPath("userId").type(JsonFieldType.STRING).description("아이디"),
                        fieldWithPath("password").type(JsonFieldType.STRING).description("비밀번호"),
                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                        fieldWithPath("regNo").type(JsonFieldType.STRING).description("주민등록번호")
                    ),
                    responseFields(
                        fieldWithPath("statusCode").type(JsonFieldType.STRING).description("응답 상태"),
                        fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
                        fieldWithPath("data").type(JsonFieldType.NUMBER).description("회원 ID")
                    )
                ));
    }
}

여기서 RestDocs 와는 다르게, MockMvcRestDocumentationWrapper 클래스를 사용하여 문서 코드를 작성한다는 점에 유의해주세요

 

 

문서화 결과

문서화 결과를 확인해보겠습니다. 테스트 작성 이후, Intellij 우측 Gradle 탭이나 터미널에서 copyOasToSwagger Task를 실행시켜주세요

./gradlew copyOasToSwagger

 

다시 /resource/static/swagger-ui 경로에서 openapi3.yml 파일을 확인해보면? 

코드에 작성한 내용이 OAS 파일에 씌여진 모습

다음과 같이 코드로 작성한 내용이 OAS 문서에 쓰여진 것을 확인하실 수 있습니다.

 

이제 다시 Spring 애플리케이션을 구동시키고 localhost:8080/swagger-ui 에 접속해보면?

다음과 같이 OAS 파일의 내용들이 시각화되는 것을 확인할 수 있습니다.

 

 

Reference

https://tech.kakaopay.com/post/openapi-documentation/

https://github.com/ePages-de/restdocs-api-spec

https://devocean.sk.com/blog/techBoardDetail.do?ID=165186

https://github.com/swagger-api/swagger-ui/releases/tag/v5.11.3

728x90
반응형