Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

Develog

코드스테이츠 57일차 본문

코드스테이츠

코드스테이츠 57일차

안형준 2022. 7. 15. 20:47

학습 목표

  • API 문서화가 무엇인지 이해 할 수 있다.
  • API 문서화가 왜 필요한지 이해할 수 있다.
  • Spring Rest Docs의 사용법을 이해할 수 있다.
  • Spring Rest Docs와 Swagger의 차이점을 이해할 수 있다.
  • Spring Rest Docs를 이용해서 API를 문서화 할 수 있다.
  • 문서화 된 API를 외부 사용자에게 제공할 수 있다.

 

API 문서화란?

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

 

이처럼 API 요청을 위해 필요한 정보들을 문서로 잘 정리해야 하는 이유는?

우리가 만들어 놓은 REST API 기반의 백엔드 애플리케이션을 클라이언트 쪽에서 사용하려면 API 사용을 위한 어떤 정보가 필요하기 때문이다.

API 사용을 위한 어떤 정보가 담겨 있는 문서를 API 문서 또는 API 스펙(사양, Specification)이라고한다.

API 문서는 개발자가 요청 URL(또는 URI) 등의 API 정보를 직접 수기로 작성할 수도 있고, 애플리케이션 빌드를 통해 API 문서를 자동 생성할 수도 있다.

 

API 문서 생성의 자동화가 필요한 이유

당연히 API 문서도 수기로 직접 작성해야 되는 것은 너무나 비효율적이다.

또한 한번 작성된 API 문서에 기능이 추가되거나 수정되면 API 문서 역시 함께 수정되어야 하는데 아무래도 사람이 직접 하는 일이다보니 깜빡하고 API 문서에 추가된 기능을 빠뜨릴수도 있고, 클라이언트에게 제공된 API 정보와 수기로 작성한 API 문서의 정보가 다를수도 있다.

실제로 클라이언트에게 제공한 API 문서와 백엔드 애플리케이션의 API 스펙 정보가 달라서 프런트엔드 쪽에서 API 문서를 기반으로 백엔드 애플리케이션 쪽에 요청을 전송했을 때 에러가 발생하는 경우가 빈번하다.

API 문서 자동화를 통해서 작업 시간을 단축하고, 애플리케이션의 완성도를 높여줄 수 있다.

 

Spring Rest Docs vs Swagger

Swagger의 API 문서화 방식

Java 기반의 애플리케이션에서는 전통적으로 Swagger라는 API 문서 자동화 오픈 소스를 많이 사용해왔다.

@ApiOperation(value = "회원 정보 API", tags = {"Member Controller"}) // (1)

@RestController

@RequestMapping("/v11/swagger/members")

@Validated

@Slf4j

public class MemberControllerSwaggerExample {

    private final MemberService memberService;

    private final MemberMapper mapper;



    public MemberControllerSwaggerExample(MemberService memberService, MemberMapper mapper) {

        this.memberService = memberService;

        this.mapper = mapper;

    }



    // (2)

    @ApiOperation(value = "회원 정보 등록", notes = "회원 정보를 등록합니다.")



    // (3)

    @ApiResponses(value = {

            @ApiResponse(code = 201, message = "회원 등록 완료"),

            @ApiResponse(code = 404, message = "Member not found")

    })

    @PostMapping

    public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post memberDto) {

        Member member = mapper.memberPostToMember(memberDto);

        member.setStamp(new Stamp()); // homework solution 추가



        Member createdMember = memberService.createMember(member);



        return new ResponseEntity<>(

                new SingleResponseDto<>(mapper.memberToMemberResponse(createdMember)),

                HttpStatus.CREATED);

    }



    ...

    ...



    // (4)

    @ApiOperation(value = "회원 정보 조회", notes = "회원 식별자(memberId)에 해당하는 회원을 조회합니다.")

    @GetMapping("/{member-id}")

    public ResponseEntity getMember(

            @ApiParam(name = "member-id", value = "회원 식별자", example = "1")  // (5)

            @PathVariable("member-id") @Positive long memberId) {

        Member member = memberService.findMember(memberId);

        return new ResponseEntity<>(

                new SingleResponseDto<>(mapper.memberToMemberResponse(member))

                                    , HttpStatus.OK);

    }



    ...

    ...

}

Swagger를 사용하면 (1) ~ (5)와 같이 API 문서를 만들기 위한 무수히 많은 애너테이션들이 애플리케이션 코드에 추가되어야 한다.

기능이 늘어나면 늘어날수록 API 문서를 위한 코드 역시 엄청나게 늘어난다.

심지어 Controller가 끝이 아니라 Request Body나 Response Body 같은 DTO 클래스에도 추가해야 한다.

하지만 장점도 있다

Postman처럼 API 요청 툴로써의 기능을 사용할 수 있다는 것이 Swagger의 대표적인 장점이라고 볼 수 있다.

 

Spring Rest Docs의 API 문서화 방식

Spring Rest Docs와 Swagger의 가장 큰 차이점은 Spring Rest Docs의 경우 애플리케이션 기능 구현과 관련된 코드에는 API 문서 생성을 위한 애너테이션 같은 어떠한 정보도 추가되지 않는다는 것이다.

@WebMvcTest(MemberController.class)

@MockBean(JpaMetamodelMappingContext.class)

@AutoConfigureRestDocs

public class MemberControllerRestDocsTest {

    @Autowired

    private MockMvc mockMvc;



    @MockBean

    private MemberService memberService;



    @MockBean

    private MemberMapper mapper;



    @Autowired

    private Gson gson;



    @Test

    public void postMemberTest() throws Exception {

        // given

        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",

                "홍길동",

                "010-1234-5678");

        String content = gson.toJson(post);



        MemberDto.response responseDto =

                new MemberDto.response(1L,

                        "hgd@gmail.com",

                        "홍길동",

                        "010-1234-5678",

                        Member.MemberStatus.MEMBER_ACTIVE,

                        new Stamp());



        // willReturn()이 최소 null은 아니어야 한다.

        given(mapper.memberPostToMember(Mockito.any(MemberDto.Post.class)))

                .willReturn(new Member());



        given(memberService.createMember(Mockito.any(Member.class)))

                .willReturn(new Member());



        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);



        // when

        ResultActions actions =

                mockMvc.perform(

                        post("/v11/members")

                                .accept(MediaType.APPLICATION_JSON)

                                .contentType(MediaType.APPLICATION_JSON)

                                .content(content)

                );



        // then

        actions

                .andExpect(status().isCreated())

                .andExpect(jsonPath("$.data.email").value(post.getEmail()))

                .andExpect(jsonPath("$.data.name").value(post.getName()))

                .andExpect(jsonPath("$.data.phone").value(post.getPhone()))

                .andDo(document("post-member",    // =========== (1) API 문서화 관련 코드 시작 ========

                        getRequestPreProcessor(),

                        getResponsePreProcessor(),

                        requestFields(

                                List.of(

                                        fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),

                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),

                                        fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호")

                                )

                        ),

                        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("스탬프 갯수")

                                )

                        )

                ));   // =========== (2) API 문서화 관련 코드 끝========

    }

}

Spring Rest Docs를 사용한 API 문서화의 대표적인 장점은 테스트 케이스에서 전송하는 API 문서 정보와 Controller에서 구현한 Request Body, Response Body, Query Parmeter 등의 정보가 하나라도 일치하지 않으면 테스트 케이스의 실행 결과가 “failed” 되면서 API 문서가 정상적으로 생성이 되지 않는다는 것이다.

따라서 우리는 애플리케이션에 정의되어 있는 API 스펙 정보와 API 문서 정보의 불일치로 인해 발생하는 문제를 방지할 수 있다.

Spring Rest Docs의 대표적인 단점이라면 테스트 케이스를 일일이 작성해야 되고, Controller에 대한 모든 테스트 케이스를 “passed”로 만들어야 한다는 점이다.

 

총정리

  • API 문서화란 클라이언트가 REST API 백엔드 애플리케이션에 요청을 전송하기 위해서 알아야 되는 요청 정보(요청 URL(또는 URI), request body, query parameter 등)를 문서로 잘 정리하는 것을 의미한다.
  • API 사용을 위한 어떤 정보가 담겨 있는 문서를 API 문서 또는 API 스펙(사양, Specification)이라고 한다.
  • API 문서 생성의 자동화가 필요한 이유
  • ㄴAPI 문서를 수기로 직접 작성하는 것은 너무 비효율적이다.
  • ㄴAPI 문서에 기능이 추가되거나 수정되면 API 문서 역시 함께 수정되어야 하는데, API 문서를 수기로 작성하면 API 문서에 추가된 기능을 빠뜨릴수도 있고, 클라이언트에게 제공된 API 정보와 수기로 작성한 API 문서의 정보가 다를수도 있다.
  • Swagger의 API 문서화 방식
  • ㄴ애터네이션 기반의 API 문서화 방식
  • ㄴ애플리케이션 코드에 문서화를 위한 애너테이션들이 포함된다.
  • ㄴ가독성 및 유지 보수성이 떨어진다.
  • ㄴAPI 문서와 API 코드 간의 정보 불일치 문제가 발생할 수 있다.
  • ㄴAPI 툴로써의 기능을 활용할 수 있다.
  • Spring Rest Docs의 API 문서화 방식
  • ㄴ테스트 코드 기반의 API 문서화 방식
  • ㄴ애플리케이션 코드에 문서화를 위한 정보들이 포함되지 않는다.
  • ㄴ테스트 케이스의 실행이 “passed”여야 API 문서가 생성된다.
  • ㄴ테스트 케이스를 반드시 작성해야된다.
  • ㄴAPI 툴로써의 기능은 제공하지 않는다.

 

Spring Rest Docs란?

Spring Rest Docs는 REST API 문서를 자동으로 생성해 주는 Spring 하위 프로젝트이다.

Spring Rest Docs의 가장 큰 특징은 Controller의 슬라이스 테스트를 통해 테스트가 통과 되어야지만 API 문서가 정상적으로 만들어 진다는 것이다.

 

Spring Rest Docs의 API 문서 생성 흐름

  1. 테스트 코드 작성
  • 슬라이스 테스트 코드 작성

ⅰ. Spring Rest Docs는 Controller의 슬라이스 테스트와 밀접한 관련이 있다고 볼 수 있다. Controller에 대한 슬라이스 테스트 코드를 먼저 작성한다.

  • API 스펙 정보 코드 작성

ⅰ. 슬라이스 테스트 코드 다음에 Controller에 정의 되어 있는 API 스펙 정보(Request Body, Response Body, Query Parameter 등)를 코드로 작성한다.

 

  1. test 태스크(task) 실행
  • 작성된 슬라이스 테스트 코드를 실행한다.

ⅰ. 하나의 테스트 클래스를 실행시켜도 되지만 일반적으로 Gradle의 빌드 태스크(task)중 하나인 test task를 실행 시켜서 API 문서 스니핏(snippet)을 일괄 생성한다.

  • 테스트 실행 결과가 “passed”이면 다음 작업을 진행하고, “failed”이면 문제를 해결하기 위해 테스트 케이스를 수정한 후, 다시 테스트를 진행해야 한다.

 

  1. API 문서 스니핏(.adoc 파일) 생성
  • 테스트 케이스의 테스트 실행 결과가 “passed”이면 테스트 코드에 포함된 API 스펙 정보 코드를 기반으로 API 문서 스니핏이 .adoc 확장자를 가진 파일로 생성된다.

스니핏(snippet)은 일반적으로 코드의 일부 조각을 의미하는 경우가 많은데 여기서는 문서의 일부 조각을 의미한다.

스니핏은 테스트 케이스 하나 당 하나의 스니핏이 생성되며, 여러개의 스니핏을 모아서 하나의 API 문서를 생성할 수 있다.

 

  1. API 문서 생성
  • 생성된 API 문서 스니핏을 모아서 하나의 API 문서로 생성한다.

 

  1. API 문서를 HTML로 변환
  • 생성된 API 문서를 HTML 파일로 변환한다.
  • HTML로 변환된 API 문서는 HTML 파일 자체를 공유할 수도 있고, URL을 통해 해당 HTML에 접속해서 확인할 수 있다.

 

Spring Rest Docs 설정

Spring Rest Docs가 API 문서 생성 작업을 정상적으로 수행할 수 있도록 기본적인 설정 작업을 먼저 해 주어야 한다.

  1. build.gradle 설정
  2. API 문서 스니핏을 사용하기 위한 템플릿 API 문서 생성

 

build.gradle 설정

plugins {

id 'org.springframework.boot' version '2.7.1'

id 'io.spring.dependency-management' version '1.0.11.RELEASE'

id "org.asciidoctor.jvm.convert" version "3.3.2"    // (1)

id 'java'

}



group = 'com.codestates'

version = '0.0.1-SNAPSHOT'

sourceCompatibility = '11'



repositories {

mavenCentral()

}



// (2)

ext {

set('snippetsDir', file("build/generated-snippets"))

}



// (3)

configurations {

asciidoctorExtensions

}



dependencies {

       // (4)

testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

  

  // (5) 

asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'



implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'org.springframework.boot:spring-boot-starter-validation'

implementation 'org.springframework.boot:spring-boot-starter-web'

compileOnly 'org.projectlombok:lombok'

runtimeOnly 'com.h2database:h2'

annotationProcessor 'org.projectlombok:lombok'

testImplementation 'org.springframework.boot:spring-boot-starter-test'

implementation 'org.mapstruct:mapstruct:1.5.1.Final'

annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final'

implementation 'org.springframework.boot:spring-boot-starter-mail'



implementation 'com.google.code.gson:gson'

}



// (6)

tasks.named('test') {

outputs.dir snippetsDir

useJUnitPlatform()

}



// (7)

tasks.named('asciidoctor') {

configurations "asciidoctorExtensions"

inputs.dir snippetsDir

dependsOn test

}



// (8)

task copyDocument(type: Copy) {

dependsOn asciidoctor            // (8-1)

from file("${asciidoctor.outputDir}")   // (8-2)

into file("src/main/resources/static/docs")   // (8-3)

}



build {

dependsOn copyDocument  // (9)

}



// (10)

bootJar {

dependsOn copyDocument    // (10-1)

from ("${asciidoctor.outputDir}") {  // (10-2)

into 'static/docs'     // (10-3)

}

}
  1. (1)에서는 .adoc 파일 확장자를 가지는 AsciiDoc 문서를 생성해주는 Asciidoctor를 사용하기 위한 플러그인을 추가한다.
  2. (2)에서는 ext 변수의 set() 메서드를 이용해서 API 문서 스니핏이 생성될 경로를 지정한다.
  3. (3)에서는 AsciiDoctor에서 사용되는 의존 그룹을 지정하고 있다. :asciidoctor task가 실행되면 내부적으로 (3)에서 지정한 ‘asciidoctorExtensions’라는 그룹을 지정한다.
  4. (4)에서 'org.springframework.restdocs:spring-restdocs-mockmvc'를 추가함으로써 spring-restdocs-core와 spring-restdocs-mockmvc 의존 라이브러리가 추가된다.
  5. (5)에서 spring-restdocs-asciidoctor 의존 라이브러리를 추가한다. (3)에서 지정한 asciidoctorExtensions 그룹에 의존 라이브러리가 포함이 된다.
  6. (6)에서는 :test task 실행 시, API 문서 생성 스니핏 디렉토리 경로를 설정한다.
  7. (7)에서는 :asciidoctor task 실행 시, Asciidoctor 기능을 사용하기 위해 :asciidoctor task에 asciidoctorExtensions 을 설정한다.
  8. (8)은 :build task 실행 전에 실행되는 task이다. :copyDocument task가 수행되면 index.html 파일이 src/main/resources/static/docs 에 copy 되며, copy된 index.html 파일은 API 문서를 파일 형태로 외부에 제공하기 위한 용도로 사용할 수 있다.
  • (8-1)에서는 :asciidoctor task가 실행된 후에 task가 실행 되도록 의존성을 설정한다.
  • (8-2)에서는 "build/docs/asciidoc/" 경로에 생성되는 index.html을 copy한 후,
  • (8-3)의 "src/main/resources/static/docs" 경로로 index.html을 추가해준다.
  1. (9)에서는 :build task가 실행되기 전에 :copyDocument task가 먼저 수행 되도록 한다.
  2. (10)에서는 애플리케이션 실행 파일이 생성하는 :bootJar task 설정이다.
  • (10-1)에서는 :bootJar task 실행 전에 :copyDocument task가 실행 되도록 의존성을 설정한다.
  • (10-2)에서는 Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가해준다.
  • jar 파일에 index.html을 추가해 줌으로써 웹 브라우저에서 접속(http://localhost:8080/docs/index.html) 후, API 문서를 확인할 수 있다.

 

(8)에서 copy되는 index.html은 외부에 제공하기 위한 용도이고, (10)에서는 index.html을 애플리케이션 실행 파일인 jar 파일에 포함해서 웹 브라우저에서 API 문서를 확인하기 위한 용도라는 것을 기억하자

 

API 문서 스니핏을 사용하기 위한 템플릿(또는 source 파일) 생성

build.gradle에 API 문서 생성을 위한 설정이 완료 되었다.

마지막으로 할 일은 API 문서 스니핏이 생성 되었을 때 이 스니핏을 사용해서 최종 API 문서로 만들어 주는 템플릿 문서(index.adoc)를 생성하는 것이다.

  • Gradle 기반 프로젝트에서는 아래 경로에 해당하는 디렉토리를 생성해 주어야 한다.
  • ㄴsrc/docs/asciidoc/
  • 다음으로 src/docs/asciidoc/ 디렉토리 내에 비어있는 템플릿 문서(index.adoc)를 생성해주면 된다.

 

총정리

  • Spring Rest Docs의 API 문서 생성 흐름은 다음과 같습니다.
  • ㄴ슬라이스 테스트 코드 작성 →
  • ㄴAPI 스펙 정보 코드 작성 →
  • ㄴtest 태스크 실행 →
  • ㄴAPI 문서 스니핏 생성
  • ㄴ스니핏을 포함한 API 문서 생성
  • ㄴ.adoc 파일의 API 문서를 HTML로 변환
  • Spring Rest Docs를 사용해서 API 문서를 생성하기 위해서는 .adoc 문서 스니핏을 생성해주는 Asciidoctor가 필요하다.
  • HTML 파일로 변환된 API 문서는 외부에 제공할 수도 있고, 웹브라우저에 접속해서 API 문서를 확인할 수도 있다.

'코드스테이츠' 카테고리의 다른 글

코드스테이츠 59일차  (0) 2022.07.19
코드스테이츠 58일차  (0) 2022.07.18
코드스테이츠 56일차  (2) 2022.07.14
코드스테이츠 55일차  (0) 2022.07.13
코드스테이츠 54일차  (0) 2022.07.12