Spring Rest Docs 적용 (1)

2021. 3. 2. 14:23Spring

Spring Rest Docs 란?

  • API 문서 자동화 도구
  • 현재 운영 중인 API 스펙API 문서동일하게 관리하기 위한 목적
  •    - API 스펙이 변경 될 때마다 문서 최신화를 위한 Update가 필요하지만 안되는 경우가 종종 있다.
  • API 스펙에 따른 Test Code 를 강제로 작성해야만 하며 문서화하지 않으면 Test Case 가 실패한다.
  •     - 즉, Rest Docs 로 된 문서는 항상 최신화가 되어 있으며, 신뢰감을 줄 수 있는 문서이다.
  • 자동화 도구에는 대표적으로 Swagger와Spring Rest Docs 가 있다.

 

Spring Rest Docs와 Swagger

출처: 우아한 형제들 기술 블로그 (https://woowabros.github.io/experience/2018/12/28/spring-rest-docs.html)

 

적용 스펙

  • SpringBoot 2.4.3
  • Gradle 6.7.1
  • JUnit 5
  • Spring REST Docs (MockMvc) 2.0.5 RELEASE
  • Asciidoctor 1.5.9.2

 

Gradle 설정

plugins {
    // RestDocs - Asci Convert (플러그인)
    id 'org.asciidoctor.convert' version '1.5.9.2'
}

ext {
    // Asciidoc 문서인 스니펫이 생성될 경로 지정
    set('snippetsDir', file("build/generated-snippets"))
}


// RestDocs 의존성: .adoc 파일에서 빌드, 스니펫 생성을 자동으로 구성되기 위해 추가
dependencies {

    // *.adoc 파일의 {snippets} 자동으로 설정
    asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'				
    // Mock MVC 에 Rest Docs 추가
    testCompile group: 'org.springframework.restdocs', name: 'spring-restdocs-mockmvc'
}


test {
    // snippets 디렉터리를 출력으로 추가하도록 테스트 작업 구성
    outputs.dir snippetsDir
    useJUnitPlatform()
}


// Asciidoctor Task 설정
asciidoctor {
    // snippets 디렉토리를 입력으로 구성
    inputs.dir snippetsDir

    // 문서가 작성되기 전에 테스트가 실행
    // gradle build 시 test → asciidoctor 순으로 수행
    dependsOn test
}


asciidoctor.doFirst {
    println "=====start asciidoctor"
    
    //asciidoctor 실행전 기존에 생성된 API 문서 삭제
    delete file('src/main/resources/static/docs')
}


asciidoctor.doLast {
    println "=====finish asciidoctor"
}


task copyDocument(type: Copy) {
    // gradle build 시 asciidoctor → bootJar 순으로 수행
    dependsOn asciidoctor
    
    // gradle build 시 ./build/asciidoc/html5/ 에 html 파일이 생김
    from file("build/asciidoc/html5")
    
    // resources/static/docs 로 복사
    // 서버가 켜져있다면, docs/index.html 로 접속 하여 볼 수 있음
    into file("src/main/resources/static/docs")
}

build {
    dependsOn copyDocument
}
 

이제 세팅은 끝났으니 코드를 작성해보자.

 

1.  Test Code 작성

@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@AutoConfigureRestDocs                      // REST Docs
class TempUserRestControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Test
    @DisplayName("Restdocs 적용 (1) - 전체 유저 조회")
    public void readAll() throws Exception {

        mockMvc.perform(get("/readAll")                            // 요청 - URL
                    .contentType(MediaType.APPLICATION_JSON_VALUE) // 요청 - 콘텐츠 타입
                    .accept(MediaTypes.HAL_JSON_VALUE)             // 요청 - 미디어 타입
        )
                .andDo(print())
                .andExpect(status().isOk())
                .andDo(document("docReadAll"));                    // 문서 이름
    }
}

위의 코드는 mockMVC 을 이용해 Get 방식으로 "readAll" 요청을 보냈을 경우, 응답으로 200 정상 상태를 기대하며, "docReadAll"이라는 네임의 restdocs 문서를 만들라는 테스트 내용이다.

 

이제 테스트가 성공할 수 있도록 Enttiy / Controller / Service / Repository를 만들어보자.

 

2-1.  Entity

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TempUser implements Serializable {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column private Long id;
    @Column private String username;
    @Column private String password;
}

 

객체의 일관성을 유지하기 위해 Entity 값의 변경을 막는 Setter 는 주로 쓰지 않는다. 하지만 예제이니... Setter를 사용하겠다.

 

2-2.  Controller

@RestController
public class TempUserRestController {

    @Autowired
    TempUserService tempUserService;

    /** 유저 조회 **/
    @GetMapping(value = "/readAll")
    public ResponseEntity readAll() {
        List<TempUser> all = tempUserService.findAll();
        return ResponseEntity.ok().body(all);
    }
}

 

2-3.  Service

@Service
public class TempUserService {

    @Autowired
    private TempUserRepository tempUserRepository;

    /** 회원 조회 **/
    public List<TempUser> findAll() {
        return tempUserRepository.findAll();
    }
}

 

 

2-4.  Repository

@Repository
public interface TempUserRepository extends JpaRepository<TempUser, Long> {

}

3.  Test 실행

테스트 코드의 성공을 위해 해당 관련 코드를 다 작성 하였다. 이제 실행해보자.

 

테스트 코드, 성공

 

 

그리고 테스트 코드가 성공하면, 프로젝트 파일의 build 디렉토리 밑 generated-snippets 디렉토리를 확인해보면, 자신이 지정 한 문서 네이밍 (=1번 Test Code 작성 시 지정함) "docReadAll"이라는 디렉토리가 생성되었고, 그 밑에 기본적이 6개의 .adoc 파일이 생겼다.

기본적으로 총 6개의 스니펫이 생성 됨.

 

이제 이 6개의 스니펫들을 가지고 RestDocs 문서화를 시작해보자.

 

 

4.  Asciidoc 파일 생성

Restdocs 를 적용하기 위해 Asciidoc 문서를 이용하겠다. Asciidoc문서는 MD(markdown)와 비슷한 문서 작성을 위한 경량 마크업 언어입니다. MD에 비해 아직 많이 사용되고 있지는 않지만, Rest Docs 문서 등을 만들 때 Asciidoc를많이 이용하여 작성합니다. MD에서 제공하는것과 유사한 문서 작성을 위한 문법뿐만 아니라 다른 adoc문서를 연결하는 link기능 등 확장된 기능을 제공하고 있다.

 

 

4-1.  Asciidoc 파일 경로

.doc 확장자의 Asciidoc 문서 위치는 프로젝트/src/docs/asciidoc 에 생성해줍니다.

 

 

 

4-2.  Asciidoc 문서 작성

Asciidoc 문서는 마크업 언어와 비슷하게 작성하면 됩니다.

자세한 사용법은 잘 정리된 타 블로그를 참고 하시면 될 듯합니다.

 

그리고 해당 전문의 스니펫을 가져오기 위해서 아래와 같이 "include"를 사용하여 작성하면 됩니다.

include::{snippets}/docReadAll/http-request.adoc[]

 

== 유저 전체 조회

=== Request
Request HTTP Example:
include::{snippets}/docReadAll/http-request.adoc[]

Request Body:
include::{snippets}/docReadAll/request-body.adoc[]

Respon Body:
include::{snippets}/docReadAll/response-body.adoc[]

=== Response
Response HTTP Example:
include::{snippets}/docReadAll/http-response.adoc[]

 

참고로 문서를 작성하며 실시간으로 문서의 내용이 어떻게 작성되는지 보고 싶을 것입니다.

IDE Intellij를 사용 중이시라면 Asciidoctor 플러그인을 설치하여 보시면 편합니다.

Asciidoctor 플러그인

 

5.  Build

이제 모든 준비가 끝이 났습니다. gradle을 build 함으로써 우리가 작성한 Asciidoc 문서가 html 문서로 변환되어 우리 눈에 어떻게 보이는지 확인해봅시다.

// 윈도우 기준
gradlew build

 

빌드가 성공적이라면 (=Test 모두 성공) 프로젝트 파일의 build/asciidoc/html5 경로에 우리가 만든 html 문서를 볼 수 있다.

 

5.  Run

이제 서버를 기동 해보자.

 

localhost:8080/docs/index.html에 접속하여 RestDocs 가 적용된 모습을 보자.

 

고생하셨습니다.