API 문서화 - Spring Rest Docs

귀찮Lee·2022년 7월 20일
0

Spring

목록 보기
28/30
post-custom-banner

◎ API 문서화

  • API 문서화

    • 클라이언트가 REST API 백엔드 애플리케이션에 요청을 전송하기 위해서 알아야 되는 요청 정보를 문서로 잘 정리하는 것
    • URI, request body, query parameter ... 등을 문서로 정리
  • API 문서화 목적

    • 만들어 놓은 REST API 기반의 백엔드 애플리케이션을 클라이언트 쪽에서 사용하려면 정보가 필요함
    • 정보를 사용하는 측에 제공하기 위해
  • API 문서 생성 자동화 필요성

    • API 문서를 수기로 작성하는 것은 매우 비효율적, 제공된 정보와 실제 정보가 다를 수 있다.
    • 작업 시간 단축, 어플리케이션의 완성도 증가를 기대할 수 있다.

◎ Spring Rest Docs

  • Spring Rest Docs vs Swagger

    • Swagger :
      • 상대적으로 많은 곳에서 Swagger 코드가 사용됨
      • 문서화를 보는 입장에서는 편할 수 있으나, 프로그래머 입장에서는 Swagger 관련 코드와 비즈니스 코드를 구분하기가 힘듦
      • 일부 스펙 정보를 문자열로 입력 (일치하지 않을 수 있음)
    • Spring Rest Docs
      • Test 코드에서만 문서화 관련 코드를 사용하면 됨
      • Test 코드가 통과하지 않으면 생성되지 않음
      • 문서화 코드에서 틀린 내용이 있다면 테스트가 실패함.
  • Spring Rest Docs

    • API 문서 생성 흐름
      슬라이스 테스트 코드, API 스팩 정보 코드 작성
      → test 테스크 실행
      → 테스트 성공시, API 문서 스니핏 생성 (.adoc)
      → API 문서 생성 (.adoc) / 일부 수기 작성
      → API 문서를 HTML로 변환 (gradle 설정 필요)
  • Spring Rest Docs 설정

    plugins {
        ...
        // .adoc 파일 확장자를 가지는 AsciiDoc 문서를 생성해주는 Asciidoctor를 사용하기 위한 플러그인을 추가
        id "org.asciidoctor.jvm.convert" version "3.3.2"
        id 'java'
    }
    
    group = 'com.codestates'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '11'
    
    repositories {
        mavenCentral()
    }
    
    // ext 변수의 set() 메서드를 통해, API 문서 스니펫 경로 설정
    ext {
        set('snippetsDir', file("build/generated-snippets"))
    }
    
    // AsciiDoctor에서 사용되는 의존 그룹을 지정
    configurations {
        asciidoctorExtensions
    }
    
    dependencies {
        // spring-restdocs-core와 spring-restdocs-mockmvc 의존 라이브러리가 추가
        testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
        // spring-restdocs-asciidoctor 의존 라이브러리를 추가
        asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    
        ...
    }
    
    // test task 실행 시, API 문서 생성 스니핏 디렉토리 경로를 설정
    tasks.named('test') {
        outputs.dir snippetsDir
        useJUnitPlatform()
    }
    
    // asciidoctor task 실행 시, Asciidoctor 기능을 사용하기 위해   
    // asciidoctor task에 asciidoctorExtensions 을 설정
    tasks.named('asciidoctor') {
        configurations "asciidoctorExtensions"
        inputs.dir snippetsDir
        dependsOn test
    }
    
    // build task 실행 전에 실행되는 task
    // index.html 파일이 copy됨
    task copyDocument(type: Copy) {
        // asciidoctor task가 실행된 후에 task가 실행 되도록 의존성을 설정
        dependsOn asciidoctor  
        // build/docs/asciidoc/" 경로에 생성되는 index.html을 copy
        from file("${asciidoctor.outputDir}")
        // src/main/resources/static/docs 경로로 index.html을 추가
        into file("src/main/resources/static/docs")
    }
    
    // build 실행 전, copyDocument task가 먼저 수행 되도록 한다.
    build {
        dependsOn copyDocument  
    }
    
    bootJar {
        // bootJar task 실행 전에 copyDocument task가 실행 되도록 의존성을 설정
        dependsOn copyDocument
        // Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가
        // 웹 브라우저에서 접속(http://localhost:8080/docs/index.html) 후, API 문서를 확인 가능
        from ("${asciidoctor.outputDir}") {  // (10-2)
            into 'static/docs'     // (10-3)
        }
    }

◎ Spring Rest Docs 적용

  • Spring Rest Docs 적용

          	...
              
    		// when
            ResultActions actions =
                    mockMvc.perform(
                                patch("/v11/members/{member-id}", memberId)
                                    .accept(MediaType.APPLICATION_JSON)
                                    .contentType(MediaType.APPLICATION_JSON)
                                    .content(content)
                    );
    
            // then
            actions.andExpect(status().isOk())
                    .andExpect(jsonPath("$.data.memberId").value(patch.getMemberId()))
                    .andExpect(jsonPath("$.data.name").value(patch.getName()))
                    .andExpect(jsonPath("$.data.phone").value(patch.getPhone()))
                    .andExpect(jsonPath("$.data.memberStatus").value(patch.getMemberStatus().getStatus()))
                    // andDo 부분이 Spring Rest Docs
                    .andDo(document("patch-member", // 문서 식별자
                            preprocessRequest(prettyPrint());, // 요청 데이터 이쁘게 보여줌
                            preprocessResponse(prettyPrint());, // 응답 데이터 이쁘게 보여줌
                            pathParameters(
                                    parameterWithName("member-id").description("회원 식별자")
                            ),
                            requestFields(
                                    List.of(
                                            fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(),
                                            fieldWithPath("name").type(JsonFieldType.STRING).description("이름").optional(),
                                            fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호").optional(),
                                            fieldWithPath("memberStatus").type(JsonFieldType.STRING)
                                            		.description("회원 상태: MEMBER_ACTIVE / MEMBER_SLEEP / MEMBER_QUIT").optional()
                                    )
                            ),
                            responseFields(
                                    List.of(
                                            fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                            fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                                            fieldWithPath("data.email").type(JsonFieldType.STRING).description("이메일"),
                                            fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
                                            fieldWithPath("data.phone").type(JsonFieldType.STRING).description("휴대폰 번호"),
                                            fieldWithPath("data.memberStatus").type(JsonFieldType.STRING).description("회원 상태: 활동중 / 휴면 상태 / 탈퇴 상태"),
                                            fieldWithPath("data.stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수")
                                    )
                            )
                    ));
  • Snippets 종류

    // query Parameter 표기
    requestParameters(
        parameterWithName("page").description("page 번호")
    )
    
    // path Parameter 표기
    // 이름이 ResultAction에 사용한 주소와 일치 
    // ex) delete("/v11/members/{member-id}", memberId)
    pathParameters(
        parameterWithName("member-id").description("회원 식별자")
    )
    
    // 요청 데이터 표기
    // ignored() 를 통해 Dto 일부 제거, optional() 을 통해 필수 여부 표기 가능
    requestFields(
        fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(),
        fieldWithPath("name").type(JsonFieldType.STRING).description("이름").optional()
    )
    
    // 응답 데이터 표기
    responseFields(
         fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
         // JSON 배열 표기 [] 만 사용
         fieldWithPath("data[].memberId").type(JsonFieldType.NUMBER).description("회원 식별자")
    )
    • 테스트 통과시 설정에 있었던 경로("build/generated-snippets")에 문서 스니펫이 생성
  • 주의사항

    • pathParameters를 사용할거면 MockMvcBuilders가 아니라 RestDocumentationRequestBuilders를 이용해야 한다.
      • mockMvc.perform() 안에 부분에 post, delete, get ... 의 메서드를 RestDocumentationRequestBuilders를 이용
      • 관련 자료 : https://java.ihoney.pe.kr/517

◎ 추가 내용

profile
배운 것은 기록하자! / 오류 지적은 언제나 환영!
post-custom-banner

0개의 댓글