Spring

Spring REST Docs 구축 예시

Beekei 2021. 9. 10. 16:59
반응형

개발 환경

  • Java(11)
  • Amazon Corretto JDK(11)
  • Spring Boot(2.5.3)
  • jvm.convert(3.3.2)
  • spring-restdocs-asciidoctor
  • spring-restdocs-mockmvc

Config

1. Dependency 추가 및 설정

의존성 주입해줄 라이브러리와 플러그인을 추가하고 문서를 생성할 폴더 구조를 설정

plugins {
	...
	// asciidoc파일을 변환해주고, Build폴더에 복사해주는 플러그인
	id "org.asciidoctor.jvm.convert" version "3.3.2"
	...
}

...

// ============== BEGIN Spring REST Docs ==============
ext {
    snippetsDir = file('build/generated-snippets')
    docsDir = file('src/docs/asciidoc')
}
asciidoctor {
    configurations 'asciidoctorExtensions'
    inputs.dir snippetsDir
    dependsOn test
    attributes 'docsDir': docsDir
}
bootJar {
    dependsOn asciidoctor
    copy {
        from "${asciidoctor.outputDir}"
        into 'src/main/resources/static/docs'
    }
}
test {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

configurations {
    asciidoctorExtensions
    compileOnly {
        extendsFrom annotationProcessor
    }
}
// ============== END Spring REST Docs ==============

...

dependencies {
	...
	// spring rest docs dependency 추가
   asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
   testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
	...
}

 

공통적으로 사용할 클래스, 인터페이스 생성

1. ApiDocsUtil

API문서를 생성할 때 공통적으로 적용될 Util 클래스 생성

public interface ApiDocumentUtil {

    static OperationRequestPreprocessor getDocumentRequest() {
        // Request Spec을 정렬해서 출력해줌
				return preprocessRequest(prettyPrint());
    }

    static OperationResponsePreprocessor getDocumentResponse() {
        // Response Spec을 정렬해서 출력해줌
				return preprocessResponse(prettyPrint());
    }

}

2. ApiDocsFormatGenerator

API문서를 생성할 때 Resquet, Response Spec에 대한 format을 지정해줄 인터페이스 생성

public interface ApiDocsFormatGenerator {

    static Attributes.Attribute boardPostType() {
        return key("format").value(BoardPostType.NOTICE + ":" + BoardPostType.NOTICE.getDescription());
    }

}

3. ControllerTest

Controller Test 시 공통적으로 Mock처리를 해줄 클래스 생성

모든 TestController에서 상속받는다.

@AutoConfigureRestDocs // REST Doc 자동설정
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) // 문서 스니펫 생성을 위한 클래스
public class ControllerTest {
    /**
     * Spring Security + JWT을 위한 Mock
     */
    @MockBean
    private JwtProvider jwtProvider;

    @MockBean
    private PasswordEncoder passwordEncoder;

    @MockBean
    private UserDetailsService userDetailsService;

    @MockBean
    private AuthenticationEntryPoint authenticationEntryPoint;

    @MockBean
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @MockBean
    private AuthenticationFailureHandler authenticationFailureHandler;

    @MockBean
    private AccessDeniedHandler accessDeniedHandler;
		
		// 상속받은 Controller에서 사용할 MockMvc 의존성 주입
		@Autowired
	  public MockMvc mockMvc;
		
		// 상속받은 Controller에서 사용할 ObjectMapper 의존성 주입
		@Autowired
		public ObjectMapper objectMapper;

}

Test Case 진행

1. Post Test Case

...
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
...

@WebMvcTest(SpringDoscExe.class) // Mocking된 컴포넌트를 사용하기 위한 환경을 설정
public class SpringDoscExeTest extends ControllerTest {
	
	@Mockbean
	private BoardService boardService;

	@Test
	@DisplayName("게시글 등록")
	// Spring Security 인증과 권한 처리 -> 로그인 한 상태
	@WithMockUser(username= "securityUsername", password="securityPassword", roles="USER")
	public void postInsert() throws Exception {
		// given
    doNothing().when(boardService).postInsert(any(PostInsertDTO.class));

    // when
    PostInsertDTO request = PostInsertDTO.builder()
													.type(BoardPostType.NOTICE)
													.subject("게시글 제목입니다.")
													.contents("게시글 내용입니다.")
													.build(); // Request DTO
    ResultActions result = mockMvc.perform(
            post("/board/post") // API호출 url
                    .content(objectMapper.writeValueAsString(request)) // Request When
                    .contentType(MediaType.APPLICATION_JSON) 
                    .accept(MediaType.APPLICATION_JSON)
    );

    // then
    result.andExpect(status().isOk())
            .andDo(document("board/post-insert", // adoc파일을 생성할 폴더 및 파일명
                    getDocumentRequest(), // ApiDocumentUtils
                    getDocumentResponse(), // ApiDocumentUtils
                    requestFields( // Request Field 설정
														// ApiDocsFormatGenerator에 설정한 Format을 attributes으로 적용
                            fieldWithPath("type").type(JsonFieldType.STRING).attributes(boardPostType()).description("게시판 유형"),	                     
														fieldWithPath("subject").type(JsonFieldType.STRING).description("게시글 제목"),
														// optional() 적용 시 null 허용
                            fieldWithPath("contents").type(JsonFieldType.STRING).optional().description("게시글 내용")
                    ),
                    responseFields( // Response Field 설정
                            fieldWithPath("code").type(JsonFieldType.NUMBER).description("결과코드"),
                            fieldWithPath("msg").type(JsonFieldType.STRING).description("결과메시지")
                    )
            ));
	}
}

2. Get Test Case

...
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
...

@WebMvcTest(SpringDoscExe.class) // Mocking된 컴포넌트를 사용하기 위한 환경을 설정
public class SpringDoscExeTest extends ControllerTest {
	
	@Mockbean
	private BoardService boardService;

	@Test
	@DisplayName("게시글 조회")
	// Spring Security 인증과 권한 처리 -> 로그인 한 상태
	@WithMockUser(username= "securityUsername", password="securityPassword", roles="USER")
	public void postInquiry() throws Exception {
		// given
		long postId = 29;
		BoardPost post = BoardPost.builder()
											.id(postId)
											.type(BoardPostType.NOTICE)
											.subject("게시글 제목입니다.")
											.contents("게시글 내용입니다.")
											.build();
		given(boardService.postInquiry(any(long.class))).willReturn(post);

    // when
    ResultActions result = mockMvc.perform(
            get("/board/post/{id}", id) // API호출 url
                    .contentType(MediaType.APPLICATION_JSON) 
                    .accept(MediaType.APPLICATION_JSON)
    );

    // then
    result.andExpect(status().isOk())
            .andDo(document("board/post-inquiry", // adoc파일을 생성할 폴더 및 파일명
                    getDocumentRequest(), // ApiDocumentUtils
                    getDocumentResponse(), // ApiDocumentUtils
										pathParameters( // Path Parameter 설정
		                        parameterWithName("id").description("게시글 고유번호")
                    )
                    responseFields( // Response Field 설정
                            fieldWithPath("code").type(JsonFieldType.NUMBER).description("결과코드"),
                            fieldWithPath("msg").type(JsonFieldType.STRING).description("결과메시지"),
														fieldWithPath("result").type(JsonFieldType.OBJECT).description("게시글"),
														fieldWithPath("result.id").type(JsonFieldType.NUMBER).description("고유번호"),
														// ApiDocsFormatGenerator에 설정한 Format을 attributes으로 적용
														fieldWithPath("result.type").type(JsonFieldType.STRING).attributes(boardPostType()).description("유형"),
														fieldWithPath("subject").type(JsonFieldType.STRING).description("제목"),
														fieldWithPath("contents").type(JsonFieldType.STRING).description("내용")
                    )
            ));
	}
}

adoc 파일 커스텀

Test Case 진행 후 생성되는 산출물(adoc파일)의 형식을 Custom할 수 있다.

src/test/resources/org/springframework/restdocs/templates/asciidoctor 하위에 커스텀 할 산출물의 파일명과 똑같은 snippet 파일 생성

1. request-fields.snippet

request-fields.adoc의 형식를 Custom

=== Request Fields

|===
|Path|Type|Required|Description|format

{{#fields}}
    |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
    |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
    |{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
    |{{#tableCellContent}}{{description}}{{/tableCellContent}}
	|{{#tableCellContent}}{{#format}}{{/tableCellContent}}
{{/fields}}
|===

src/test/resources/org/springframework/restdocs/templates/asciidoctor/request-fields.snippet

2. response-fields.snippet

response-fields의 형식을 Custom

=== Response Fields

|===
|Path|Type|Nullable|Description

{{#fields}}
    |{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
    |{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
    |{{#tableCellContent}}{{optional}}{{/tableCellContent}}
    |{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}
|===

src/test/resources/org/springframework/restdocs/templates/asciidoctor/response-fields.snippet2

더보기
External Libraries중 org.springframework.restdocs:spring-restdocs-code 라이브버리 하위 org/springframework/restdocs/templates/asciidoctor에서 default snippet 확인 가능

API 문서 작성

src/docs/asciidoc 하위에 adoc파일을 include해 API 문서를 작성한다.

1. include 파일 생성

API 문서에 공통적으로 들어갈 include 파일 생성

:doctype: book
:icons: font
:source-highlighter: highlightjs
:toclevels: 2
:sectlinks:

src/docs/asciidoc/include.adoc

2. 게시판 관련 API 문서

= 게시판 관련 API
:toc:

// include.adoc을 include, docsDir은 build.gradle에서 설정
include::{docsDir}/include.adoc[] 

[[board-insert]]
== 게시글 작성
=== Method & Path
include::{snippets}/board/post-insert/http-request.adoc[]
include::{snippets}/board/post-insert/request-fields.adoc[]
include::{snippets}/board/post-insert/response-fields.adoc[]
=== Example
include::{snippets}/board/post-insert/curl-request.adoc[]
include::{snippets}/board/post-insert/response-body.adoc[]

[[board-insert]]
== 게시글 조회
=== Method & Path
include::{snippets}/board/post-inquiry/path-parameters.adoc[]
include::{snippets}/board/post-inquiry/http-request.adoc[]
include::{snippets}/board/post-inquiry/response-fields.adoc[]
=== Example
include::{snippets}/board/post-inquiry/curl-request.adoc[]
include::{snippets}/board/post-inquiry/response-body.adoc[]

빌드

  1. 터미널에서 ./gradlew clean build test
  2. BUILD SUCCESSFUL시 src/main/resources/static/docs 하위에 생성된 API문서(.html) 확인
  3. localhost:8080/docs/(생성한 API 문서 명).html 접속 후 확인
반응형