[Section3] Spring MVC - API 문서화

2023. 7. 3. 23:10

🧑🏻‍💻 TIL(Today I Learned)


✔️ API 문서화, Spring Rest Docs

 

💡 API 문서화(Documentation)

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

 

API 요청을 위해 필요한 정보들을 문서로 잘 정리해야 하는 이유는? 
:  만들어 놓은 REST API 기반의 백엔드 애플리케이션을 클라이언트 쪽에서 사용하려면 API 사용을 위한 어떤 정보가 필요하기 때문에 

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

 

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

➡️ 프로젝트 진행 중 프론트엔드 개발자가 개발한 애플리케이션의 API 정보를 문서로 제공해 줄 것을 요청하여 API 문서를 하나하나 수기로 작성한 후에 제공하려고 한다면 너무 비효율적임 또한 API에 기능이 추가되거나 수정되면 API 문서 역시 함께 수정되어야 하는데 그 과정에서 추가된 기능을 빠뜨릴 수도 있고 클라이언트에 제공된 API 정보와 수기로 작성한 정보가 다를 수도 있음 

이러한 이유로 API 문서 자동화를 통해 작업 시간을 단축하고 오류를 줄여 애플리케이션의 완성도를 높일 수 있음 

 

 

🔎 Spring Rest Docs 와 Swagger

✔️ Swagger의  API 문서화 방식

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

@ApiOperation(value = "회원 정보 API", tags = {"Member Controller"}) //📍
@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;
    }

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

	//📍
    @ApiResponses(value = {
            @ApiResponse(code = 201, message = "회원 등록 완료"),
            @ApiResponse(code = 404, message = "Member not found")
    })
    
    ... ...
    
    //📍
    @ApiOperation(value = "회원 정보 조회", notes = "회원 식별자(memberId)에 해당하는 회원을 조회합니다.")
    @GetMapping("/{member-id}")
    public ResponseEntity getMember(
            @ApiParam(name = "member-id", value = "회원 식별자", example = "1")  //📍
            @PathVariable("member-id") @Positive long memberId) {
        Member member = memberService.findMember(memberId);
        return new ResponseEntity<>(
                new SingleResponseDto<>(mapper.memberToMemberResponse(member))
                                    , HttpStatus.OK);
    }
  • Swagger를 사용하면 표시한 것과 같이 API 문서를 만들기 위한 많은 애너테이션을 애플리케이션에 추가해야 함 
  • Controller 뿐 아니라 RequestBody나  ResponseBody 같은 DTO 클래스에도 위와 같은 애너테이션을 일일이 추가해야 함 
  • 하지만 Swagger에서는 Postman처럼 API 툴로써 기능을 사용할 수 있다는 장점이 있음 

 

✔️ Spring Rest Docs의 API 문서화 방식 

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

➡️ 대신 슬라이스 테스트를 위한 테스트 클래스에 API 문서를 위한 정보가 추가됨 

// then
        actions
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v11/members/"))))
                .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("휴대폰 번호")
                                )
                        ),
                        responseHeaders(
                                headerWithName(HttpHeaders.LOCATION).description("Location header. 등록된 리소스의 URI")
                        )
                ));   // =========== (2) API 문서화 관련 코드 끝========
  • 테스트 코드에 API 문서화를 위한 위와 같은 코드를 추가하면 문서 생성을 할 수 있음 
  • Spring Rest Docs를 사용한 API 문서화는 테스트 케이스에서 전송하는 API 문서 정보와 Controller에서 구현한 RequestBody, ResponseBody, Query Parameter 등의 정보가 하나라도 일치하지 않으면 테스트 케이스의 실행 결과가 failed 되면서 API 문서가 정상적으로 생성이 되지 않는다는 것이 장점
    즉, 결과를 passed로 만들지 않으면 문서가 생성되지 않음 → API 스펙 정보와 API 문서 정보의 불일치로 인한 문제 방지할 수 있음 
  • 하지만 테스트 케이스를 일일이 작성해야 되고, 모든 테스트 케이스를 "passed"로 만들어야 한다는 것이 단점, 또한 Swagger 처럼 API 툴로써의 기능은 제공하지 않음

 

 

💡 Spring Rest Docs

✔️ Spring Rest Docs의 API 문서 생성 흐름

  1. 테스트 코드 작성 
    • 슬라이스 테스트 코드 작성 
    • API 스펙 정보 코드 작성 
  2. test 태스크(task) 실행
    • 작성된 슬라이스 테스트 코드 실행 
      : 일반적으로 Gradle의 빌트 태스크(task) 중 하나인 test task를 실행시켜서 API 문서 스니펫(.snippet) 일괄 생성 
    • 테스트 실행 결과가 passed이면 다음 작업 진행, failed이면 문제를 해결하기 위해 테스트 케이스 수정한 후 다시 테스트 진행
  3. API 문서 스니펫(.adoc 파일) 생성 
    • 테스트 케이스의 테스트 실행 결과가 passed 이면 테스트 코드에 포함된 API 스펙 정보 코드를 기반으로 API 문서 스니펫이 .adoc 확장자를 가진 파일로 생성됨 
  4. API 문서 생성 
    • 생성된 API 문서 스니펫을 모아서 하나의 API 문서로 생성
  5. API 문서를 HTML로 변환
    • 생성된 API 문서를 HTML 파일로 변환
    • HTML로 변환된 API 문서는 HTML 파일 자체를 공유할 수도 있고 URL을 통해 HTML에 접속해서 확인할 수 있음 
스니펫(Snippet)은 일반적으로 코드의 일부 조각을 의미하지만 여기서는 문서의 일부 조각 의미
테스트 케이스 하나당 하나의 스니펫이 생성되며, 여러 개의 스니펫을 모아 하나의 API 문서를 생성할 수 있음 

 

 

🔎 Spring Rest Docs 설정

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

✔️ build.gradle 설정

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

ext {
	set('snippertsDir', file("build/generated-snippets")) // (2)
}

configurations {
	asciidoctorExtensions //(3)
}

dependencies { 
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' //(4)
    asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' // (5)
}

// (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)
}

bootJar {
	dependsOn copyDocument    // (10-1)
	from ("${asciidoctor.outputDir}") {  // (10-2)
		into 'static/docs'     // (10-3)
	}
}
  1. .adoc 파일 확장자를 가지는  AsciiDoc 문서를 생성해 주는  Asciidoctor를 사용하기 위한 플러그인 추가 
  2. ext 변수의 set() 메서드를 이용해서 API 문서 스니펫이 생성될 경로 지정
  3. AsciiDoctor에서 사용되는 의존 그룹을 지정함,
    :asciidoctor task가 실행되면 내부적으로 지정한 "asciidoctorExtension" 이라는 그룹 지정
  4. spring-resdocs-core, spring-restdocs-mockmvc 의존 라이브러리 추가 
  5. asciidoctorExtension 그룹에 추가한 의존 라이브러리 포함됨 
  6. :test task 실행 시 API 문서 생성 스니펫 디렉토리 경로 설정
  7. :asciidoctor task 실행 시  Asciidoctor 기능을 사용하기 위해 :asciidoctor task에 asciidoctorExtensions을 설정
  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/asciidoctor 경로에 생성되는 index.html을 copy
    • 8-3 : src/main/resources/static/docs 경로로 index.html 추가
  9. : build task가 실행되기 전에 :copyDocument task가 먼저 수행되도록 함 
  10. 애플리케이션 실행 파일이 생성하는 :bootJar task 설정
    • 10-1 : :bootJar task 실행 전에 :copyDocument task가 실행되도록 의존성 설정
    • 10-2 , 10-3
      : Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가해 줌 
      : jar 파일에 index.html을 추가해 줌으로써 웹 브라우저에서 접속 후 API 문서 확인 가능 

 

✔️  API 문서 스니펫을 사용하기 위한 템플릿 생성

➡️ build.gradle 설정이 끝나면 API 문서 스니펫이 생성되었을 때 이 스니펫을 사용해서 최종 API 문서로 만들어주는 템플릿 문서를 생성해야 함 

  • Gradle 기반 프로젝트에서는 "src/docs/asciidoc" 경로에 해당하는 디렉토리 생성해 주어야함 
  • 그리고 디렉토리 내에 비어있는 템플릿 문서를 생성해 주면 됨 

 

 

🔎  API 문서 생성을 위한 테스트 케이스 기본 구조

@WebMvcTest(MemberController.class) // (1)SpringBootTest 사용하지 않음, WebMvcTest는 Controller 전용 애너테이션 
@MockBean(JpaMetamodelMappingContext.class)   // (2)
@AutoConfigureRestDocs    // (3) Spring Rest Docs에 대한 자동 구성 위해 추가
public class MemberControllerRestDocsTest {
    @Autowired
    private MockMvc mockMvc;  // (4)

    @MockBean
	  // (5) 테스트 대상 Controller 클래스가 의존하는 객체를 Mock Bean 객체로 주입받기

    @Test
    public void postMemberTest() throws Exception {
        // given
        // (6) 테스트 데이터 

        // (7) Mock 객체를 이용한 Stubbing

        // when
        ResultActions actions =
                mockMvc.perform(
                     // (8) request 전송
                );

        // then
        actions
                .andExpect(// (9) response에 대한 기대 값 검증)
                .andDo(document(
                            // (10) API 문서 스펙 정보 추가
                 ));
                 
                 //document() : API 문서를 작성하기 위해 Spring Rest Docs에서 지원하는 메서드
                 // andDo() : 검증하는 메서드가 아니라 일반적인 동작을 정의하고자 할 때 사용 
    }
}

(2) @MockBean(JpaMetamodelMappingContext.class)
: JPA에서 사용하는  Bean들을 Mock 객체로 주입해 주는 설정 
: Spring Boot 기반의 테스트는 항상 최상위 패키지 경로에 있는 ~ Application 클래스 찾아서 실행함 

  • @EnableJpaAuditing 을 추가하게 되면 JPA와 관련된 Bean을 필요로 하기 때문에 @Webmvctest 애너테이션을 사용해서 테스트를 진행할 경우에는 JpaMetamodelMappingContext.class를 Mock 객체로 주입해 주어야 함 

 

@SpringBootTest vs @WebMvcTest
: SpringBootTest 애터테이션은  @AutoConfigureMockMvc 와 함께 사용되어 Controller를 테스트할 수 있는데 프로젝트에서 사용하는 전체 Bean을  ApplicationContext에 등록하여 사용함 
→ 테스트 환경을 구성하는 것은 편리하지만 실행 속도가 상대적으로 느리다는 단점이 있음 

: WebMvcTest 애너테이션은  Controller 테스트에 필요한 Bean 만 ApplicationContext에 등록하기 때문에 실행 속도는 상대적으로 빠름
→ 하지만 Controller에서 의존하고 있는 객체가 있다면 해당 객체에 대해서 Mock 객체를 사용하여 의존성을 일일이 제거해야 함

※ 결과적으로 SpringBootTest 는 데이터베이스까지 요청 프로세스가 이어지는 통합 테스트에 주로 사용되고 WebMvcTest는 Controller를 위한 슬라이스 테스트에 주로 사용함 

 

✔️ 실습하기 

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MemberService memberService;

    @MockBean
    private MemberMapper memberMapper;

    @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);

        // Mock 객체를 이용한 Stubbing --> 가짜 메서드 호출
        given(memberMapper.memberPostToMember(Mockito.any(MemberDto.Post.class))).willReturn(new Member());

        Member mockResultMember = new Member();
        mockResultMember.setMemberId(1L);
        given(memberService.createMember(Mockito.any(Member.class))).willReturn(mockResultMember);

        //when
        ResultActions actions =
                mockMvc.perform(
                        post("/v11/members")
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

        //then
        actions
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v11/members/"))))
                // ---- 여기서 API 문서 생성 시작 ----
                // docunment() 메서드는 API 스펙 정보를 전달받아 실질적인 문서화 작업을 수행하는  RestDocumentationResultHandler 클래스에서 핵심 기능을 하는 메서드
                .andDo(document(
                        "post-member", // API 문서 스니펫의 식별자 역할, 문서 스니펫은 post-member 디렉토리 하위에 생성
                        getRequestPreProcessor(), // 문서 스니펫을 생성하기 전에 request와 response에 해당하는 문서 영역을 전처리하는 역할을 함, 공통화한 후 모든 테스트 케이스에서 재사용할 수 있도록 함
                        getResponsePreProcessor(),
                        requestFields( // 문서로 표현될 requestbody 의미, FieldDescriptor 객체가 request body에 포함된 데이터 표현함
                                List.of(
                                        // request body를 JSON 포맷으로 표현했을 때 하나의 프로퍼티를 의미하는 FieldDescriptor
                                        fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호")

                                )
                        ),
                        responseHeaders( // 문서로 표현될 requestresponse 의미 , 파라미터로 전달되는 HeaderDesciptor 객체가 response header 표현
                                headerWithName(HttpHeaders.LOCATION).description("Location header. 등록된 리소스의 URI")
                                // HttpHeaders.LOCATION : HTTP response의 Location header 의미
                        )
                ));

    }

 

✔️ API 문서화하기 

  • 스니펫을 포함하는 템플릿 문서를 포함할 디렉토리 생성
  • 디폴트 경로는 "src/docs/asciidoc" 
  • Asciidoc 문법으로 템플릿 작성하고 Gradle의 :build 또는 :bootJar로 task 명령 실행해서 index.adoc 파일을 index.html 파일로 변환

  • 위와 같이 변환된 html 파일이 생성된 것을 확인할 수 있음 
  • http://localhost:8080/docs/index.html 
    → 애플리케이션을 실행하고 URL 브라우저에 입력하면 입력한 API 문서가 나타남

BELATED ARTICLES

more