3 분 소요

Spring Rest Docs는 우아한 형제들 기술블로그를 보고 적용해보고 싶은 마음이 들었다.

적용하기 위해서 아래 글들을 참고했다.


0. 전체 흐름

  1. Member CRUD 코드 작성
  2. test 코드 작성
  3. build.gradle 내용 추가
  4. snippet 파일 생성
  5. adoc 파일생성 및 snippet 파일 import
  6. html 파일로 변환
  7. 최종화면


※ 전체 코드는 git 참고


1. Member CRUD 코드 작성

spring rest docs 을 적용하기 위한 코드가 필요하다. 간단하게 member에 대한 CRUD 를 작성하였다.
스크린샷 2023-02-26 오후 11 51 45

참고
테스트 환경에서는 H2를 데이터베이스로 사용한다.
(개발 및 배포 환경에서는 docker compose를 통해 mysql을 사용한다.)

2. test 코드 작성

UserControllerTest.java

@ActiveProfiles("test")
@AutoConfigureRestDocs // Spring REST Docs 자동 설정 어노테이션 (테스트 수행 시, 자동으로 sinppets 파일을 생성)
@AutoConfigureMockMvc
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class UserControllerTest {
    /*
     *  1. member 조회
     *  2. member 추가
     *  3. member 수정
     *  4. member 삭제
     *  5. member 전체 조회
     * */

    @Autowired
    private MockMvc mockMvc;
    @Autowired
    private MemberService memberService;

    @BeforeAll
    void init() {
        CreateRequest createRequest = new CreateRequest();
        createRequest.setName("john");
        createRequest.setYear(2);
        createRequest.setGrade("junior");

        memberService.createMember(createRequest);
    }


    @Order(1)
    @Test
    void getMemberTest() throws Exception {
        mockMvc.perform(get("/member/{name}", "john"))
                .andDo(print())
                .andDo(document("getMember"))
                .andExpect(jsonPath("$.name", is("john")));
    }

    @Order(2)
    @Test
    void createMemberTest() throws Exception {
        mockMvc.perform(post("/member")
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .content("{\n" +
                                "    \"name\":\"jessi\",\n" +
                                "    \"grade\":\"junior\",\n" +
                                "    \"year\":2\n" +
                                "}"))
                .andDo(print())
                .andDo(document("createMember"))
                .andExpect(status().isOk());
    }

    @Order(3)
    @Test
    void updateMemberTest() throws Exception {
        UpdateRequest request = new UpdateRequest();
        request.setGrade("senior");
        request.setYear(7);

        mockMvc.perform(put("/member/{name}", "john")
                        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andDo(print())
                .andDo(document("updateMember"))
                .andExpect(jsonPath("$.year", is(7)))
                .andExpect(jsonPath("$.grade", is("senior")));
    }

    @Order(5)
    @Test
    void deleteMemberTest() throws Exception {
        mockMvc.perform(delete("/member/{name}", "john"))
                .andDo(print())
                .andDo(document("deleteMember"))
                .andExpect(jsonPath("$.name", is("john")))
                .andExpect(status().isOk());
    }

    @Order(4)
    @Test
    void getAllMemberTest() throws Exception {
        mockMvc.perform(get("/members"))
                .andDo(print())
                .andDo(document("getAllMember"))
                .andExpect(status().isOk());
    }
}

3. build.gradle 내용 추가

✅ 부분을 잘 확인하자!

plugins {
	id 'java'
	id 'org.springframework.boot' version '2.7.8'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id 'org.asciidoctor.jvm.convert' version '3.3.2' // ✅ asciidoctor plugin 추가
}

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

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

ext{
	snippetDir = file('build/generated-snippets') // ✅ snippets 파일을 저장할 디렉토리 생성
}

test {
	useJUnitPlatform()
	outputs.dir snippetDir // ✅ 생성된 파일을 미리 생성한 디렉토리(build/generated-snippets)에 저장
}

asciidoctor{ // asciidoctor 설정 task
	inputs.dir snippetDir // ✅ snippets 디렉토리를 입력으로 함
	dependsOn test // ✅ test task를 의존하도록 하여, 문서 생성 전에 test를 수행하도록 함
}

bootJar{ // springboot를 이용한 jar 파일 생성 시 필요한 설정 task
	dependsOn asciidoctor // ✅ asciidoctor 를 의존하도록 하여, bootJar 생성 전에 asciidoctor task를 수행하도록 함
	// (jar 파일 생성 시, 문서 생성을 보장 함)
	from("src/docs/asciidoc"){ // ✅ 문서 생성 시, Jar 파일 내 static/docs 에도 복사되도록 함 // 🌟 경로 조심! 
		into 'BOOT-INF/classes/static/docs'
	}

	// ✅ index 페이지를 노출하기 위해 설정
	copy {
		from asciidoctor.outputDir
		into "src/main/resources/static/docs"
	}
}

dependencies {
	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.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// spring rest docs 을 이용하기 위한 라이브러리
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // ✅

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

	// H2
	implementation 'com.h2database:h2:1.4.199' // ✅ 테스트를 위한 데이터베이스로 사용
}

4. snippet 파일 생성

테스트 코드 빌드 후, 테스트가 통과되었다면 snippet 파일이 자동으로 생성될 것이다.
테스트 코드 안에서 실행 버튼을 클릭하고 확인해보자.
스크린샷 2023-02-27 오전 12 01 35

5. adoc 파일생성 및 snippet 파일 import

Rest API 문서의 메인 파일을 생성한다.
src/docs/asciidoc 디렉토리 생성 후, index.adoc 생성
스크린샷 2023-02-27 오전 12 12 11

src/docs/asciidoc/index.adoc

= REST Docs
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:

ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]


[[Member_API]]
== Member API


[[Member_생성]]
=== Member 생성
include::{snippets}/createMember/http-request.adoc[]
include::{snippets}/createMember/http-response.adoc[]

[[Member_조회]]
=== Member 조회
include::{snippets}/getMember/http-request.adoc[]
include::{snippets}/getMember/http-response.adoc[]


[[Member_수정]]
=== Member 수정
include::{snippets}/updateMember/http-request.adoc[]
include::{snippets}/updateMember/http-response.adoc[]

[[Member_삭제]]
=== Member 삭제
include::{snippets}/deleteMember/http-request.adoc[]
include::{snippets}/deleteMember/http-response.adoc[]


[[Member_전체_조회]]
=== Member 전체 조회
include::{snippets}/getAllMember/http-request.adoc[]
include::{snippets}/getAllMember/http-response.adoc[]
  • include::{snippets} 를 이용하여 생성한 snippets 파일을 추가하였다.

gradle bootJar 실행

$ ./gradlew clean
$ ./gradlew build jar

jar 파일 내부에 생성된 index.html 확인

bootJar 실행 후 자동으로 생성된다.

# jar 파일 내부 리스트 보기
$ jar -tf ./build/libs/jenkins-test-0.0.1-SNAPSHOT.jar

# jar 파일 내부 index.adoc 검색
$ jar -tf ./build/libs/jenkins-test-0.0.1-SNAPSHOT.jar | grep index.adoc

스크린샷 2023-02-27 오전 12 33 55

6. html 파일로 변환

bootJar 파일에 index.html 문서를 추가하기 위한 build.gradle 설정을 한다.
(아래 부분인데, 이전에 미리 작성 해두었으니 넘어가자!)

plugins {
   ...
   
   id 'org.asciidoctor.jvm.convert' version '3.3.2' //asciidoctor plugin 추가
}

... 

asciidoctor{ //asciidoctor 설정 task
   inputs.dir snippetDir //snippets 디렉토리를 입력으로 함
   dependsOn test // test task를 의존하도록 하여, 문서 생성 전에 test를 수행하도록 함
}

bootJar{ //springboot를 이용한 jar 파일 생성 시 필요한 설정 task
   dependsOn asciidoctor //asciidoctor 를 의존하도록 하여, bootJar 생성 전에 asciidoctor task를 수행하도록 함
                     // (jar 파일 생성 시, 문서 생성을 보장 함)
   from("src/docs/asciidoc"){ //문서 생성 시, Jar 파일 내 static/docs 에도 복사되도록 함
      into 'BOOT-INF/classes/static/docs'
   }
}
...

7. 최종 화면

bootJar 실행후, localhost:8080/docs/index.html 에 들어갔을 때 화면이다.
스크린샷 2023-02-27 오전 12 07 19

모든 설정이 잘 되었는지 확인

모든 설정이 잘 되었다면 아래 파일들이 자동으로 생성되어 있어야 한다.
스크린샷 2023-02-27 오전 12 10 59

스크린샷 2023-02-27 오전 12 13 10



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기

태그:

카테고리:

업데이트: