관리 메뉴

bright jazz music

blog13: Spring REST Docs 1 -기본설정 본문

Projects/blog

blog13: Spring REST Docs 1 -기본설정

bright jazz music 2023. 1. 15. 15:12

API를 설명하기 위한 문서 작성 라이브러리는 스웨거를 포함하여 다수이다. 여기서는 Spring REST docs를 사용한다.

https://spring.io/projects/spring-restdocs

 

Spring REST Docs

It helps you to produce documentation that is accurate, concise, and well-structured. This documentation then allows your users to get the information they need with a minimum of fuss.

spring.io

 

Spring REST Doc의 장점

  • 운영 코드에 영향이 없다.

다른 라이브러리는 운영 중인 코드에 어노테이션 또는 xml을 사용해 영향을 가해서 문서를 만들어 내는 반면, Spring REST Docs는 운영코드를 수정하지 않고도 단순히 테스트 케이스만을 사용하여 문서를 만들어낼 수 있다.

 

  • 변경된 기능에 대해 최신문서 유지가 어느 정도 가능하다.

(어떤 라이브러리는 코드가 수정되어도 문서가 수정되지 않는다.) Spring REST Docs는 테스트 케이스를 실행하여 테스트가 성공했을 때 문서를 자동으로 생성해 준다. 즉, 개발자가 기능을 추가 또는 변경한 후, 그에 대한 테스트 케이스를 만들어 줌으로써 스프링 레스트 독스가 자동으로 최신버전을 유지하도록 할 수 있다는 뜻이다.

 

 

-- 설정 시작 --

 

공식 페이지 - Learn - Reference Page

 

1. build.gradle에 설정 추가

 

/*build.gradle*/

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.6'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    //SpringRestDoc
    id "org.asciidoctor.jvm.convert" version "3.3.2"
}

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

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    //SpringRestDoc
    asciidoctorExt

}

repositories {
    mavenCentral()
}

ext { //변수를 선언할 것이므로 파일 상부에 적어주는 것이 좋다.
    //SpringRestDoc
    snippetsDir = file('build/generated-snippets') //생성된 asciidoc 위치 지정
    asciidocVersion = "3.0.0"
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation' /*추가*/

    //gradle 5버전부터 annotationprocessor가 사용 가능해서
    // gradle 레벨에서 컴파일 할 때 필요한 querydsl클래스 파일을 생성할 수 있게 됐다.
    //querydsl 관련 디펜던시
    implementation 'com.querydsl:querydsl-core'
    implementation 'com.querydsl:querydsl-jpa'

    //annotationProcessor : 인식할 타입을 추가함
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'



    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //SpringRestDoc

    asciidoctorExt "org.springframework.restdocs:spring-restdocs-asciidoctor:${asciidocVersion}"
    testImplementation "org.springframework.restdocs:spring-restdocs-mockmvc:${asciidocVersion}"


}



test {
    //SpringRestDoc
    outputs.dir snippetsDir
}

//SpringRestDoc
asciidoctor {
    inputs.dir snippetsDir
    configurations 'asciidoctorExt'
    dependsOn test //테스트를 먼저 수행한다
}

//SpringRestDoc
//나중에 프로젝트를 jar로 빌드할 때 ascciidoc도 포함될 수 있도록 한다.
bootJar {
    dependsOn asciidoctor
    from ("${asciidoctor.outputDir}/html5") {
        into 'static/docs'
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

 

추가해준 뒤 gradle sync 해주는 것을 잊지 말 것.

 

2. Document Snippet 생성하기

테스트를 통해 asciidoc 파일을 생성하자.

 

JUnit5를 사용하는 경우, RestDocumentExtension을 적용하기 위해 클래스를 생성해 줘야 한다.

 

Spring REST Docs를 생성하기위한 테스트를 PostControllerDocTest에서 실행한다.

 

//PostControllerDocTest.java

package com.endofma.blog.controller;

import com.endofma.blog.domain.Post;
import com.endofma.blog.repository.PostRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest //부트의 경우 이걸 사용. 이 안에 SpringExtension이 포함되어 있다.
@ExtendWith(RestDocumentationExtension.class) //부트의 경우
//@ExtendWith({RestDocumentationExtension, SpringExtension.class}) //Spring MVC의 경우는 이렇게
public class PostControllerDocTest {
//테스트 수행 전 셋업
private MockMvc mockMvc;

@Autowired
private PostRepository postRepository;

//JUnit자체적으로 주입하기 때문에 생성자를 통해 주입하면 아래 에러 발생
//org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter
//public PostControllerDocTest(PostRepository postRepository){
//    this.postRepository = postRepository;
//}


    //asciidoc에 맞는 설정을 가지고 mockMvc를 설정하는 코드
    @BeforeEach
    void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }

    @Test
    @DisplayName("글 단건 조회 테스트")
    void test1() throws Exception {
        //given
        Post post = Post.builder()
                .title("제목")
                .content("내용")
                .build();
        postRepository.save(post);
        //expected
//       this.mockMvc.perform(get("/")
       this.mockMvc.perform(get("/posts/{postId}", 1L)
               .accept(MediaType.APPLICATION_JSON))
               .andDo(print())
               .andExpect(status().isOk())
               .andDo(document("index"));
    }
}



/*
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.6)

2023-01-15 16:01:47.084  INFO 40064 --- [    Test worker] c.e.b.controller.PostControllerDocTest   : Starting PostControllerDocTest using Java 11.0.12 on DESKTOP-Q7HBM41 with PID 40064 (started by user in D:\personal\blog)
2023-01-15 16:01:47.086  INFO 40064 --- [    Test worker] c.e.b.controller.PostControllerDocTest   : No active profile set, falling back to 1 default profile: "default"
2023-01-15 16:01:48.174  INFO 40064 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-01-15 16:01:48.255  INFO 40064 --- [    Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 70 ms. Found 1 JPA repository interfaces.
2023-01-15 16:01:48.938  INFO 40064 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-01-15 16:01:49.268  INFO 40064 --- [    Test worker] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-01-15 16:01:49.383  INFO 40064 --- [    Test worker] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-01-15 16:01:49.467  INFO 40064 --- [    Test worker] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 5.6.14.Final
2023-01-15 16:01:49.772  INFO 40064 --- [    Test worker] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-01-15 16:01:49.974  INFO 40064 --- [    Test worker] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-01-15 16:01:50.830  INFO 40064 --- [    Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-01-15 16:01:50.841  INFO 40064 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-01-15 16:01:51.752  WARN 40064 --- [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-01-15 16:01:52.199  INFO 40064 --- [    Test worker] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:blog'
2023-01-15 16:01:52.704  INFO 40064 --- [    Test worker] c.e.b.controller.PostControllerDocTest   : Started PostControllerDocTest in 6.09 seconds (JVM running for 8.91)
2023-01-15 16:01:52.950  INFO 40064 --- [    Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2023-01-15 16:01:52.950  INFO 40064 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2023-01-15 16:01:52.952  INFO 40064 --- [    Test worker] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 2 ms

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /posts/1
       Parameters = {}
          Headers = [Accept:"application/json"]
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = com.endofma.blog.controller.PostController
           Method = com.endofma.blog.controller.PostController#get(Long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"id":1,"title":"제목","content":"내용"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
2023-01-15 16:01:53.364  INFO 40064 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-01-15 16:01:53.364  INFO 40064 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2023-01-15 16:01:53.369  WARN 40064 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 90121, SQLState: 90121
2023-01-15 16:01:53.370 ERROR 40064 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-15 16:01:53.371  WARN 40064 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 90121, SQLState: 90121
2023-01-15 16:01:53.371 ERROR 40064 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper   : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-15 16:01:53.371  WARN 40064 --- [ionShutdownHook] o.s.b.f.support.DisposableBeanAdapter    : Invocation of destroy method failed on bean with name 'entityManagerFactory': org.hibernate.exception.JDBCConnectionException: Unable to release JDBC Connection used for DDL execution
2023-01-15 16:01:53.372  INFO 40064 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2023-01-15 16:01:53.377  INFO 40064 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
BUILD SUCCESSFUL in 12s
4 actionable tasks: 2 executed, 2 up-to-date
PM 4:01:53: Execution finished ':test --tests "com.endofma.blog.controller.PostControllerDocTest.test1"'.

*/

 

테스트에 성공하면 아래와 같은 파일들이 생성된다.

 

build.gradle에서 아래의 경로로 설정해주었던 것을 기억하자.

snippetsDir = file('build/generated-snippets') //생성된 asciidoc 위치 지정

 

해당 경로에 asciidoc파일이 생성된다.
curl을 통해서 테스트 할 때
http를 통해서 테스트 할 때
http 응답
curl과 비슷한 종류인 듯하다.
단건 조회의 경우에는 body를 넘기지 않기 때문에 비어있다.
리스펀스 바디는 이렇게 온다.

 

3. 생성된 스니펫들을 HTML파일로 보여주기

 

우선은 .adoc 파일을 하나 생성해야 한다.

 

// index.adoc
//{snippetes}: /build/generated-snippets

include::{snippets}/index/http-request.adoc[]
include::{snippets}/index/http-response.adoc[]
include::{snippets}/index/curl-request.adoc[]
// include::{snippets}/index/httpie-request.adoc[]
// include::{snippets}/index/request-body.adoc[]
// include::{snippets}/index/response-body.adoc[]

 

html5 파일이 생성될 경로를 만들어준다. 현재는 static/docs가 아니라 다른 데 생성되고 있어서 build.gradle에 println을 통해서 생성되는 경로를 알아보았다.

//SpringRestDoc
//나중에 프로젝트를 jar로 빌드할 때 ascciidoc도 포함될 수 있도록 한다.
bootJar {
    dependsOn asciidoctor
    from ("${asciidoctor.outputDir}") {
        println " >>> " + asciidoctor.outputDir //경로 확인하기 위해 수정해 줬음
        into 'static/docs'
    }
}

//> Configure project :
// >>> D:\personal\blog\build\docs\asciidoc

여기에 생성되고 있었군.

 

이걸 /src/main/resources/docs 디렉토리에 옮겨오기 위해 아래처럼 수정해 주었다.

 

이제 재빌드 한다.

 

*(gradle - build -bootjar만 실행해 주어도 된다.bootjar만 수정했기 때문이다.)

//SpringRestDoc
//나중에 프로젝트를 jar로 빌드할 때 ascciidoc도 포함될 수 있도록 한다.
bootJar {
    dependsOn asciidoctor
    
    copy {
        from asciidoctor.outputDir
        into "src/main/resources/static/docs"
    }
    
//    from ("${asciidoctor.outputDir}/html5") {
//    from ("${asciidoctor.outputDir}") {
//        println " >>> " + asciidoctor.outputDir
//        into 'static/docs'
//    }
}

 

 

 

 

 

 

프로젝트 빌드

> Task :test // 먼저 테스트

...

> Task :asciidoctor	//그 다음 아스키독 생성
> Task :bootJarMainClassName
> Task :bootJar
> Task :assemble
> Task :check
> Task :build

 

/src/resources/static/docs에 생성된 html파일을 확인할 수 있다.

 

4. 생성된 HTML 확인

 

프로젝트를 구동하자.

브라우저로 확인.

 

기본적인 내용이 출력되었다. 너무 심플하기 때문에 asciidoc 문법을 통해 조금

 

이 녀석을 수정.

// index.adoc
//{snippetes}: /build/generated-snippets

= 블로그 API
:toc:

== 글 단건 조회

=== 요청
include::{snippets}/index/http-request.adoc[]

=== 응답
include::{snippets}/index/http-response.adoc[]

=== curl
include::{snippets}/index/curl-request.adoc[]
// include::{snippets}/index/httpie-request.adoc[]
// include::{snippets}/index/request-body.adoc[]
// include::{snippets}/index/response-body.adoc[]

 

1. gradle.build 재빌드 또는 gradle - build - bootjar 실행

2. 서버 재시작

 3. 브라우저에서 localhost:8080/docs/index.html 접근

 

 

 

'Projects > blog' 카테고리의 다른 글

blog13: Spring REST Docs3 - 커스터마이징  (0) 2023.01.29
blog13: Spring REST Docs2 - 요청, 응답필드  (0) 2023.01.16
blog12: 예외처리 4  (0) 2023.01.14
blog12: 예외처리 3  (0) 2023.01.14
blog12: 예외처리2  (0) 2023.01.13
Comments