서버 API 문서화(Spring Rest docs)

카일·2020년 8월 15일
4

카카오 헤어

목록 보기
2/2
post-thumbnail

해당 문서는 카카오 헤어와 우아한 마켓 모두에게 적용되기 때문에 한 번에 정리하였습니다. 코드는 아래에서 확인하실 수 있습니다.
카카오 헤어샵
우아한 테크 마켓
현재 배포 하지 않은 상태인데, 배포하게 되면 문서 링크도 같이 첨부하겠습니다. 😊

서버 Api를 개발한다면 Client를 위한 Api 문서화 작업을 수행해야 한다. 문서화를 지원하는 기술로는 크게 Swagger, Rest docs가 있다. 어떤 툴을 사용하여 문서화를 진행하면 좋을지에 대해 간략하게 서술하고 어떤 식으로 문서화를 진행했는지 포스팅 할 예정이다.

Swagger VS Rest docs

카카오 헤어샵에서는 Spring Rest Docs 를 선택했다. 이유는 아래와 같다.

  • 문서를 작성하는 이유는 Api를 사용하는 방법을 보여주는 것이다. 즉 코드와 동기화가 되지 않는다면 의미 없다고 생각하였다. 이에 동기화에서 강점을 가진 Rest docs가 나을 것 같다.
  • 현재 각 레이어별로 테스트를 작성하고 있고 Controller Test 또한 존재하기 때문에 테스트 기반으로 작성하는 것이 어렵지 않다.
  • Spring 환경에서는 Annotation 을 통해 의도를 드러내는 경우가 많다. 코드와 관계 없는 어노테이션은 타인에게 혼란을 줄 수 있다고 생각하고 외부 Api가 Production 코드에 노출되는 것은 바람직하지 않다고 생각하였다.

Spring Rest docs 사용법

build.gradle

Rest docs를 사용하기 위해서는 의존성을 우선적으로 추가해야 한다. 본인은 아래와 같이 의존성을 추가하여 Gradle에서 ./gradlew asciidoctor 를 사용할 수 있도록 하였다. 설정 방법은 Spring Rest docs build.gradle이라고 검색하면 구체적으로 확인할 수 있다.

plugins {
    id 'org.springframework.boot' version '2.3.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id "org.asciidoctor.convert" version "1.5.9.2"
    id 'java'
}

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

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
		testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}

test {
    useJUnitPlatform()
}

asciidoctor {
    dependsOn test
}

bootJar {
    dependsOn asciidoctor
    from ("${asciidoctor.outputDir}/html5") {
        into 'static/docs'
    }
}

Controller test

Controller Test에서는 자신의 역할을 수행하고 andDo(MemberDocumentation.createMember()); 부분을 추가하면 MemberDocumentation 클래스에서 문서화를 진행한다.

@DisplayName("회원을 생성하는 요청을 정상적으로 처리한다.")
    @Test
    void create() throws Exception {
        when(memberService.create(any(Member.class))).thenReturn(TokenResponse.of("TEST"));
        when(authorizationInterceptor.preHandle(any(), any(), any())).thenReturn(true);

        final MemberCreateRequest request = MemberFixture.createDto();

        mockMvc.perform(post("/api/members")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsBytes(request))
        )
            .andExpect(status().isOk())
            .andExpect(jsonPath("accessToken").isNotEmpty())
            .andDo(MemberDocumentation.createMember());
    }

문서화

문서화를 위해서 아래와 같이 테스트에서 발생했던 요청 및 응답에 대한 항목을 기록하면 문서로 나타나게 된다.

public static RestDocumentationResultHandler createMember() {
        return document("member/create-member",
            getDocumentRequest(),
            getDocumentResponse(),
            requestHeaders(
                headerWithName(HttpHeaders.CONTENT_TYPE).description("Content-type")
            ),
            requestFields(
                fieldWithPath("name").description("프로필로 사용할 이름"),
                fieldWithPath("socialId").description("소셜 로그인 ID")
            ),
            responseFields(
                fieldWithPath("accessToken").description("사용자가 사용 할 엑세스 토큰")
            )
        );
    }

Customizing

유틸성 메소드 분리

문서화를 위한 코드도 결국은 코드이다. 즉 유지보수 및 변경에 대한 고민이 필요하고 추상화 혹은 역할 분리를 통해 사용하기 편하게 관리되어야 한다. 이를 위해 현재는 문서가 많이 없기 때문에 간단한 부분만 DocumentUtil 로 분리하여 관리하고 있으며, 추가적으로 문서가 많아지는 경우 코드를 어떻게 관리할지 고민해야 한다.

public interface ApiDocumentationUtils {

    static OperationRequestPreprocessor getDocumentRequest() {
        return preprocessRequest(
            modifyUris()
                .scheme("http")
                .host("localhost")
                .port(8080),
            prettyPrint()
        );
    }

    static OperationResponsePreprocessor getDocumentResponse() {
        return preprocessResponse(prettyPrint());
    }

    static ResponseFieldsSnippet getErrorResponseFieldsWithFieldErrors() {
        return responseFields(
            fieldWithPath("status").description("에러 상태"),
            fieldWithPath("code").description("에러 코드"),
            fieldWithPath("message").description("에러 메세지"),
            subsectionWithPath("errors").description("필드 에러").optional()
        );
    }
}

문서의 사용 편의성

문서를 작성하는 이유는 Client가 편리하게 사용할 수 있게 하기 위함이다. 따라서 각 요청과 응답에 대해서 구체적으로 사용법을 적어주는 것이 좋은데, 이 부분을 커스터마이징 하는 방법 중 하나는 아래와 같다.

특정 포맷을 지원하는 형태로 제공하거나 필수값 여부 등을 포맷으로 지정하는 등 다양한 형태로 자신만의 커스터마이징된 문서를 관리할 수 있다.

// 커스텀 문서 인터페이스

public interface DocumentFormatGenerator {
    static Attributes.Attribute getDateFormat() {
        return key("format").value("yyyy-MM-dd");
    }
}

// 사용

fieldWithPath("race_duration.start_date").type(STRING)
                    .attributes(getDateFormat())
                    .description("Race 시작 날짜"),

최종 문서

동일한 소셜 로그인 Api를 카카오, 깃허브, 네이버, 구글 등 다양한 곳에서 지원하지만 본인은 Github가 가장 명확하게 잘 표현되어 있다고 생각한다. 즉 외부에서 사용할 수 있도록 노출되는 Api 문서는 보다 명확하고 간략하게 Api 사용법을 작성해야 하는데 아래와 같은 추가 고민들이 있을 수 있다.

  • 보내는 타입에 대한 추가적인 명시
  • 보내는 필드에 대한 설명 구체화
  • 등등등 ...

추가적인 고민 포인트

Spring Rest Docs는 MockMvcRest Assured 를 모두 지원하는데 일반적으로 Rest Assured는 SpringBootTest 즉 슬라이싱 테스트가 불가능하다. 이에 빌드 시간이 길어지는 요인으로 MockMvc 를 주로 사용한다.

하지만 나는 인수 테스트에서 RestAssured 를 사용해서 SpringBootTest를 사용하는데 그러면 MockMvc 대신 Rest Assured 를 사용하는 형태로 하는 것이 맞을까..?

2개의 댓글

comment-user-thumbnail
2021년 3월 23일

ㅋㅋ

답글 달기
comment-user-thumbnail
2021년 7월 28일

REST Docs 도입에 도움이 되었습니다! 감사합니다

답글 달기