본문 바로가기
야미로깅

Swagger와 Spring rest docs, 두마리 토끼 잡기!🐰

by 의정부핵꿀밤 2023. 2. 22.
728x90

도입 계기

테크루키 중간발표 때 API 문서 자동화 Tool에 대해 발표한 적이 있다

아무래도 개발자에게 중요한 소양(?) 중 하나가 문서화라고 생각해서 우리팀은 문서화에 대해 광적으로 집착하였다...ㅋㅋㅋ

암튼! 그 때 swagger와 spring rest docs 중 고민하다가 보다 신뢰성 있는 문서화를 위해 spring rest docs를 채택했다고 하였다

그럤더니 피드백으로 "2가지를 동시에 사용할 수 있는데 굳이 하나만 골라야 했나요?🤔" 라는 말씀을 듣고 띵..했다

그래서 바아로 2가지를 동시에 사용하는 방법을 채택했다고 한다~

 

 

이제 swagger와 spring rest docs의 장단점을 비교해보고 적용 방법과 느낀 점에 대해 간단히 기록하려고 한다!


 

Swagger 

장점

  • API 문서가 자동으로 생긴다
  • 어노테이션(annotation)을 통해 문서가 생성되기 때문에 API 현행화가 쉽다
  • 화면에서 API를 직접 호출하여 테스트해볼 수 있다

 

단점

  • 프로덕션 코드에 문서화를 위한 코드가 들어간다
  • 서버가 실행될 때 문서가 만들어지기 때문에 API 스펙만 분리해서 관리하기 어렵다
  • 검증되지 않은 API가 생성될 수 있다

 

 


Spring Rest Docs

장점

  • 테스트코드 통과 후 API 문서가 생성되기 때문에 신뢰할 수 있다
  • 비즈니스 로직에는 API 문서 관련 코드가 없다
  • 커스텀이 자유롭다

 

단점

  • 문서가 추가되면 asciido 문서를 일일이 편집해야 한다
  • swagger에 있는 API 문서 관련 코드가 없다
  • 커스텀이 자유롭다

SwaggerUI + Spring Rest Docs

  • 위의 그림에서 첫번째가 기존 Spring rest docs를 사용했을때의 흐름이고, 두번째가 Spring rest docs에 openapi3와 swaggerui를 통해 합쳤을 때의 흐름이다
  • 기존에 Spring rest docs만을 사용할 때는 Asciidoctor를 통해 문서를 생성한 반면, 새로운 방법에서는 spring rest docs 실행 결과를 openapi3 스펙으로 출력하고 이를 통해 swagger ui를 생성하는 방식으로 동작한다
  • 이 둘을 합치는 방법의 핵심은 아래의 두 플러그인이다
    • com.epages.restdocs-api-spec
      • Spring Rest Docs의 결과물을 OpneAPI3 스펙으로 변환한다
    • org.hidetake.swagger.generator
      • OpenAPI3 스펙을 기반으로 SwaggerUI를 생성한다 → HTML, CSS, JS

 

 


OpenAPI3 스펙 출력 설정

// 1. restdocsApiSpecVersion 버전 변수 설정
+ buildscript {
+     ext {
+         restdocsApiSpecVersion = '0.16.2'
+     }
+ }

plugins {
    id 'org.springframework.boot' version '2.7.2'
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
// 2. asciidoctor 플러그인 제거, restdocs-api-spec 플러그인 추가
-   id 'org.asciidoctor.convert' version '1.5.8'
+   id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

// 3. openapi3 설정
+ openapi3 {
+   setServer("http://localhost:8080") // API 요청을 보낼 서버 주소 설정
+   title = "restdocs-swagger API Documentation" // API 문서 제목
+   description = "Spring REST Docs with SwaggerUI." // API 문서 설명
+   version = "0.0.1" // API 문서 버전
+   format = "yaml" // API 문서 출력 포맷 (default = JSON)
+ }

// 4. 불필요한 설정 제거 (1)
- ext {
-     set('snippetsDir', file("build/generated-snippets"))
- }

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 5. restdocs-api-spec restassured 관련 의존성 추가
-   testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
+   testImplementation "com.epages:restdocs-api-spec-restassured:${restdocsApiSpecVersion}"
    testImplementation 'io.rest-assured:rest-assured'
}

tasks.named('test') {
// 4. 불필요한 설정 제거 (2)
-    outputs.dir snippetsDir
    useJUnitPlatform()
}

// 4. 불필요한 설정 제거 (3)
- tasks.named('asciidoctor') {
-     inputs.dir snippetsDir
-     dependsOn test
- }

위와 같이 build.gradle에 관련 의존성을 추가해준다!

 

 

 

기존 API에 대한 테스트코드 작성

나는 테크루키 프로젝트에서 미리 구현한 댓글 조회 API에 대해 테스트코드를 작성해보았다!

 

CommentApi.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class CommentApi {

    private final CommentCommandService commentCommandService;
    private final CommentQueryService commentQueryService;

    @GetMapping("/feeds/{feed-id}/comments")
    public CommentSelectResponse getComments(@PathVariable("feed-id") Long feedId) {
        return commentQueryService.getComments(feedId);
    }
}

 

이제 위의 api에 대한 테스트 코드를 작성해보자!

 

1) BaseControllerTest.java

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(RestDocumentationExtension.class)
public abstract class BaseControllerTest {
    protected static final String DEFAULT_RESTDOC_PATH = "{class_name}/{method_name}/";
    protected RequestSpecification spec;

    @LocalServerPort
    int port;

		// RestAssured 설정
    @BeforeEach
    void setUp() {
        RestAssured.port = port;
    }

		// API 스펙 설정
    @BeforeEach
    void setUpRestDocs(RestDocumentationContextProvider provider) {
        this.spec = new RequestSpecBuilder()
                .setPort(port)
                .addFilter(documentationConfiguration(provider)
                        .operationPreprocessors()
                        .withRequestDefaults(prettyPrint())
                        .withResponseDefaults(prettyPrint())
                )
                .build();
    }
}
  • RestAssured와 API 스펙 관련 설정 코드의 중복을 제거하기 위해 BaseControllerTest 라는 추상 클래스를 선언한다

 

 

2) CommentApiTest.java

class CommentApiTest extends BaseControllerTest {

    @Test
    @DisplayName("댓글 조회 테스트")
    void getCommentsTest() {
        CommentSelectResponse response = new CommentSelectResponse(1L,
                List.of(CommentSingleDao.builder()
                        .email("wldnjs@ssg.com")
                        .content("댓글입니다")
                        .createdAt(LocalDateTime.now())
                        .build())
        );

        given(this.spec)
                .filter(document(DEFAULT_RESTDOC_PATH,
                                pathParameters(
                                        parameterWithName("feed-id").description("feed id")
                                ),
                                responseFields(fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("댓글 개수"),
                                        fieldWithPath("comments").type(JsonFieldType.ARRAY).description("댓글 목록"))
                        )
                )
                .accept(MediaType.APPLICATION_JSON_VALUE)
                .header("Content-type", "application/json")
                .log().all()

        .when()
                .get("/api/feeds/{feed-id}/comments", 1L)

        .then();
    }
}
  • 추상 클래스 BaseControllerTest를 상속받아 테스트 코드를 작성한다
  • RestAssured의 given-when-then 방식을 사용하여 테스트코드를 작성했다 :)

 

 

 

문서 생성 확인

문서 생성 확인을 위한 Gradle Task는 openapi3 이다

  • openapi3는 Open API 3.0.1 문서를 yaml 포맷으로 build/api-spec 경로에 출력한다
  • 그리고 openapi3는 check를 의존하도록 설정되어 있다

 

gradle task 실행 순서

task 의존관계에 따라 openapi3 명령을 수행하면 compile → test → openapi3가 실행되며, 문서가 생성된다

 

  • 명령어 : ./gradlew openapi3
  • gradle task를 실행하면 위의 사진처럼 build/api-spec 에 openapi3.yaml 파일이 생성된 것을 볼 수 있다

 

 

 

SwaggerUI 연동 추가

+ import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
+ import org.springframework.boot.gradle.tasks.bundling.BootJar

buildscript {
    ext {
        restdocsApiSpecVersion = '0.16.2'
    }
}

plugins {
    id 'org.springframework.boot' version '2.7.2'
    id 'io.spring.dependency-management' version '1.0.12.RELEASE'
    id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
// 3.1 swagger generator 플러그인 추가
+   id 'org.hidetake.swagger.generator' version '2.18.2'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

// 3.2. swaggerSources 설정 추가
+ swaggerSources {
+   sample {
+       setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml"))
+   }
+ }

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation "com.epages:restdocs-api-spec-restassured:${restdocsApiSpecVersion}"
    testImplementation 'io.rest-assured:rest-assured'
// 3.3. Swagger 의존성 추가
+   swaggerUI 'org.webjars:swagger-ui:4.11.1'
}

tasks.named('test') {
    useJUnitPlatform()
}

// 3.4. Task 및 설정 추가
// 3.4.1
+ // GenerateSwaggerUI 태스크가, openapi3 task 를 의존하도록 설정
+ tasks.withType(GenerateSwaggerUI) {
+     dependsOn 'openapi3'
+ }
+ 
// 3.4.2
+ // 생성된 SwaggerUI 를 jar 에 포함시키기 위해 build/resources 경로로 로 복사
+ tasks.register('copySwaggerUI', Copy) {
+     dependsOn 'generateSwaggerUISample'
+ 
+     def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get()
+ 
+     from("${generateSwaggerUISampleTask.outputDir}")
+     into("${project.buildDir}/resources/main/static/docs")
+ }
+ 
// 3.4.3
+ // bootJar 실행 전, copySwaggerUI 를 실행하도록 설정
+ tasks.withType(BootJar) {
+     dependsOn 'copySwaggerUI'
+ }
  • 앞에서 생성한 openapi3.yaml 으로 SwaggerUI를 생성하기 위해 위의 의존성들을 build.gradle에 추가해준다
  • 위의 의존성을 추가한 뒤, gradle build를 하면 build/swagger-ui-sample에 생성된 SwaggerUI가 build/resources/main/static/docs로 복사된다

 

 

 

SwaggerUI 확인

이후 bootRun 명령어를 통해 이전 단계에서 빌드된 jar 파일을 실행하면, http://localhost:8080/docs/index.html 에서 SwaggerUI를 확인할 수 있다

(나의 경우에는 build.gradle에서 설정한 index.html 파일 복사 경로에 /swagger를 추가해서 url path가 하나 더 추가된 것이다!)

 

 

 


적용 후기

이처럼 swaggerui와 openapi3를 이용하여 swagger와 rest docs의 장점을 모두 갖춘, 신뢰성 있고 테스트 가능한 api 문서화를 할 수 있었다!

생각보다 많이 사용되는 방법인 것 같고, 테스트 코드를 보다 정성스레(?) 짠다면 아주 효율적으로 사용할 수 있을 것 같다!

 

근데 생각보다 커스텀하는 방식이 어려웠던 것 같다

사실상 spring rest docs에 openapi3를 통해 swagger의 ui만 적용한 것이라 swagger 처럼 단순히 어노테이션만 붙여서 생성되는 것이 아니라 일일이 설정을 다 해줘야 한다는 게 꽤나 번거롭고, 어느 부분을 어떤 방식으로 바꿔야 적용되는지도 알기가 쉽지 않았따..😢

 

물론 내가 테스트코드 작성이 미숙해서 더욱 어렵게 느껴졌던 걸수도 있다

그래도 적용해봤다는 것에 아주 만족하고, 테스트코드와 문서화의 중요성을 다시 한번 느낄 수 있어서 너무 좋은 경험이었다!

 

나는 테스트 코드가 익숙한 사람들이라면 2가지 방식을 조합해서 사용하는 이 방식을 추천하고 싶다! 👍

 

 

 


🤔 테스트 코드를 작성해도 rest docs 문서가 생성되지 않는다?

import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper

테스트 코드를 작성할 때 document() 함수는 위의 라이브러리를 import하여 사용해야 한다!

만약 테스트코드가 통과를 했는데도 문서가 생성되지 않는다면 임포트한 라이브러리를 다시 확인해보자 :)

 


참고)

https://taetaetae.github.io/posts/a-combination-of-swagger-and-spring-restdocs/

 

Swagger와 Spring Restdocs의 우아한 조합 (by OpenAPI Spec)

MSA 환경에서의 API 문서화는 어떤 식으로 구성하는 걸까? 예컨대, 모듈이 10개 있다고 하면 각 모듈마다 API 문서가 만들어질 테고 API 문서를 클라이언트에 제공하기 위해서 각각의 (10개의) URL를

taetaetae.github.io

https://www.youtube.com/watch?v=qguXHW0s8RY 

https://jwkim96.tistory.com/274

 

SwaggerUI + Spring REST Docs 함께 사용하기(feat. Rest Assured)

이 글은 딥다이브한 내용을 쭉 풀어쓴 내용입니다. 시간이 없으신 분은 전체 소스코드를 참고해 주세요. 0. 시도하게된 이유 0.1 Swagger 경험 이전에 TODO List 라는 프로젝트에서 Swagger 를 사용해본

jwkim96.tistory.com

https://blog.naver.com/qjawnswkd/222340413113

 

Spring RestDocs 와 Swagger 같이 사용하기(OpenAPI Spec 사용)

Spring RestDocs 를 사용하면 테스트 코드를 통과해야지 문서가 생성되어서 좋고 테스트 코드에 작성할 ...

blog.naver.com

 

728x90

댓글