๊ด€๋ฆฌ ๋ฉ”๋‰ด

๋‚˜์˜ ๋ชจ์–‘

057 | API Documentation, Swagger, SpringRest ๋ณธ๋ฌธ

SEB/TIL

057 | API Documentation, Swagger, SpringRest

kexon 2022. 9. 14. 22:56

๐ŸŽˆ API Documentation

ํด๋ผ์ด์–ธํŠธ๋Š” HTTP request URL(๋˜๋Š” URI)์„ ํ†ตํ•ด ์„œ๋ฒ„์— ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•œ๋‹ค. ์ด ๋•Œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ REST API ๋ฐฑ์—”๋“œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์š”์ฒญ์„ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด์„œ ์•Œ์•„์•ผ ๋˜๋Š” ์š”์ฒญ ์ •๋ณด(์š”์ฒญ URL(๋˜๋Š” URI), request body, query parameter ๋“ฑ)๋ฅผ ๋ฌธ์„œํ™” ํ•œ ๊ฒƒ์„ API ๋ฌธ์„œ ๋˜๋Š” API ์ŠคํŽ™(์‚ฌ์–‘)์ด๋ผ๊ณ  ํ•œ๋‹ค.

API ๋ฌธ์„œ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ˆ˜๊ธฐ๋กœ ์ž‘์„ฑํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ๊ฐœ๋ฐœ์ค‘์ด๊ฑฐ๋‚˜ ์œ ์ง€๋ณด์ˆ˜๋ฅผ ํ•  ๋•Œ API๊ฐ€ ์ˆ˜์ •๋  ์ˆ˜๋„ ์žˆ๊ณ , ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ œ๊ณต๋œ API ์ •๋ณด์™€ ์ˆ˜๊ธฐ๋กœ ์ž‘์„ฑ๋œ API ๋ฌธ์„œ ์ •๋ณด๊ฐ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋น„ํšจ์œจ์ ์ด๋‹ค. API ๋ฌธ์„œ ์ž๋™ํ™”๋ฅผ ํ†ตํ•ด API์—์„œ ์ƒ๊ธฐ๋Š” ์—๋Ÿฌ ๋ฐœ์ƒ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์ž‘์—… ์‹œ๊ฐ„์„ ๋‹จ์ถ•ํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๐Ÿงฉ Swagger์˜ API ๋ฌธ์„œํ™” ๋ฐฉ์‹ - ์• ๋„ˆํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜

    • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ์— ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ ์• ๋„ˆํ…Œ์ด์…˜๋“ค์ด ํฌํ•จ๋œ๋‹ค.
    • ๊ฐ€๋…์„ฑ ๋ฐ ์œ ์ง€ ๋ณด์ˆ˜์„ฑ์ด ๋–จ์–ด์ง„๋‹ค.
    • API ๋ฌธ์„œ์™€ API ์ฝ”๋“œ ๊ฐ„์˜ ์ •๋ณด ๋ถˆ์ผ์น˜ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.
    • API ํˆด๋กœ์จ์˜ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿงฉ Spring Rest Docs์˜ API ๋ฌธ์„œํ™” ๋ฐฉ์‹ - ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๊ธฐ๋ฐ˜

    • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ์— ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•œ ์ •๋ณด๋“ค์ด ํฌํ•จ๋˜์ง€ ์•Š๋Š”๋‹ค.
    • ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค์˜ ์‹คํ–‰์ด “passed”์—ฌ์•ผ API ๋ฌธ์„œ๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.
    • ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๋ฐ˜๋“œ์‹œ ์ž‘์„ฑํ•ด์•ผ๋œ๋‹ค.
    • API ํˆด๋กœ์จ์˜ ๊ธฐ๋Šฅ์€ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š”๋‹ค.

๐ŸŽˆ Swagger

  • build.gradle > ์˜์กด์„ฑ ์ถ”๊ฐ€
dependencies{
	implementation 'io.springfox:springfox-boot-starter:3.0.0'
}
  • NPE ๋ฐœ์ƒ > application.yml > ์†์„ฑ ์ถ”๊ฐ€
spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  • ๋‹จ์ : @Api~์• ๋„ˆํ…Œ์ด์…˜์„ ์ปจํŠธ๋กค๋Ÿฌ์™€ dtoํด๋ž˜์Šค์—๋„ ๊ณ„์† ์ถ”๊ฐ€ํ•ด์•ผํ•จ → ์ฝ”๋“œ ๋ณต์žก
  • ์„œ๋ฒ„ ์‹คํ–‰ ํ›„ ์•„๋ž˜ url๋กœ ์ ‘์†ํ•˜๋ฉด swagger๋กœ ๋งŒ๋“ค์–ด์ง„ API ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๐ŸŽˆ Spring Rest Docs

Swagger๋Š” ์‹ค์ œ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๋กœ์ง์— ์• ๋„ˆํ…Œ์ด์…˜์ด ์ถ”๊ฐ€๋˜์–ด ๊ฐ€๋…์„ฑ๋„ ๋–จ์–ด์ง€๊ณ  ๋ถˆํ•„์š”ํ•˜๊ฒŒ ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์ง€๋Š” ๋ฌธ์ œ์ ์ด ์žˆ์—ˆ๋Š”๋ฐ, Spring Rest Docs๋Š” ์ฝ”๋“œ๊ฐ€ ์‹ค์ œ ๊ธฐ๋Šฅ ๋กœ์ง์ด ์•„๋‹Œ test์— ์ถ”๊ฐ€ํ•œ๋‹ค. ๊ธฐ๋Šฅ ๋กœ์ง์— ์—๋„ˆํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•  ํ•„์š”๋„ ์—†๊ณ , API๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ…Œ์ŠคํŠธ๋„ ํ†ต๊ณผํ•ด์•ผ ๋งŒ๋“ค์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ์˜ค๋ฅ˜ ๋ฐœ์ƒ ํ™•๋ฅ ์ด ์ค„์–ด๋“ ๋‹ค.

๐Ÿงฉ Spring Rest Docs ํ๋ฆ„

  1. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
    1-1. ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
    1-2. API ์ŠคํŽ™ ์ •๋ณด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
  2. test task ์‹คํ–‰
    2-1. ์Šฌ๋ผ์ด์Šค ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์‹คํ–‰ํ•œ๋‹ค.
    2-2. ์‹คํ–‰ ๊ฒฐ๊ณผ passed: ๋‹ค์Œ ์ž‘์—… ์ง„ํ–‰ / failed: ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์ˆ˜์ • ํ›„ passedํ•  ๋•Œ๊นŒ์ง€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.
  3. API ๋ฌธ์„œ ์Šค๋‹ˆํ• ์ƒ์„ฑ
    3-1. ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ์‹คํ–‰๊ฒฐ๊ณผ๊ฐ€ passed์ด๋ฉด ์ฝ”๋“œ์— ํฌํ•จ๋œ API ์ŠคํŽ™ ์ •๋ณด ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ API ๋ฌธ์„œ ์Šค๋‹ˆํ•์ด .adoc ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง„ ํŒŒ์ผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.
  4. ์Šค๋‹ˆํ•์„ ํฌํ•จํ•œ API ๋ฌธ์„œ ์ƒ์„ฑ
  5. .adoc ํŒŒ์ผ์˜ API ๋ฌธ์„œ๋ฅผ HTML๋กœ ๋ณ€ํ™˜
    ์ƒ์„ฑ๋œ API ๋ฌธ์„œ๋ฅผ HTML ํŒŒ์ผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
    HTML๋กœ ๋ณ€ํ™˜๋œ API ๋ฌธ์„œ๋Š” HTML ํŒŒ์ผ ์ž์ฒด๋ฅผ ๊ณต์œ ํ•  ์ˆ˜๋„ ์žˆ๊ณ , URL์„ ํ†ตํ•ด ํ•ด๋‹น HTML์— ์ ‘์†ํ•ด์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿงฉ Spring Rest Docs ์„ค์ •

plugins {
	id 'org.springframework.boot' version '2.7.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'

	// ํ™•์žฅ์ž .adoc๋ฅผ ๊ฐ€์ง€๋Š” AsciiDoc ๋ฌธ์„œ๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š” Asciidoctor ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€
	id "org.asciidoctor.jvm.convert" version "3.3.2"

	id 'java'
}

// ext ๋ณ€์ˆ˜์˜ set() ๋ฉ”์„œ๋“œ๋กœ API ๋ฌธ์„œ ์Šค๋‹ˆํ• ์ƒ์„ฑ ๊ฒฝ๋กœ ์ง€์ •
ext {
	set('snippetsDir', file("build/generated-snippets"))
}

// AsciiDoctor์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์˜์กด ๊ทธ๋ฃน ์ง€์ •
configurations {
	asciidoctorExtensions
}

dependencies {
	// spring-restdocs-core / spring-restdocs-mockmvc ์˜์กด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

	// spring-restdocs-asciidoctor ์˜์กด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ถ”๊ฐ€
	asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}

// :test task ์‹คํ–‰ ์‹œ API ๋ฌธ์„œ ์ƒ์„ฑ ์Šค๋‹ˆํ• ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ ์„ค์ •
tasks.named('test') {
	outputs.dir snippetsDir
	useJUnitPlatform()
}

// :asciidoctor task ์‹คํ–‰ ์‹œ asciidoctorExtensions ์„ค์ •
tasks.named('asciidoctor') {
	configurations "asciidoctorExtensions"
	inputs.dir snippetsDir
	dependsOn test
}

// :build task ์‹คํ–‰ ์ „์— ์‹คํ–‰๋˜๋Š” task
task copyDocument(type: Copy) {
	dependsOn asciidoctor
	from file("build/docs/asciidoc/")
	into file("src/main/resources/static/docs")
}

build {
	// :copyDocument task ๋จผ์ € ์‹คํ–‰
	dependsOn copyDocument
}

// :bootJar task๋Š” ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ํŒŒ์ผ์„ ์ƒ์„ฑ
bootJar {
	enabled = true
	dependsOn copyDocument // :bootJar task์‹คํ–‰ ์ „์— :copyDocument task๊ฐ€ ์‹คํ–‰
	from ("${asciidoctor.outputDir}/html5") {
		into 'static/docs'
	}
}

๐Ÿงฉ Spring Rest Docs ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค

@EnableJpaAuditing
@SpringBootApplication
public class Application {
		public static void main(String[] args) {
				SpringApplication.run(Application.class, args);
		}
}

@EnableJpaAuditing์„ Application ํด๋ž˜์Šค์— ์ถ”๊ฐ€ํ•˜๊ฒŒ ๋˜๋ฉด JPA ๊ด€๋ จ Bean์„ ํ•„์š”๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— @WebMvcTest ์• ๋„ˆํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ ํ•  ๋• @MockBean(JpaMetamodelMappingContext.class)๋ฅผ Mock ๊ฐ์ฒด๋กœ ์ฃผ์ž…ํ•ด์•ผ ํ•œ๋‹ค.

 

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

    @MockBean
    private Service service;

    @MockBean
    private Mapper mapper;

    @Autowired
    private Gson gson;

    @Test
    public void rest() throws Exception {
        // given
        Dto dto = new Dto("cheese@cat.com", "cheese", "010-1212-1212");
        String content = gson.toJson(dto);

        Response response = new Response(
                        1L,
                        "cheese@cat.com",
                        "cheese",
                        "010-1212-1212",
                        new Stamp());

        given(mapper.Mocking1(Mockito.any(Dto.class))).willReturn(new Entity());
        given(service.Mocking2(Mockito.any(Entity.class))).willReturn(new Entity());
        given(mapper.Mocking3(Mockito.any(Entity.class))).willReturn(response);

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

        // then
        actions
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.data.field").value(post.getField()))
                .andDo(document(
                        "identifier",
                        getRequestPreProcessor(),
                        ApiDocumentUtils.getResponsePreProcessor(),
                        requestFields(
                                List.of(
                                        fieldWithPath("field1").type(JsonFieldType.STRING).description("field1"),
                                        fieldWithPath("field2").type(JsonFieldType.STRING).description("field2")
                                )
                        ),
                        responseFields(
                                List.of(
                                        fieldWithPath("field1").type(JsonFieldType.OBJECT).description("field1"),
                                        fieldWithPath("field2").type(JsonFieldType.NUMBER).description("field2")
                                )
                        )
                ));
    }
}
public interface ApiDocumentUtils {
    static OperationRequestPreprocessor getRequestPreProcessor() {
        return preprocessRequest(prettyPrint());
    }

    static OperationResponsePreprocessor getResponsePreProcessor() {
        return preprocessResponse(prettyPrint());
    }
}

ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผํ•˜๋ฉด build/generated-snippets/์Šค๋‹ˆํ• ์‹๋ณ„์ž ๊ฒฝ๋กœ์— .adoc API ๋ฌธ์„œ๋“ค์ด ์ƒ์„ฑ๋œ๋‹ค.

๐Ÿงฉ ์Šค๋‹ˆํ•์„ ์ด์šฉํ•œ API ๋ฌธ์„œํ™”

๋งŒ๋“  API ๋ฌธ์„œ๋Š” ์Šค๋‹ˆํ•(์กฐ๊ฐ ๋ชจ์Œ)์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด ์กฐ๊ฐ์„ ํ•˜๋‚˜๋กœ ๋ชจ์„ ํ…œํ”Œ๋ฆฟ ๋ฌธ์„œ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

1. gradle ํ”„๋กœ์ ํŠธ์—์„œ ํ…œํ”Œ๋ฆฟ ๋ฌธ์„œ ๊ธฐ๋ณธ ๊ฒฝ๋กœ์ธ src/docs/asciidoc์— index.adoc ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ  ํ…œํ”Œ๋ฆฟ ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

= template documentation name
:sectnums:
:toc: left
:toclevels: 4
:toc-title: Table of Contents
:source-highlighter: prettify

writer <email>

version, date

***
== controller
=== handler method
.curl-request
include::{snippets}/identifier/curl-request.adoc[]

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

.request-fields
include::{snippets}/identifier/request-fields.adoc[]

.http-response
include::{snippets}/identifier/http-response.adoc[]

.response-fields
include::{snippets}/identifier/response-fiedls.adoc[]

2. Gradle > Tasks > build > bootJar ๋˜๋Š” build๋กœ ๋นŒ๋“œํ•œ๋‹ค.

    src/main/resource/static/docs ๋””๋ ‰ํ† ๋ฆฌ์— index.htmlํŒŒ์ผ์ด ์ƒ์„ฑ๋œ๋‹ค.

3. html ํŒŒ์ผ ํ™•์ธ
    http://localhost:8080/docs/index.html

โœ… Ref.

https://swagger.io/docs/specification/about/

 

About Swagger Specification | Documentation | Swagger

What Is OpenAPI? OpenAPI Specification (formerly Swagger Specification) is an API description format for REST APIs. An OpenAPI file allows you to describe your entire API, including: Available endpoints (/users) and operations on each endpoint (GET /users,

swagger.io

https://springdoc.org/

 

OpenAPI 3 Library for spring-boot

Library for OpenAPI 3 with spring boot projects. Is based on swagger-ui, to display the OpenAPI description.Generates automatically the OpenAPI file.

springdoc.org

 

'SEB > TIL' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

061 | ์ธ์ฆ๋ณด์•ˆ ๊ธฐ์ดˆ  (0) 2022.09.20
058 | Asciidocs, Asciidoctor  (2) 2022.09.15
052 | Transaction  (0) 2022.09.05
046 | Spring Data JDBC  (0) 2022.08.26
045 | Checked / Unchecked / Customised Exception  (0) 2022.08.25
Comments