내가 만든 API를 널리 알리기 - Spring REST Docs 가이드편

'추석맞이 선물하기 재개발'에 차출되어 API 문서화를 위해 도입한 Spring REST Docs 를 소개합니다.

들어가며…

저는 컬리 입사한지 100일을 갓 넘긴 '파릇파릇한(???) 신규입사자'입니다.

그런 제가 입사한지 며칠 지나지 않아, 프로젝트 지원요청을 받아, 차출되었습니다.
같은 시기에 입사한 '파릇파릇한(???) 신규입사자' 1명이 저와 함께 차출되었습니다.

선물하기 자바개발자 2명 투입
출처: http://scienceon.hani.co.kr/66101

우리 두 사람에게 떨어진 미션은 '상품 및 주문 개편에 따른 선물하기 시스템을 재개발하여 추석 전까지 오픈하기' 였습니다. 약간 우여곡절이 있었지만 추석 전에 잘 오픈하면서 대목(?) 을 놓치지 않는 성과를 이뤘습니다.

선물하기 서비스를 오픈하며 "기존에 Wiki 문서로 되어 있던 API 문서"가 탐탁치 않았던 저는 "Spring REST Docs API 문서"로 전환했습니다.

프로젝트 코드를 직접 보여드리려고 하면 곤란한 상황과 번거로운 일이 많을 것 같아서 별도로 예제 프로젝트를 하나 만들었습니다. GitHub - Repository Spring REST Docs(https://github.com/thefarmersfront/spring-rest-docs-guide)


"Spring REST Docs"로 만든 API 문서를 제공하자!


자바에서 사용하는 REST API 문서화 도구는 몇 가지가 있습니다.

'선물하기 시스템' 재개발 돌입시점부터 '선물하기 API문서'를 제공 준비를 마쳤습니다(프로젝트 구성시 습관처럼 "Spring REST Docs"를 추가합니다).

함께 프로젝트를 진행한 개발자는 "스웨거(Swagger)"를 추천했습니다. 저는 이유 2가지를 들어서 "스웨거" 대신 "Spring REST Docs" 를 추천했습니다(이미 프로젝트 초기부터 구성되어있었습니다. 정해져있으니 따라오세요).

  • "스웨거"는 운영코드에 침투적이다.
  • "Spring REST Docs"는 테스트를 반드시 작성해야 한다.

제가 들었던 2가지 이유를 좀 더 살펴보겠습니다.


스웨거(swagger)는 운영코드에 침투적이다(그래서 싫다).

스웨거(swagger.io)

스프링 애플리케이션에서 스웨거를 사용할 수 있도록 통합기능을 제공하는 프로젝트가 있습니다.

"Springfox"의 경우 2020년 이후 활동이 없는 상태라, 스웨거 API문서를 제공하고자 한다면 "Springdoc"를 추천합니다.

"Springfox" 프로젝트는 스웨거 애노테이션을 운영코드에 추가해야 했습니다. API 문서를 제공하기 위해 운영코드에 침투한 스웨거 애노테이션을 이용해서 스웨거 문서를 생성합니다. 그런데, 최근에 살펴본 "Springdoc" 는 별다른 설정을 하지 않아도 비교적 쓸만한 형태로 Swagger-UI 화면을 렌더링하여 제공했습니다. 그래서 잠깐 '운영코드에 침투적이다.' 라는 생각이 틀렸나 고민하기도 했습니다. 조금 더 살펴보니 그런 생각이 틀리지는 않았습니다. 왜 그렇게 생각했는지 "Springdoc"을 한번 살펴보겠습니다.

'어? 아닌가?' - "Springdoc" 적용하기

"Springdoc"는 스프링 부트 프로젝트에서 API 문서를 자동생성할 수 있는 자바 라이브러리 입니다.

작동하는 범위는 아래 그림을 통해 확인하실 수 있습니다.

Springdocs General Overview
출처: https://springdoc.org/#general-overview

"Springdoc"가 API문서를 생성하는 과정은 생각보다 간단했습니다.

먼저, 빌드스크립트에 springdoc-openapi-ui 의존성을 선언합니다(swagger-ui 를 사용합니다.).

//생략
dependencies {
    //생략 
    //@see <a href="https://springdoc.org/#getting-started">springdoc-openapi getting started.</a>
    implementation("org.springdoc:springdoc-openapi-ui:1.6.11")
//생략

build.gradle

프로젝트 의존성을 갱신하여 그레이들이 springdoc-openapi-ui 라이브러리를 가져오도록 기다립니다.

애플리케이션이 구동되고 다음과 같은 경로로 접근하면 스웨거UI를 볼 수 있습니다.

http://server:port/context-path/swagger-ui.html
-> 로컬에서 띄울 경우: http://127.0.0.1:8080/swagger-ui.html

Springdoc) "Springdoc" 으로 생성된 API 문서 예제

스프링 부트 애플리케이션에서 '컨텍스트패스(ContextPath)'를 별도로 지정하지 않으면 빈값이()가 기본설정됩니다.
server.servlet.context-path:

스웨거 애노테이션을 사용하지 않는다면, product-rest-controller 처럼 @Controller 가 선언된 클래스명을 '케밥(Kebab, 모두 소문자로 표기하며 띄어쓰기는 - 로 표기)'으로 표현한 태그를 사용합니다. 그리고 각 메서드(핸들러) 파라미터로 사용된 클래스를 스키마(Schema)로 추출하여 보여줍니다.

`ProductRestController` 스웨거API ProductRestController 스웨거API

이 기본적인 작동 및 결과물을 봤을 때, "우와! 좋은데? …Spring REST Docs 써야하는데…" 라며 잠시 주춤할 정도였습니다.

'에이, 역시나!' - 스웨거 애노테이션 사용하기

하지만, 기본제공되는 정보만 가지고 API문서로 활용하기에는 많이 밋밋했습니다. 여기서 좀 더 풍부한 정보를 제공하고 싶어서 살펴보니 "Springfox" 때와 마찬가지로 스웨거 애노테이션을 운영코드에 추가해야 제가 생각하는 정보를 제공할 수 있었습니다. 스웨거 애노테이션이 본격적으로 운영코드로 침투!!합니다.

`OrderRestController` OrderRestController 코드

보다 자세한 내용은 예제코드를 확인하세요.

스웨거를 이용하면 "Spring REST Docs"로 API 문서를 작성하는 것에 비해 쉽게 API 문서를 생성할 수 있습니다. 그러나 스웨거 API 문서에서 정보를 풍부하게 제공하려고 하면 운영코드에 스웨거 애노테이션이 침투하기 시작하며 생각보다 많은 코드를 작성하게 됩니다. "Springdoc"가 제공하는 기본 기능만 활용한다면 "Springfox"에 비해서 운영코드에 침투력이 굉장히 줄어듭니다.

인정!
이 부분은 API 문서 작성방식을 고려할 때 중요한 비교사항이 될 겁니다.

여기서 멈추면 이야기가 더이상 진행되지 않겠죠? 본격적으로 "Spring REST Docs" 사용을 권하는 이야기를 시작합니다.


Spring REST Docs 는 테스트를 반드시 작성해야 한다.

Spring REST Docs)

"Spring REST Docs" 가 테스트를 강제하는 이유

"Spring REST Docs"는 API 문서에 포함(include)되는 "스니펫(snippets)"을 생성하기 위해 테스트 코드를 작성해야 합니다. 테스트 코드를 작성하지 않으면 스니펫을 얻을 수 없고, 스니펫을 얻지 못하면 API 문서에 포함시킬 수 없습니다. 고로, API 문서를 제공하기 위해서 반드시 테스트 코드를 작성해야 합니다.

"Spring REST Docs"는 Spring MVC 테스트 프레임워크, 스프링 WebFlux WebTestClient 혹은 REST Assured3를 이용해서 테스트를 작성하고 스니펫을 생성합니다. 이렇게 테스트를 기반으로 한 접근을 통해서 서비스 안정성을 보장합니다. 스니펫에 오류가 있는 경우 테스트는 실패합니다. HTTP 요청과 응답을 대상으로 작업해야하는 탓에 작성초기에 번거롭게 느껴질 수 있는 반복적인 작업을 해야 합니다.
이거 정말 귀찮습니다.

그러나 이렇게 힘겹게힘겹게 작성된 테스트 코드는 대상 API에 대한 신뢰감을 제공합니다. API가 변경되며 누락된 부분(추가 필드, 응답값, 상태값)을 API 문서 생성과정에서 즉각적으로 확인할 수 있습니다.

작성된 테스트 실행 결과 아스키닥(Asciidoc) 스니펫을 수집해서 "아스키닥터(asciidoctor)" 플러그인을 이용해서 HTML 문서로 렌더링 됩니다. 여기서 마크다운(markdown)이 제공하지 못하는 기능을 제공합니다. 스니펫을 비롯해서 개발자가 임의적으로 분리한 아스키닥 원본파일을 원하는 형태로 조합하게 됩니다. 그 결과가 이런 API 문서입니다.

Spring REST Docs 산출물 "Spring REST Docs 가이드 예제"

보다 자세한 내용은 예제코드를 확인하세요.

  • build.gradle: asciidoctor 플러그인 및 asciidoctor 태스크 관련 스크립트 확인
  • OrderRestControllerDocTest: 스니펫을 생성하기 위한 테스트 코드
  • index.adoc: API 문서 생성시 기준이 되는 최상위 아스키닥(asciidoc) 문서입니다.
  • 사용자 정의 스니펫: "Spring REST Docs"에서 제공하는 스니펫 형식을 구미에 맞게 변경

살짝 까먹기 쉬운점

"Spring REST Docs" 에서 제공하는 스니펫을 구미에 맞게 변경(사용자정의, custom)하기 위해 사용자정의 스니펫 파일을 작성하는데, 그 위치가 약간 헷갈릴 수 있습니다. 문서에서는 src/test/resources/org/springframework/restdocs/templates/asciidoctor 경로에 변경하길 원하는 스니펫 파일을 정의하라고 기술했습니다.

Spring REST Docs - Customizing the Generated Snippets Spring REST Docs - Customizing the Generated Snippets

이게 인텔리제이에서 보면 org.springframework.restdocs.templates.asciidoctor 처럼 보여서 그대로 등록했지만 스니펫이 적용되지 않는 상황을 가끔 경험하실 수 있을 겁니다. 버전에 따라서 경로가 달라질 수 있기 때문에, 사용하시는 "Spring REST Docs" 버전을 확인하시고 참고문서(Reference Documentation)를 살펴보기 바랍니다.


"Spring REST Docs"에 대한 아쉬움

하지만, "Spring REST Docs" API 문서는 뭔가 심심합니다. 정적인 HTML 문서기 때문에 개발자 외에 기획자가 테스트하려고 하면 뭔가 불편합니다. 맥북을 사용하시는 경우는 터미널과 명령어 실행에 필요한 'cUrl''httpie' 설치 및 사용법을 알려드린 후에는 능숙하게 사용하셨습니다. 응??? 개발자처럼 능숙했던 능력자들

심심하고 정적인 "Spring REST Docs" API문서도 괜찮지만, 스웨거처럼 문서에서 바로 API를 테스트하며 확인할 수 있으면 여러모로 편리한 점이 많습니다.

  • API 정상작동 확인이 필요하다면 스웨거 API 문서에서 요청값을 입력하고 작동을 확인할 수 있습니다.
    • 이 테스트를 하려면 애플리케이션 CORS, CSRF, FrameOption 등을 비활성화 해야했습니다. 개발 환경에서만 사용할 수 있도록 설정하는 것을 잊지마세요.
      public class WebSecurityConfig{
      
        private WebSecurityConfig() {
            throw  new UnsupportedOperationException("Config class.");
        }
      
        @Profile({PROFILE_LOCAL})
        @Configuration
        public static class LocalWebSecurityConfig extends WebSecurityConfigurerAdapter {
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.requestMatcher(PathRequest.toH2Console());
                http.authorizeRequests().antMatchers("/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**", "/now").permitAll(); // 스웨거용
                http.csrf(AbstractHttpConfigurer::disable);
                http.cors(AbstractHttpConfigurer::disable);
                http.headers().frameOptions().disable();
            }
        }
      
        @Profile({PROFILE_DEV, PROFILE_STAGE})
        @Configuration
        public static class SwaggerWebSecurityConfig extends WebSecurityConfigurerAdapter {
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests().antMatchers("/v2/api-docs", "/configuration/**", "/swagger*/**", "/webjars/**").permitAll(); // 스웨거용
                http.csrf(AbstractHttpConfigurer::disable);
                http.cors(AbstractHttpConfigurer::disable);
                http.headers().frameOptions().disable();
            }
        }
      }
      
  • 기획자나 운영자가 개발환경에서 테스트를 위해 데이터등록을 요청하는 경우 가이드하여 자율적으로 이용하도록 안내할 수 있습니다.
    • 스웨거를 사용하지 않은 경우에는 cUrl 혹은 httpie 사용방법을 소개하는 것으로 마무리 되기도 함.
  • 그 외에는…. 음… 알록달록 이뻐보인다??

밋밋한게 아쉬우니까 Spring REST Docs 에 Swagger 를 넣어보자.

'선물하기 시스템 API 문서화'를 위한 도구를 탐색하던 중에 "Spring REST Docs" 테스트 코드와 유사한 코드를 작성하고 "OpenAPI Specification(줄여서 OAS)" 문서를 생성해서 이를 "Swagger UI"로 활용하는 내용을 발견했습니다.

"Spring Rest Docs로 OpenAPI (Swagger) 문서를 만들어 Swagger UI로 호출하여 보기(파란하늘의 지식창고:티스토리)"

"Spring REST Docs API specification Integration" 를 이용하면 OAS 문서를 JSON 혹은 YAML 형식으로 생성할 수 있습니다. OAS 문서 Swagger-UI 나 포스트맨(https://www.postman.com/) 등 다양하게 활용할 수 있습니다. 그래서 적극적으로 채용했습니다(새로운 것은 일단 써보고 좋으면 널리 전파!!). 테스트 코드를 거의 공유하고 있기 때문에 다음과 같이 코드를 수정하여 간단하게 대응할 수 있었습니다. 반복적인 스니펫 사용을 피하기 위해 기존에 사용하던 코드가 쓸모가 없어지고, 한땀한땀 작성해야하는 번거로움이 있기는 했지만, 저에게는 '복붙(복사하고 붙이기, Copy & Paste)'라는 강력한 기술이 있었기에 이를 적극 활용합니다.

 MockMvcFactory.getRestDocsMockMvc(contextProvider, HOST_LOCAL, controller)
        .perform(RestDocumentationRequestBuilders.put("/orders/{orderNo}/complete", orderNo)
                .contentType(MediaType.APPLICATION_JSON)
        )
        .andDo(MockMvcResultHandlers.print())
        .andExpect(MockMvcResultMatchers.status().isOk())
        //REST Docs 용
        .andDo(MockMvcRestDocumentation.document("put-v1-complete-order",
                getDocumentRequest(),
                getDocumentResponse(),
                pathParameters(
                        parameterDescriptor
                ),
                responseFields(
                        responseFieldDescription
                )
        ))
        //OAS 3.0 - Swagger
        .andDo(MockMvcRestDocumentationWrapper.document("put-v1-complete-order",
                getDocumentRequest(),
                getDocumentResponse(),
                resource(ResourceSnippetParameters.builder()
                        .pathParameters(
                                parameterDescriptor
                        )
                        .responseFields(
                                responseFieldDescription
                        )
                        .build()))
          );

참고: OrderRestControllerDocTest

이 테스트를 수행하면 두 가지 문서가 생성이 됩니다.

  • 스니펫(snippet): build/generated-snippets/put-v1-complete-order
  • OAS: build/api-spec/openapi3.yaml

스니펫은 아스키닥터가 실행하면서 index.adoc - include::order.adoc 파일을 build/docs/asciidoc 디렉터리에 index.htmlorder.html 파일로 변환합니다.

openapi3.yaml 파일은 패키징되면서 Swagger-UI에서 열어볼 수 있도록 경로를 지정해야 합니다. swagger-ui/swagger-ui.html 파일을 열어 SwaggerUIBundle({url}) 항목을 다음과 같이 변경합니다.

<!-- 생략 -->
<body>
  <div id="swagger-ui"></div>

  <script src="/swagger-ui/swagger-ui-bundle.js" charset="UTF-8"> </script>
  <script src="/swagger-ui/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
  <script>
    window.onload = function () {
      // Begin Swagger UI call region
      const ui = SwaggerUIBundle({
        url: "/swagger/openapi3.yaml", // <-- build.gradle bootJar 태스크에서 합쳐짐
        dom_id: '#swagger-ui',
<!-- 생략 -->

swagger-ui/swagger-ui.html

그리고 build.gradle 을 수정하여 실행가능한 jar(Executable jar) 생성하는 bootJar 태스크가 실행될 때, 추가적으로 build/docs/asciidocbuild/api-spec 디렉터리에 "Spring REST Docs" 생성물과 openapi3.yaml 파일을 포함하여, jar로 패키징했습니다.

bootJar {
    from("${asciidoctor.outputDir}") {
        into "BOOT-INF/classes/static/docs"
    }
    from("swagger-ui") {
        into "BOOT-INF/classes/static/swagger"
    }
    from("build/api-spec") {
        into "BOOT-INF/classes/static/swagger"
    }

    archiveFileName.set "application.jar"
}

build.gradle - bootJar 태스크 정의

이렇게 해서 스프링 부트가 구성한 애플리케이션에서 정적파일 지원기능을 활용하여 웹브라우저로 접근하여 활용할 수 있었습니다.

Spring REST Docs - OpenAPI Sepecification 예제 Spring REST Docs - OpenAPI Sepecification 예제

여기서 yaml 파일을 내려받아 포스트맨에서 사용할 수도 있습니다.

openapi3.yaml - Postman 불러오기(import) openapi3.yaml - Postman 불러오기(import)


정리

비교 스웨거(Springdocs 기준) Spring REST Docs
장점
  • 동적인 API문서를 제공한다.
  • API 를 직접 조작하며 확인할 수 있다.
  • 테스트를 통해 스니펫을 생성한다.
  • 코드변경에 대해 즉각적인 확인이 가능하다.
단점
  • 상세한 API 정보를 제공하기 위해 스웨거 애노테이션이 운영코드에 침투된다.
  • 준비해야할 것이 많다(Spring Web, Test 프레임워크, Spring REST Docs 작성, asciidoc, 그레이들).
  • 개발자의 노력 정도에 따라 품질편차가 심하다.

"스웨거"와 "Spring REST Docs" 중에 어느 것을 선택하든, 그것은 엔지니어가 장단점을 따지고 자신에게 유리한 기술을 선택하면 됩니다.

제가 꺼려했던 '스웨거는 운영코드에 침투적이다' 라는 거부감이 누군가에게는 그렇게 대수롭지 않을 수 있습니다. 오히려 '"Spring REST Docs"를 사용하기 위해 Spring TEST, Asciidoctor, Spring REST Docs 작동원리를 알기 위해 공부해야할 내용이 많다'는 점이 누군가에게는 부담이 될 수 있습니다. 그러나, 저는 다음과 같이 생각하기에 지속적으로 "Spring REST Docs"를 권하고 있습니다.

"자신이 구현한 기능을 테스트하는 코드를 통해서 '신뢰감'을 높이는 행위"가 더욱 자신감있게 기능을 개발하고 공유할 수 있게 도움을 줍니다.



이 블로그 포스트는 컬리 내부 개발자에게 공유할 'Spring REST Docs 사용 가이드' 도입부 입니다.

컬리 내부 개발자들에게는 보다 친절하고 자세한 가이드 및 전파교육을 진행하려고 합니다. ^^

앞으로도 컬리 기술블로그를 통해서 종종 찾아뵙겠습니다.

P.S. 1.

그레이들 7.5 업그레이드를 하실 때는 여러가지 살펴보시기 바랍니다.

힌트: Dealing with validation problems

P.S. 2.

컬리는 당신을 필요로 합니다.

지원하세요!!

--> 컬리 채용 <--


참고