[spring] API 문서 자동화 : Spring rest docs + Open API Specification(OAS)

Minyoung kim·2024년 11월 27일
1

Spring

목록 보기
6/9

API 문서화

api를 문서화하는 방법은 매우 다양하다.
지금하고 있는 프로젝트의 api 명세 방법으로는 postman을 사용해 왔다. 그런데 쓰면서도 굉장히 불편함을 느꼈다.

  • api를 등록하기 위해 일일히 url을 다 작성해줘야 했던 점
  • api가 수정이 될 때 마다 일일히 postman api 명세도 다시 작성해줘야 한다는 점
  • 바뀐 api를 postman에서 수정을 했었는지 확인이 되지 않을때가 많아 실제 api와 api 명세가 동기화되지 않는 문제도 발생.
  • 프론트에서 최신화 되지 않은 api 문서를 통해 작업을 진행하며 문제 발생.

그래서 이건 아니다 싶어서 새로운 방법을 찾아봤다.

내가 찾은 것은 3가지 방법이 있는데, 각각을 설명하고 내가 택한 방법을 작성해보도록 하겠다.

Swagger

Swagger UI 의 장점

  • 직관적인, 아름다운 UI
    -> 스웨거는 API 에 대한 요청과 응답 등을 시각적으로 표현하여 사용자가 쉽게 이해할 수 있다.
  • API 엔드포인트에 대한 실시간 테스트를 제공.
  • Controller에 몇 가지의 어노테이션을 달기만 해도 API 문서가 만들어지는 편리함이 존재.

Swagger UI 의 단점

  • 어노테이션 수동 기입 기반 API
    -> 어노테이션 등을 수기로 기입하여 문서를 생성하기에, 코드와 문서간의 불일치가 발생할 수 있다.
  • 유지 보수의 문제성
    -> API 변경시마다 스웨거 어노테이션을 수정해야한다.
  • 코드가 더럽다
    -> 어노테이션때문에 불필요하게 컨트롤러단과 DTO 단에서 피로함이 가중된다.
  • Test를 강제하지는 않기에 문서의 신뢰도를 높게 유지하기 어려운 문제가 있다.

Spring Rest Docs

Spring Rest Docs 의 장점

  • 테스트 코드를 기반으로 문서를 생성하기에 코드와 문서간 일관성 유지가 가능하며 높은 신뢰성을 보장한다.

  • 어노테이션을 사용하지 않기에, 코드 런타임 단계에서 오버헤드가 존재하지 않는다.

  • 비즈니스 소스코드에 영향 없다.

Spring Rest Docs 의 단점

  • API 테스트 UI 의 부재
    -> 스웨거 처럼 API 를 실시간으로 테스트할 수 있는 기능이 존재하지 않는다
  • 아름답지 못한 UI..

따라서 두 방법 모두 장단점이 뚜렷하기에, 두 방법의 장점들을 활용하여 새로운 api 문서화 방법이 생겨났다.

OpenApi Specification(OAS) 기반 API 문서화

Swagger 팀이 SmartBear Software에 합류하면서 Swagger Spec.이 OpenApi Spec.으로 명칭이 바뀌었고 오늘날에는 RESTful API 스펙에 대한 사실상의 표준으로서 활용되고 있다고 한다. Swagger-UI는 이 OAS를 해석하여 API 스펙을 시각화해준다. 또한 Postman, Paw 같은 API Client들도 OAS를 지원하고 있어 OAS의 활용도가 다양한 것을 알 수 있다.

독일 기업 epages에서 Spring REST Docs를 연동하여 OAS 파일을 만들어주는 오픈소스(restdocs-api-spec)를 제공하고 있다. 이 오픈소스를 이용해서 OAS 파일을 생성하고 Swagger-UI로 띄우면 된다.

즉, test code를 기반으로 api를 문서화하는 spring rest docs의 방법과 마찬가지로, test 코드와 함께 api 문서에 대한 specification을 작성해주면 이 코드를 바탕으로 OAS 문서가 생성된다. 그리고 이 OAS 문서를 해석하여 api specification을 Swagger의 UI로 시각화 해준다.

Settings

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.4.0'
	id 'io.spring.dependency-management' version '1.1.6'
	id 'com.epages.restdocs-api-spec' version '0.18.2'
}

build.gradle에 pluging을 설정해준다.


testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'

마찬가지로 openapi3에 대한 명세도 작성해준다. 이 항목은 custom해주면 된다.
나는 openapi3를 사용했지만 postman과 같은 다른 것도 지원을 하고 있다.

openapi3 {
	server = 'http://localhost:8080'
	server =
	title = 'My API'
	description = 'My API description'
	version = '0.1.0'
	format = 'yaml'
}

그리고 위와 같이 dependency를 설정해준다.

이 상태에서 gradle road를 하면 아래와 같이 gradle Tasks - documentation에 openapi, openapi3, Postman Task가 기본으로 추가된 모습을 확인할 수 있다.

Test 코드 없이 openapi3을 실행하면 /build/api-spec 디렉터리가 생성되고 그 아래 OAS 기본 구조가 작성된 openapi3.yaml 파일이 생성된다. test code가 없기 때문에 위에 작성한 기본값만 채워져 있고 나머지는 비워져 있을 것이다.

tasks.register('copyOasToSwagger', Copy) {
	// 기존 yaml 파일 삭제
	delete 'src/main/resources/static/swagger-ui/openapi3.yaml'

	// 복제할 yaml 파일 타겟팅
	from "build/api-spec/openapi3.yaml"

	// 타겟 디렉토리로 파일 복제
	into 'src/main/resources/static/swagger-ui'

	//test가 먼저 실행되도록 설정
	dependsOn tasks.named('test')

	// openapi3 task가 먼저 실행되도록 설정
	dependsOn tasks.named('openapi3')

}

이 때 위와 같이 gradle에 작성을 해주면 된다.

static/swagger-ui에 openapi3.yaml이 존재한다면 이를 삭제하고,
build/api-sepc/openapi3.yaml에 생성된 oas 파일을 static/swagger-ui 아래로 복제하는 내용이다.

dependsOn을 작성해주면 특정 task가 실행된 이후에 위 task를 실행하도록 순서를 지정해줄 수 있다.
따라서 copyOasToSwagger를 실행하면, test가 실행되고 openapi3가 실행되고 (oas 파일 생성) 이후 copyOasToSwagger를 통해 생성된 oas 파일이 지정한 static 경로 아래로 복제되게 된다.

gradle을 road하면 copyOasToSwagger task가 생성된다.
test code를 모두 작성하고 api 문서를 update하고 싶다면, 앞으로 이 task를 실행해주면 된다.

MockMvc 와 RestAssured

MockMvc

  • MockMvc는 @WebMvcTest를 사용하여 testing을 수행한다.
    또한 Controller Layer 만 테스트 하기에 속도가 빠르다.

RestAssured

  • BDD 스타일로 작성되어 직관적이다.
  • RestAssured는 application context 전체를 로드해 bean으로 주입하여 test를 진행하기 때문에 속도가 느리다.
    -> @SpringBootTest를 통해 진행한다.
  • 통합 테스트 시 유리하다.

나는 RestAssured를 사용해 보았다. RestAssured를 사용한다고 해도 mockMvc에 대한 의존성을 함께 추가해줘야 한다.

Resource 준비하기

swagger UI를 구성하기 위해서 아래 사이트에서 정적 파일을 다운로드 받는다.
https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/

latest release를 다운 받아 /dist 디렉터리 내부 파일만 복사를 해주면 된다. 나는 resources/static/swagger-ui 라는 폴더를 두고 그 아래 파일들을 위치시켰다.

받아온 파일 수정하기

1) Swagger file 수정

  • index.html을 swagger-ui.html으로 이름 변경
  • 내부 js, css 경로를 static routing으로 적용
    SwaggerUIBundle 경로는 생성될 yaml 파일의 경로로 입력
    2) 불필요한 파일 삭제
  • oauth2-redirect.html
  • swagger-ui.js
  • swagger-ui-es-bundle-core.js
  • swagger-ui-es-bundle.js

OAS 파일 생성하기

SampleController

package org.example.rest_api_docs_test.Controller;

import org.example.rest_api_docs_test.Dto.SampleDto;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

@Controller
public class SampleController {

    @ResponseBody
    @GetMapping("/{id}")
    public SampleDto getSomething(@PathVariable Integer id, @RequestParam String name){

        return new SampleDto(id,name);
    }
}

위와 같은 간단한 controller가 있다고 가정해보자.

ControllerTest

아래와 같이 test code를 작성해보았다.


@DisplayName("상품 관리 API 테스트")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(RestDocumentationExtension.class)
public class ControllerTest {

    protected RequestSpecification spec;

    @LocalServerPort
    int port;

    // API 스펙 설정
    @BeforeEach
    void setUp(RestDocumentationContextProvider restDocumentation) {
        RestAssured.port = port;
        this.spec = new RequestSpecBuilder().addFilter(documentationConfiguration(restDocumentation))
                .build();
    }

    @Test
    @DisplayName("id로 멤버 조회")
    void findById() throws Exception {

        RestAssured.given(spec).log().all()
                .filter(RestAssuredRestDocumentationWrapper.document("sample test",
                       ResourceSnippetParameters
                           .builder()
                           .tag("Get path param")
                           .summary("path param을 가져옵니다")
                           .description("이렇게 하면 됩니둥")
                      ,pathParameters(RequestDocumentation.parameterWithName("id").description("memberId"))
                      ,queryParameters(RequestDocumentation.parameterWithName("name").description("member 이름"))
                      ,responseFields(
                              fieldWithPath("id").description("memberId"),
                              fieldWithPath("name").description("member 이름"))))
                .contentType(ContentType.JSON)
                .when()
                .get("/{id}?name={name}", "70","kim")
                .then().equals(new SampleDto(70,"kim"));
    }
}
  • @SpringbootTest
    ->RestAssured를 가지고 test 코드를 작성했기 때문에, 어플리케이션 컨텍스트 내부에 있는 모든 bean들을 가져와 주입하는 것이 필요하다.
    -> RANDOM_PORT : test 코드를 실행할 서버의 port를 랜덤으로 지정해준다.

  • @ExtendWith(RestDocumentationExtension.class) :
    RestAssuredRestDocumentationWrapper를 사용하기 위해서 필요하다.

  • setUp :
    api의 spec을 설정해준다. restDocs를 만들기 위한 filter를 추가해준다.

  • given(spec)
    : api 요청을 하기 전에 조건을 작성한다. 위에서 작성해준 specification을 전달한다.

  • ResourceSnippetParameters :
    api에 대한 전반적인 명세를 작성할 수 있다. api의 이름, 요약, 설명을 작성해준다.

  • pathParameters
    : 요청 url의 pathParameter에 대한 명세를 작성한다.

  • queryParameters
    : 요청 url의 queryParamter에 대한 명세를 작성한다.

  • responseFields
    : 응답에 대한 명세를 작성한다. fieldWithPath는 json으로 반환했을 때 각 key의 값이다.

-> pathParam과 queryParam,에 들어가는 parameterWithName에는 실제 이름과 동일하게 작성해줘야 한다. 만약, uri가 http://localhost:8080/{id}와 같이 get 요청을 지정했다면, paramterWithName에는 id를 작성해줘야 한다. 그렇지 않으면, 요청 parameter에 대한 명세를 작성해주지 않았다는 오류를 반환하게 된다.
responseFields에 들어가는 filedWithPath역시 실제 응답값의 json key값과 동일해야 한다.

  • when()
    : 응답을 요청한다는 것을 명시

  • get()
    : 해당 url로 응답을 요청한다. post, put 등 다른 method를 사용해줄 수 있으며, reqeust(method,url)형태로 작성해줄 수도 있다.

  • then()
    : 반환된 결과값을 바탕으로 성공되어야 하는 테스트의 결과를 매칭한다.


공식 깃허브 상에서는 requestParamters를 사용하여 test code를 작성한 예시를 들고 있다. 나의 경우, 이 requestParameters로 인해 test code가 제대로 작동하지 않았다.
결론은 0.17.1 version 이하로 쓰는 경우, requestParamters를 doucument model의 field로 가지고 있는데 non-null type이다.
따라서 이것을 작성해주지 않을 경우 에러가 발생한다.

0.17.1 이후 version의 경우 requestParamters가 없어지고 PathParamters와 QueryParamters로 대체되었다.

따라서 version에 맞게 잘 작성해줘야 동작이 가능하다.

참고로 주의할 점은, 나의 경우 dependency를 0.18.2로 맞춰놓고 requestPAramters 대신 pathparmeters와 queryparamters를 사용했음에도, requestParamters가 null이라는 에러가 계속 발생했다.
한참....을 뒤져보고 구글링을 열심히 해봤지만 원인은 pluging의 version은 0.17.1 이전 버전으로 설정해 놨어서 계속 문제가 발생한 것이었다.
누군가 나와 같은 문제가 발생한다면, plugin도 함께 살펴보길 바란다!

Test code 작성 후 task 실행해보기

copyOasToSwagger를 실행하면 아래와 같이 우선 build/api-spec에 openapi3.yaml 파일이 생성된다.

그리고 이 파일이 복제되어 resources/static/swagger-ui에 생성된다.

두 파일 내부에는 test code에 작성해준 api명세가 들어왔다.

UI로 확인하기

이제 생성된 api 명세를 ui로 확인해야 한다.

먼저 server를 run해주자.

api 명세를 update 해주고 싶다면, server를 run하기 전에 반드시 copyOasToSwagger task를 실행해줘야 한다.

그리고 다음 경로로 들어가면 swagger ui가 뜰 것이다.
http://localhost:8080/swagger-ui/swagger-ui.html

spring에서는 기본적으로 정적 resource file에 대해서 static/ 아래에 경로로 탐색을 하도록 설정되어 있다. 따라서 main/resources/static/../..와 같이 경로를 모두 작성해 주지 않아도, static 아래 경로만 작성해주면 알아서 mapping 된다.

그럼 이러한 화면이 뜰 것이다. 나는 처음에 이 화면에서 어떻게 내가 만든 api를 확인할 수 있는지 몰라서 애를 먹었다.

이 검색창에 opeanapi3.yaml의 경로를 입력해주면 된다.

지금 현재 swagger-ui.html가 위치해 있는 폴더가 static/swagger-ui이기 때문에 같은 폴더에 위치해 있는 파일의 경우 별도의 경로 없이 작성해줄 수 있다.

혹은 static/ 아래의 경로를 지정해주면 된다.

따라서 내가 만든 api 문서를 확인하는 방법은 다음과 같이 두가지로 추렸다. 아래 두가지를 치고 explore를 해주면 된다.
내가 했을 때 논리적인 흐름으로 생각해봤을 때 두가지를 찾았는데, 그 밖에 다른 방법이 있거나 혹은 잘못 생각했을 수도 있다.

  • /swagger-ui/openapi3.yaml
  • openapi3.yaml

그럼 이제 내가 작성한 api명세가 보이는 것을 확인할 수 있다.

참고로 서버를 호스팅해서 외부에서 사용하는 경우, server 주소를 바꿔서 사용할 수 있고, gradle파일에 여러개의 서버를 나열해서 api 명세에서 선택해서 사용할 수도 있다.

그리고 이 swager ui는 host의 url 주소만 알면 누구나 접근할 수 있다. 따라서 api를 노출하게 되면 매우 위험할 수 있기 때문에, 호스팅하여 사용하는 경우 접근에 대한 제한을 해줘야 한다.

이 때 security를 사용해서 접근 권한(ROLE)이 있는 사람만 접근을 가능하게 할 수도 있고, api문서에 접근할 때 아이디와 비밀번호 등을 입력하게 할 수도 있는 것 같다.

0개의 댓글