tosspayments-restdocs: 선언형 문서 작성 라이브러리

이준희 · 토스페이먼츠 Server Developer
2023년 3월 22일

최소한의 코드로 문서 작성하기

들어가며

토스페이먼츠에서는 두 가지 장점 때문에 Spring REST Docs를 권장하고 있는데요. 첫 번째는 문서 작성 단계부터 API를 통합 테스트할 수 있다는 점, 두 번째는 인터페이스의 의도치 않은 변경을 감지할 수 있다는 점 때문입니다.

하지만 Spring REST Docs에는 단점도 있습니다. 장황한 코드 때문에 가독성이 떨어지고, 코드 반복으로 인해 생산성이 떨어지는 아쉬움이 있었습니다. 이런 문제를 해소하기 위해 Kotlin DSL을 구현해서 반복적이고 지루한 Spring REST Docs 코드 작성에 필요한 노력을 줄이는 방법을 한규주님의 이전 글 'Kotlin으로 DSL 만들기: 반복적이고 지루한 REST Docs 벗어나기'에서 소개했었습니다.

이번에는 나아가 더 높은 가독성, 더 최소화된 코드 중복, 세부 구현 및 의존성 은닉, 마지막으로 확장에 열려있는 특성을 갖춘 문서화 라이브러리 'tosspayments-restdocs'를 소개하고 개발 후기를 공유합니다.

동일한 컨트롤러를 Spring REST Docs(좌측)와 tosspayments-restdocs(우측)로 문서화했을 때의 차이. 작성한 문서화 코드의 양이 크게 감소했다.

다시 살펴보기: Spring REST Docs의 문제들

Spring REST Docs 기반 문서화 코드의 아쉬운 점을 아래 코드를 통해 다시 한번 살펴볼게요.

요청 필드 2개, 응답 필드 3개로 구성된 단순한 PUT 인터페이스지만, 장황하게 작성된 코드 때문에 전체 구조를 한눈에 파악하기 어렵습니다. 이런 구조의 코드는 처음 작성하는 비용이 많이 들 뿐만 아니라 유지보수 비용도 늘립니다. 위 코드의 문제점을 좀 더 구체적으로 살펴보겠습니다.

1. 코드 중복

먼저, 같은 내용이 반복적으로 명세에 포함되어 있습니다. 예를 들어 MockMvcResponse와 REST Docs Snippet을 만들기 위해 Path Variable, 그리고 Request Body Field 코드 중복이 발생했습니다.

또한 Request Body값을 MockMvc에 전달하는 과정에서 샘플 데이터를 통해 필드의 타입 및 샘플이 추론 가능함에도 REST Docs Snippet에 불필요하게 다시 명세하고 있습니다.

2. 불필요한 명령

given()prettyPrint()then()preprocessRequest()preprocessResponse() 등의 메서드는 인터페이스 명세에 꼭 필요한 핵심이라 하기 어렵습니다. 문서 작성과 관계 없이 빌드에 필요하기 때문에 추가된 사항입니다.

3. 인터페이스 명세 순서

실제 HTTP 프로토콜에서는 Request Line → Request Header → Request Body → Response Line → Response Header → Response Body 순으로 페이로드가 만들어집니다.

그러나 위의 예시 코드는 이러한 흐름에 맞춘 자연스러운 명세 작성 대신, MockMvc를 구성하는 기반 기술에 의존하는 명령을 나열하는데 집중하고 있습니다. 그래서 코드를 작성할 때나 읽을 때 모두 코드 블록을 왔다갔다 해야 하는 불편을 겪습니다.

4. 기반 기술에 강한 의존

기반 기술에 강하게 의존하고 있기 때문에 기반 기술에 브레이킹 체인지가 생기거나, 유지보수가 중단되면 대응이 어렵습니다.

또, 이렇게 기반 기술에 많이 의존하게 되면 문서를 작성하는 모든 개발자가 기반 기술의 세부 항목을 학습해야 합니다. 가령 MultiPart API, Streaming API, Reactive API 등 다른 형태의 API를 문서화할 때마다 개발자는 사용할 MockMvc의 세부 기능을 각각 학습해야 합니다. 이런 방식은 문서화에 필요한 학습 비용을 높이고 지속적인 대응을 어렵게 만듭니다.

위 코드를 tosspayments-restdocs 라이브러리를 사용해서 다시 작성한 코드 예시입니다.

생성될 문서를 직관적으로 예측할 수 있고, 기반 기술을 완전히 추상화해서 코드가 절반 이하로 감소했습니다.

tosspayments-restdocs를 적용한 결과

이런 개선점을 만든 tosspayments-restdocs 라이브러리에는 선언형 프로그래밍, 타입 추론 개념이 녹아있는데요. 어떻게 적용됐는지 하나씩 설명해보겠습니다.

선언형 프로그래밍

https://developer.mozilla.org/en-US/docs/Glossary/Element

선언적으로 코드를 작성하는 대표적인 사례는 HTML입니다. HTML에서는 표현하고자 하는 항목을 요소(Element)로 추상화하고, 요소의 태그(Tag), 속성(Attribute), 내용(Content)을 명세해서 최종 결과물을 만듭니다.

같은 방식으로, tosspayments-restdocs에서는 API의 실제 형태(HTTP의 페이로드)를 있는 그대로 표현할 수 있도록 Documentation, Request Line, Request Header, Request Body, Response Body를 요소화했습니다. 이렇게 모든 문서화 항목을 요소로 만들었기 때문에 HTML을 작성하는 방식처럼 선언적으로 문서를 작성할 수 있습니다.

선언적인 작성에는 이런 장점이 있습니다.

  • 명세의 내용에 집중하게 됩니다. 세부 기술 및 이를 위한 불필요한 명령 없이, 문서화 코드의 본질인 인터페이스 명세에 집중할 수 있습니다.
  • 꼭 필요한 정보만 명세할 수 있습니다. 꼭 필요한 항목만 요소의 속성으로 선언하고, 추가적인 속성들은 중괄호를 열어 표현하게 됩니다.
  • 읽기 쉬운 코드가 됩니다. 최종 결과물의 실제 형태가 그대로 녹아져 있으므로, 위에서 아래로 한번만 읽으면 결과를 파악할 수 있습니다.
  • 확장이 쉽습니다. 새로운 문서화 항목이 생기는 경우 요소를 새로 정의하면 됩니다. 기존 항목에 자식 항목이 새롭게 생기는 경우에도 마찬가지로 대응이 가능합니다.
  • 구현 방법에서 자유로워집니다. 각 요소를 어떻게, 무엇으로 렌더링 할 지를 모두 라이브러리에 위임해서 기반 기술의 변화나 결과물의 포맷 변화가 코드에 미치는 영향을 줄일 수 있습니다.

선언형 프로그래밍 구현 - 함수와 확장 함수

tosspayments-restdocs의 문서화 코드 진입점인 documentation 요소 함수를 살펴보겠습니다.

documentation 함수 호출 예

documentation 내부 구현(이해를 돕기 위해 단순화하였습니다)

documentation은 문서의 이름(documentName)을 필수 속성으로, requestLine 등 세부 스펙 요소 추가적인 속성으로 취급하는 요소 함수로, 문서화의 시작(문서 스펙 정의)과 끝(문서 출력)을 담당합니다.

앞서 꼭 필요한 항목만 요소의 속성으로 선언하고, 추가적인 속성들은 중괄호를 열어 표현한다고 했는데요. 이 개념을 코틀린으로 구현하면서 필수 속성은 함수의 파라미터로, 추가 속성은 람다 함수 파라미터로 표현했습니다. 요소의 모든 속성을 파라미터로 펼쳐두면 함수가 장황해지고 확장성이 떨어지기 때문입니다.

추가 속성을 람다 확장 함수(Extension Function) 스코프 내에서 정의하도록 하면 스코프 내에서 일어나는 일들에는 함수가 관여하지 않습니다. 그래서 편의성을 확보하면서 확장성을 유지할 수 있고, 스코프를 계층화하여 도메인을 더욱 잘 표현할 수 있습니다.

예를 들어 documentation 요소 함수의 람다 확장 함수(specCustomizer: DocumentSpec.() -> Unit) 스코프에서는 RequestLineSpec, RequestBodySpec, ResponseBodySpec 등을 세부 요소 스펙으로 품고 있는 DocumentSpecthis가 주어집니다. 스코프 및 스코프에 this를 주입하는 이런 방식에는 다음과 같은 장점이 있습니다.

  • 스코프 단위로 특화된 기능을 제공할 수 있습니다. 예를 들어 requestLine 함수는 DocumentSpec의 세부 요소 스펙인 RequestLineSpec을 정의하기 위한 요소 함수입니다.
  • 구조화된 확장성을 갖습니다. 새로운 속성이 추가되었을 때 스코프 내에 함수나 프로퍼티를 추가하여 쉽게 대응할 수 있습니다. 또한 새로운 자식 요소를 갖게 되는 경우에도 동일한 방식으로 람다 확장 함수 스코프를 정의해 대응할 수 있습니다. documentation → requestBody → field 로 이어지는 nested scope 가 그 예입니다.

타입 추론

Spring REST Docs의 문제 중 하나는 필드의 타입을 REST Docs Snippet에 다시 명세하는 비효율적인 작성 방식입니다.

Kotlin에서는 Inline Function 한정으로 Reified Type Parameter를 제공합니다. 타입 정보가 소거되는 일반적인 Generic Function의 Type Parameter와 달리, Reified Type Parameter의 경우에는 타입 정보가 소거되지 않아 라이브러리에서 접근할 수 있습니다.

tosspayments-restdocs에서는 항상 문서화 요소가 샘플을 받게 강제하고, 샘플의 타입과 값을 내부 자료구조에 저장하도록 했습니다.

sample이 reified T로 선언되어 타입 정보(T::class.java)에 접근이 가능합니다.

타입 정보가 남아있다면 문서를 작성하는 개발자를 대신해 다양한 작업을 자동화 할 수 있습니다. tosspayments-restdocs에서는 타입 명세, 열거형 예시 작성, 포멧 명세 등에 타입 정보를 활용하고 잇습니다.

타입 정보 활용 예(타입별 양식 자동생성) – 열거형은 엔트리 나열, 시간 타입은 타임 포멧을 반환

타입 정보 활용 예(생성된 문서) – 열거형 타입으로부터 얻은 정보로 ResultType의 엔트리(SUCCESS, ERROR)가 자동생성 되었습니다.

지금까지 문서를 최소한의 코드로 작성하면서 변화에도 더 유연하게 대처할 수 있는 tosspayments-restdocs 라이브러리와, 라이브러리에 녹인 생각들을 소개했습니다.

tosspayments-restdocs는 토스페이먼츠의 다양한 팀에서 Spring REST Docs, kotlin-dsl-restdocs를 대신해 활용하며 생산성을 높이고 있습니다.

개발자가 더욱 변하지 않고 가치있는 일에 집중할 수 있도록, 토스페이먼츠에서는 생산성을 높이기 위한 다양한 활동을 이어나가고 있습니다. 함께 고민하며 더욱 좋은 문화와 기술을 만들어나가면 좋겠습니다.

댓글 0댓글 관련 문의: toss-tech@toss.im
연관 콘텐츠
㈜비바리퍼블리카 Copyright © Viva Republica, Inc. All Rights Reserved.