BE/Spring

스프링부트3 Spring REST docs + Swagger UI 사용하기

E@st 2023. 8. 4. 05:53

문서화

게시판 프로젝트 미션에는 API 문서화에 대한 조건이 주어졌다. API 문서화를 해본 적이 없었기 때문에 조사를 먼저 시작했다.
우선 스프링 부트 환경에서 문서화로 유명한 도구는 REST Docs와 Swagger로 알고 있었기에 두 가지를 짧게 사용해 보면서 비교해 보고
현재 내 상황에 맞는 것을 사용하기로 했다.

REST Docs

Spring REST Docs는 RESTful 서비스의 정확하고 읽기 쉬운 문서를 생성하기 위한 도구입니다. 이는 실제 코드와 함께 동작하며, 테스트 기반의 접근 방식을 사용하여 API 문서화 작업을 수행한다.

주요 특징

장점

  1. 테스트 기반: Spring REST Docs는 테스트 중에 문서 조각(snippets)을 생성합니다. 코드 변경으로 테스트코드가 같이 변경될 때 코드와 문서 간의 일치성을 보장함.
  2. 포맷 선택: Asciidoctor나 Markdown과 같은 다양한 출력 포맷을 지원하므로, 원하는 형식으로 문서를 작성.
  3. 커스터마이즈 가능: 기본 제공되는 문서 구조 외에도 사용자 정의 문서 조각을 작성하여 문서화 과정을 완전히 제어할 수 있음.
  4. HTTP 요청 및 응답 표시: API의 HTTP 요청과 응답을 자세히 문서화하므로 개발자와 API 사용자의 이해도가 높아짐.

단점

  1. Asciidoctor나 Markdown과 같은 문서 포맷에 익숙하지 않은 경우 시간 투자가 필요하다.
  2. 테스트 코드에 문서화와 관련된 코드가 추가되서 테스트코드가 길어진다.
  3. 완전 자동화된 도구라기 보단 일부분 직접 문서화를 진행해야한다.

장점과 단점을 보고 든 생각은 현재 나에게 적합하지 않은 도구라고 생각했다. 현재 미션은 기간이 짧고 문서화 외에도 학습할 내용이 많아 조금 더 간편 했으면 좋겠다는 생각이 들었다.

Swagger

Swagger는 API의 설계, 문서화, 그리고 API 테스트 돕는 라이브러리입니다. 이를 통해 API 개발자와 사용자는 효과적으로 협업할 수 있으며, API의 생애 주기 전반에 걸쳐 다양한 도구와 기능을 제공

주요 특징

장점

  1. 자동 문서화: Swagger는 API의 구조와 엔드포인트를 자동으로 분석하여 문서화합니다. 결과 문서는 실시간으로 업데이트되어 항상 최신 상태를 유지함.
  2. 인터랙티브한 탐색: Swagger UI를 통해 개발자와 클라이언트는 API의 엔드포인트를 실시간으로 탐색하고 테스트할 수 있음.
  3. 표준화: Swagger는 OpenAPI 명세와 호환되므로, API의 설명 및 문서화가 표준화되어 있음.

단점

  1. 문서화를 디테일 하게 하기 위해서는 프로덕션 코드에 문서화를 위한 코드가 필요함.
  2. 일부 경우에 Swagger가 애플리케이션의 로딩 시간에 영향을 미칠 수 있으며, 많은 리소스를 사용할 수 있음.

REST Docs와 비교했을 때 현재 내 상황에 사용하기 더 적합하다고 생각했다. 하지만 직접 사용하면서 아쉬운 부분이 있었는데 처음에는 Swagger의 자동 문서화를 사용해 프로덕션 코드에 영향을 주지 않고 사용했다. 하지만 클라이언트에 정확한 정보를 전달해 줄 수 없다는 단점이 존재했고 어노테이션을 사용하니 문서화를 위한 코드가 프로덕션 코드에 추가된다는 게 내키지 않았다.

REST Docs와 Swagger의 장단점이 명확해서 한쪽의 단점을 없앨 수 있다면 좋겠다고 생각하고 검색을 하던 중에 REST Docs + Swagger 방법을 찾았고 이 방법을 적용해 보기로 했다.

설정하기

buildscript {
    ext {
        restdocsApiSpecVersion = '0.18.2' // restdocsApiSpecVersion 버전 변수 설정
    }
}

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

group = 'sample'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
//snippetsDirectory 설정
    snippetsDir = file('build/generated-snippets')
}

dependencies {
    // Spring REST Docs 의존성
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    // epages REST Docs 의존성
    testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}" 
    // Swagger 의존성
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
}

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

tasks.withType(GenerateSwaggerUI) {
// openapi3 테스크 이후에 실행 됨
    dependsOn 'openapi3'

    //기존 파일 삭제했다가, build 에 json 정적파일 복사 (안해도 됨 → local 확인용)
    delete file('src/main/resources/static/docs/')
    copy {
        from "build/resources/main/static/docs"
        into "src/main/resources/static/docs/"
    }
}

// restdocs-api 옵션 설정 외에 설정은 공식문서 참고
openapi3 {
    server = "http://localhost:8080"
    title = "SAMPLE API 문서"
    description = "Spring REST Docs with SwaggerUI."
    version = "0.0.1"
    outputFileNamePrefix = 'SAMPLE - 0.0.1'
    format = 'json'

    // /static/docs/SAMPLE - 0.0.1.json 생성 → jar 파일만 배포할 예정이기에 build 에 출력
    outputDirectory = 'build/resources/main/static/docs'
}

//openAPI 작성 자동화를 위해 패키징 전에 openapi3 테스크 선실행 설정
bootJar {
    dependsOn(':openapi3')
}

코드 작성

@PostMapping()
    @ResponseStatus(CREATED)
    public PostResponse create(@RequestBody PostCreateRequest request,
                               @SessionAttribute(name = "userId", required = false) Long userId) {
        validateUserId(userId);
        return postService.create(request, userId);
    }

테스트 코드 작성

import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.resourceDetails;
import static com.epages.restdocs.apispec.ResourceDocumentation.headerWithName;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@WebMvcTest(PostController.class)
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith(RestDocumentationExtension.class)
public class PostControllerTest {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private PostService postService;


    @Test
    void create() throws Exception {
        PostCreateRequest request = new PostCreateRequest("제목", "내용");
        given(postService.create(any(), any())).willReturn(new PostResponse("제목", "내용"));

        MockHttpSession session = new MockHttpSession();
        session.setAttribute("userId", 1L);

        mockMvc.perform(post("/api/posts")
                        .session(session)
                        .accept(MediaType.APPLICATION_JSON)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request))
                )
                .andDo(document("post-create",
                                resourceDetails().description("게시물 생성"),
                                preprocessRequest(prettyPrint()),
                                preprocessResponse(prettyPrint()),
                                requestHeaders(
                                        headerWithName("JsessionID").description("세션").getAttributes()),
                                requestFields(
                                        fieldWithPath("title").type(JsonFieldType.STRING)
                                                .description("제목"),
                                        fieldWithPath("content").type(JsonFieldType.STRING)
                                                .description("내용")
                                ),
                                responseFields(
                                        fieldWithPath("title").type(JsonFieldType.STRING)
                                                .description("제목"),
                                        fieldWithPath("content").type(JsonFieldType.STRING)
                                                .description("내용")
                                )
                        )
                )
                .andDo(print())
                .andExpect(status().isCreated());
    }

⛔ 공식문서에서는 MockMvcRestDocumentation.document 를 MockMvcRestDocumentationWrapper.document 로 변경하는 것을 추천하고 있습니다.
MockMvcRestDocumentationWrapper.document는 지정된 스니펫을 실행하고, 입력된 스니펫 정보와 함께 ResourceSnippet을 추가합니다.

 

 

 

테스트코드의 작성을 마쳤으면 제대로 동작하는지 확인 후 gradle 태스크에서 openapi3 를 실행해 문서를 생성합니다.

 

그러면 main - java - resources - static - docs에 문서가 생긴걸 확인 할 수 있습니다.

 

 

Swagger default Json 경로 수정

//application.yml
springdoc:
  swagger-ui:
    url: /docs/SAMPLE - 0.0.1.json
    path: /docs/swagger

Swagger의 기본 경로를 수정해 준 뒤 서버를 실행하고 http://localhost:8080/docs/swagger를 접속하면 문서를 확인 할 수 있다.

 

 


 

 

참고 자료

- https://thalals.tistory.com/433

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

 

[Spring] restdocs + swagger ui 같이사용하기 (restdocs 문서 자동화)

✨ api 문서화 정리 글 Swagger Spring Rest Docs RestDocs + Swagger-UI 같이사용하기 오느른, 오늘은,, 오늘우리는,,, Spring Restdocs 를 사용해 test 코드로 Ascciidoc 문서조각을 모으고,, 모아서 만든 adoc 문서를 또

thalals.tistory.com