2 posts tagged with "api"

View All Tags

Spring Rest Docs & OAS - μ‹œμž‘ν•˜κΈ°

κ°œμš”#

μ§€λ‚œ κΈ€ Spring Rest Docs & OAS 의 ν•„μš”μ„± μ—μ„œ 이 λ‘˜μ˜ 쑰합이 μ™œ ν•„μš”ν•œμ§€ μ•Œμ•„λ³΄μ•˜μŠ΅λ‹ˆλ‹€. 이제 κ°„λ‹¨ν•œ μƒ˜ν”Œ μ½”λ“œλ₯Ό 톡해 OAS λ₯Ό μ‚¬μš©ν•΄μ„œ API λ¬Έμ„œλ₯Ό λ§Œλ“€μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

본문에 λ“±μž₯ν•˜λŠ” μ½”λ“œλŠ” κΉƒν—ˆλΈŒμ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

μ‹œμž‘ν•˜κΈ° 전에#

이 νŠœν† λ¦¬μ–Όμ€ restdocs-api-spec 라이브러리 μ‚¬μš©μ— 쀑점을 두고 μžˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ μ•„λž˜ λ‚˜μ—΄λœ 방법듀은 μƒμ„Ένžˆ μ„€λͺ…ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

  • Sprint Rest Docs λ₯Ό μ‚¬μš©ν•˜λŠ” 방법
  • Spring Boot Start Web 으둜 API λ₯Ό λ§Œλ“œλŠ” 방법

restdocs-api-spec ν”„λ‘œμ νŠΈλŠ” μ—¬λŸ¬κ°€μ§€ ꡬ성 μš”μ†Œλ‘œ κ΅¬μ„±λ˜μ–΄ 있으며, mockmvc λ₯Ό μ‚¬μš©ν•œ ν…ŒμŠ€νŠΈμ™€ restassured 을 μ‚¬μš©ν•œ ν…ŒμŠ€νŠΈ λ‘˜ λ‹€ μ§€μ›ν•©λ‹ˆλ‹€. 두 ν…ŒμŠ€νŠΈμ˜ 큰 차이점은 MockMvc λŠ” @WebMvcTest λ₯Ό μ‚¬μš©ν•˜κ³ , Rest Assured λŠ” @SpringBootTest λ₯Ό μ‚¬μš©ν•œλ‹€λŠ” 점인데 μžμ„Έν•œ λ‚΄μš©μ€ MockMvc VS RestAssured 글을 μ°Έκ³ λ˜μ§€λ§Œ, 이 글이 μž‘μ„±λœ μ‹œμ μ€ 2020λ…„ 8μ›”λ‘œ 1년이 μ§€λ‚œ ν˜„μž¬λŠ” io.rest-assured:spring-mock-mvc module 이 μ‘΄μž¬ν•˜μ—¬ RestAssured λ₯Ό μ‚¬μš©ν•˜λ”λΌλ„ 전체 Bean 을 λ‘œλ“œν•  ν•„μš”κ°€ μ—†μ–΄μ‘ŒμŠ΅λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈ κ°€λ…μ„±μ΄λ‚˜, μž‘μ„±μ˜ νŽΈλ¦¬ν•¨μ„ λ³΄μ•˜μ„ λ•Œ RestAssured λ₯Ό μ‚¬μš©ν•˜λŠ”κ²Œ μ’‹μ•„λ³΄μ΄λ‚˜, 이전에 μ‚¬μš© κ²½ν—˜μ΄ μ—†μ–΄μ„œ, 이 κΈ€μ—μ„œλŠ” μ΅μˆ™ν•œ MockMvc λ₯Ό μ‚¬μš©ν•˜μ—¬ μž‘μ„±ν•©λ‹ˆλ‹€. μΆ”ν›„ RestAssured λ₯Ό μ‚¬μš©ν•œ μƒ˜ν”Œλ„ μž‘μ„±ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

λ°”λ‘œ RestAssured 둜 μ‹œλ„ν•΄λ³΄κ³  싢은 뢄듀은 μ•„λž˜ 글듀을 μ°Έκ³ ν•˜λ©΄ 쒋을 것 κ°™μŠ΅λ‹ˆλ‹€.

κ°„λ‹¨ν•œ μƒ˜ν”Œ API κ΅¬ν˜„#

λ¨Όμ € μ‚¬μš©μž 정보λ₯Ό μ‘°νšŒν•˜λŠ” κ°„λ‹¨ν•œ API λ₯Ό λ§Œλ“€μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€. κ·Έλž˜λ“€ 섀정을 ν•΄μ£Όκ³ ...

plugins {
id 'org.springframework.boot' version '2.5.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'com.epages.restdocs-api-spec' version '0.11.5' // (1)
}
group = 'net.dezang'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // (2)
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.11.5' // (2) For openapi spec
}
openapi3 { // (3)
server = 'http://localhost:8080'
title = 'USER-API'
description = 'μ‚¬μš©μž λ¦¬μ†ŒμŠ€ API μž…λ‹ˆλ‹€.'
version = '0.1.0'
format = 'yaml'
}
test {
useJUnitPlatform()
}
  • (1) restdocs-api-spec ν”ŒλŸ¬κ·ΈμΈ μΆ”κ°€
  • (2) spring-restdocs-mockmvc , restdocs-api-spec-mockmvc μ˜μ‘΄μ„± μΆ”κ°€
  • (3) openapi3 에 μ„€μ • 정보 μž…λ ₯

μœ μ € 정보λ₯Ό μ‘°νšŒν•˜λŠ” κ°„λ‹¨ν•œ API λ₯Ό λ§Œλ“€μ–΄μ€λ‹ˆλ‹€.

package net.dezang.restdocopenapi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RestdocOpenapiApplication {
public static void main(String[] args) {
SpringApplication.run(RestdocOpenapiApplication.class, args);
}
}
package net.dezang.restdocopenapi;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
class User {
private Long id;
private String username;
private String password;
private Integer age;
private boolean enabled;
}
package net.dezang.restdocopenapi;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<List<UserDto.Response>> list() {
List<UserDto.Response> responses = userService.list().stream()
.map(UserDto.Response::of)
.collect(Collectors.toList());
return ResponseEntity.ok(responses);
}
@NoArgsConstructor(access = AccessLevel.PRIVATE)
static class UserDto {
@Data
@AllArgsConstructor
static class Response {
private String username;
private Integer age;
public static Response of(User domain) {
return new Response(domain.getUsername(), domain.getAge());
}
}
}
}
package net.dezang.restdocopenapi;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
class UserService {
public List<User> list() {
return Collections.singletonList(User.builder()
.id(1L)
.username("dezang")
.password("!topSecret!")
.age(20)
.enabled(true)
.build());
}
}

μš°λ¦¬λŠ” 컨트둀러 ν…ŒμŠ€νŠΈμ™€ 이λ₯Ό 톡해 λ‚˜μ˜€λŠ” API λ¬Έμ„œμ— 관심이 μžˆκΈ°μ— λ ˆν¬μ§€ν† λ¦¬ λ ˆλ²¨μ€ μ˜λ„μ μœΌλ‘œ μΆ”κ°€ν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. κ°„λ‹¨ν•œ μƒ˜ν”Œμ΄μ§€λ§Œ 도메인 객체와 응닡 κ°μ²΄λŠ” λΆ„λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

OPENAPI SPEC λ¬Έμ„œ 생성#

package net.dezang.restdocopenapi;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import java.util.Collections;
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; // (1)
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureRestDocs
@WebMvcTest(UserController.class)
class UserApiDoc {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Test
void list() {
//given
given(userService.list())
.willReturn(Collections.singletonList(
User.builder()
.username("dezang")
.age(10)
.build()
));
//when
ResultActions resultActions = mockMvc.perform(get("/users"));
//then
resultActions
.andExpect(status().isOk())
.andDo(document("list-user",
responseFields(
fieldWithPath("[].username").description("μ‚¬μš©μž 아이디"),
fieldWithPath("[].age").description("μ‚¬μš©μž λ‚˜μ΄")
)))
.andDo(print());
}
}
  • (1): MockMvcRestDocumentation 이 μ•„λ‹Œ MockMvcRestDocumentationWrapper λ‚΄ document λ©”μ†Œλ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±

μ½”λ“œ μž‘μ„±μ€ λλ‚¬μŠ΅λ‹ˆλ‹€. 이제 openapi3 λ¬Έμ„œλ₯Ό λ§Œλ“€κΈ° μœ„ν•΄ ν”ŒλŸ¬κ·ΈμΈμ΄ μ œκ³΅ν•˜λŠ” ν…ŒμŠ€ν¬λ₯Ό μ‹€ν–‰ν•©λ‹ˆλ‹€.

./gradlew openapi3

μœ„ λͺ…λ Ήμ–΄λ₯Ό μ‹€ν–‰ν•˜λ©΄, ν…ŒμŠ€νŠΈκ°€ 돌고 ν…ŒμŠ€νŠΈ 결과물이 build κ²½λ‘œμ— μƒμ„±λ©λ‹ˆλ‹€.

build
β”œβ”€β”€ api-spec
β”‚Β Β  └── openapi3.yaml
...
β”œβ”€β”€ generated-snippets
β”‚Β Β  └── list-user
β”‚Β Β  β”œβ”€β”€ curl-request.adoc
β”‚Β Β  β”œβ”€β”€ http-request.adoc
β”‚Β Β  β”œβ”€β”€ http-response.adoc
β”‚Β Β  β”œβ”€β”€ httpie-request.adoc
β”‚Β Β  β”œβ”€β”€ request-body.adoc
β”‚Β Β  β”œβ”€β”€ resource.json
β”‚Β Β  β”œβ”€β”€ response-body.adoc
β”‚Β Β  └── response-fields.adoc
...

μš°λ¦¬κ°€ μ›ν•˜λŠ” 파일이 μ €κΈ° λ³΄μ΄λ„€μš”. build/api-spec/openapi3.yaml μž…λ‹ˆλ‹€. 아웃풋 κ²½λ‘œλŠ” 처음 κ³΅μœ ν•œ build.gradle 에 openapi3 μ—μ„œ μ„€μ • κ°€λŠ₯ν•©λ‹ˆλ‹€. 그럼 yaml νŒŒμΌμ„ ν•œλ²ˆ μ‚΄νŽ΄λ³ΌκΉŒμš”?

openapi: 3.0.1
info:
title: USER-API
description: μ‚¬μš©μž λ¦¬μ†ŒμŠ€ API μž…λ‹ˆλ‹€.
version: 0.1.0
servers:
- url: http://localhost:8080
tags: []
paths:
/users:
get:
tags:
- users
operationId: list-user
responses:
"200":
description: "200"
content:
application/json:
schema:
$ref: '#/components/schemas/users450998075'
examples:
list-user:
value: "[{\"username\":\"dezang\",\"age\":10}]"
components:
schemas:
users450998075:
type: array
items:
type: object
properties:
age:
type: number
description: μ‚¬μš©μž λ‚˜μ΄
username:
type: string
description: μ‚¬μš©μž 아이디

생각보닀 별거 μ—†μ–΄λ³΄μ΄μ§€λ§Œ, μŠ€νŽ™ μžμ²΄λŠ” κ°•λ ₯ν•©λ‹ˆλ‹€. 이전 κΈ€ Spring Rest Docs & OAS 의 ν•„μš”μ„± 에 λŒ€ν•œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

μŠ€νŽ™ λ¬Έμ„œ ν™œμš©ν•˜κΈ°#

이제 μœ μ—°ν•œ 결과물을 μ–»μ—ˆμœΌλ‹ˆ, OAS λ₯Ό μ§€μ›ν•˜λŠ” λ„κ΅¬λ‚˜ μ„œλΉ„μŠ€λ₯Ό ν™œμš©ν•˜λ©΄ λ©λ‹ˆλ‹€. 일단 μ—¬κΈ°μ„œλŠ” 도컀λ₯Ό ν™œμš©ν•˜μ—¬ Swagger λ₯Ό μ‚¬μš©ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

docker run --rm -p 80:8080 \
-v __YOUR_BUILD_PATH__/api-spec:/usr/share/nginx/html/docs/ \
-e URL=docs/openapi3.yaml \
swaggerapi/swagger-ui

λ“œλ””μ–΄ API λ¬Έμ„œλ₯Ό λ³Ό 수 있게 λ˜μ—ˆμŠ΅λ‹ˆλ‹€!

Swagger UI 2021-09-05 13-56-49.png

마치며#

이제 API λ¬Έμ„œλ₯Ό λ§Œλ“€μ—ˆμœΌλ‹ˆ λμΌκΉŒμš”? REST DOCS + OAS λ₯Ό μ‹œμž‘ν•˜κ²Œ 된 μ΄μœ κ°€ MSA κ΅¬μ‘°μ—μ„œ λΆ„μ‚°λ˜μ–΄μžˆλŠ” API λ¬Έμ„œλ₯Ό μ–΄λ–»κ²Œ ν•˜λ©΄ ν†΅ν•©ν•΄μ„œ λ³Ό 수 μžˆμ„κΉŒ? μ˜€λ˜ 것을 κΈ°μ–΅ν•˜μ‹œλ‚˜μš”? 각 μ„œλΉ„μŠ€κ°€ μ–΄λ–€ 방법을 μ‚¬μš©ν•΄μ„œ OAS λΌλŠ” μŠ€νŽ™μ„ 지킨 λ¬Έμ„œλ₯Ό μƒμ„±ν–ˆλ‹€λ©΄, 이 λ¬Έμ„œλ₯Ό μ–΄λ–»κ²Œ μ„œλΉ™ν•˜λŠ” 것이 μ’‹μ„κΉŒμš”? λ‹€μŒ κΈ€μ—μ„œλŠ” μ—­μ‹œ λΆ„μ‚°λ˜μ–΄ μžˆλŠ” OAS νŒŒμΌμ„ μ–΄λ–»κ²Œ μžλ™μœΌλ‘œ ν•œ 곳으둜 λͺ¨μœΌκ³ , μ‚¬μš©μžλ“€μ—κ²Œ 보여쀄 것인지 고민의 κ²°κ³Όλ₯Ό κ³΅μœ ν•˜κ² μŠ΅λ‹ˆλ‹€.

Spring Rest Docs & OAS 의 ν•„μš”μ„±

κ°œμš”#

ν…ŒμŠ€νŠΈλ₯Ό 톡해 API λ¬Έμ„œκ°€ μ½”λ“œμ™€ μΌμΉ˜λ¨μ„ 보μž₯ν•΄μ£ΌλŠ” SPRING REST DOCS κ³Ό λ›°μ–΄λ‚œ λ²”μš©μ„±μ„ μžλž‘ν•˜λŠ” OPEN API SPEC (OAS 3) 의 μ‘°ν•©μœΌλ‘œ 개발된 API ν™œμš©λ„λ₯Ό κ·ΉλŒ€ν™” μ‹œν‚€λŠ” 법을 μ†Œκ°œν•©λ‹ˆλ‹€. λͺ¨λ“  API λ¬Έμ„œλ₯Ό ν•œ 곳으둜 λͺ¨μ„ 수 μžˆλŠ” 것은 λ³΄λ„ˆμŠ€μž…λ‹ˆλ‹€.

SPRING REST DOCS κ³Ό OAS 쑰합이 ν•„μš”ν•œ 이유#

개발된 API κ°€ λͺ©μ μ— 맞게 μ œλŒ€λ‘œ 이용되렀면 λ¬Έμ„œν™”λŠ” ν•„μˆ˜μ μž…λ‹ˆλ‹€. μ €λŠ” μ˜ˆμ „λΆ€ν„° ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό 톡해 API λ¬Έμ„œκ°€ μƒμ„±λ˜λŠ” SPRING REST DOCS 을 μ‚¬μš©ν•΄μ™”μŠ΅λ‹ˆλ‹€. API λ¬Έμ„œλ₯Ό μœ„ν•œ ν…ŒμŠ€νŠΈλ₯Ό λ§Œλ“€μ–΄μ•Ό ν•œλ‹€λŠ” 점은 λΆ„λͺ…νžˆ λ²ˆκ±°λ‘œμ› μ§€λ§Œ, API λ¬Έμ„œκ°€ μ½”λ“œμ™€ μΌμΉ˜λ˜μ§€ μ•Šμ•„ λ°œμƒν•  수 λ§Žμ€ μ΄μŠˆλ“€μ— λΉ„ν•˜λ©΄ 별거 μ•„λ‹ˆλΌκ³  μƒκ°ν•©λ‹ˆλ‹€. SPRINT REST DOCS κ°€ μ•„λ‹ˆλΌλ©΄ μ§€κΈˆκΉŒμ§€λ„ λͺ» 듀어보지 μ•Šμ•˜μ„κΉŒ 싢은 AsciiDoc 이 뢈만이긴 ν–ˆμŠ΅λ‹ˆλ‹€λ§Œ, λ§ˆν¬λ‹€μš΄κ³Ό λΉ„μŠ·ν•œ μ‚¬μš©λ²•μ— λŸ¬λ‹μ»€λΈŒκ°€ 거의 μ—†λ‹€μ‹œν”Όν•˜μ—¬ 잘 μ¨μ™”μŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ 색닀λ₯Έ 방법을 찾아봐야 ν•˜λŠ” μ΄μœ λŠ” 도ꡬ μžμ²΄κ°€ μ•„λ‹ˆλΌ, 도ꡬλ₯Ό 톡해 λ§Œλ“€μ–΄μ§„ λ¬Έμ„œ μ ‘κ·Όμ—μ„œ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

κ³„μ†ν•΄μ„œ λŠ˜μ–΄λ‚˜λŠ” API μ„œλ²„μ™€ λΆ„μ‚°λ˜λŠ” API λ¬Έμ„œ#

μ„œλΉ„μŠ€κ°€ ν™•μž₯함에 따라 API μ—”λ“œν¬μΈνŠΈκ°€ λŠ˜μ–΄λ‚˜λŠ” 것은 λΆˆκ°€ν”Όν–ˆκ³ , MSA ν™˜κ²½μ—μ„œ 개발되고 μžˆλŠ” μ œν’ˆμΈμ§€λΌ, API μΆ”κ°€λŠ” API μ„œλ²„μ˜ μΆ”κ°€λ‘œ μ΄μ–΄μ‘ŒμŠ΅λ‹ˆλ‹€. 각 μ„œλ²„λ§ˆλ‹€ REST DOCS 을 톡해 λ§Œλ“€μ–΄μ§€λŠ” API λ¬Έμ„œλŠ” 각 μ„œλ²„μ˜ νŠΉμ • μ—”λ“œν¬μΈνŠΈλ‘œ μ ‘κ·Όν•  수 μžˆμ—ˆκ³ , 우리 νŒ€μ€ μ—¬κΈ°μ €κΈ° ν©μ–΄μ ΈμžˆλŠ” API λ¬Έμ„œλ₯Ό μ°Ύμ•„λ‹€λ‹ˆλ©° κ°œλ°œμ„ ν•΄μ•Όν•˜λŠ”λΆˆνŽΈν•¨μ„ κ²ͺμ–΄μ•Όν–ˆμŠ΅λ‹ˆλ‹€.

λ˜ν•œ μžλ°”κ°€ μ•„λ‹ˆλΌ, λ‹€λ₯Έ μ–Έμ–΄λ‘œ 개발된 API의 경우 REST DOCS 을 μ‚¬μš©ν•  수 μžˆλŠ” 방법이 μ—†μ—ˆκ³ , 이런 이유둜 REST DOCS 을 μ‚¬μš©ν•˜μ§€ μ•Šκ±°λ‚˜ μ‚¬μš©ν•˜μ§€ λͺ»ν•˜λŠ” API μ„œλ²„λŠ” 사내 Notion 에 API λ¬Έμ„œλ₯Ό μž‘μ„±ν•΄ κ³΅μœ ν•˜λ„λ‘ ν•˜μ˜€μœΌλ‚˜, μ—­μ‹œλ‚˜ 크고 μž‘μ€ μˆ˜μ • 사항듀이 λΉˆλ²ˆν•˜κ²Œ λ™κΈ°ν™”λ˜μ§€ μ•Šμ•˜κ³ , λ…Έμ…˜μ— μžˆλŠ” API λ¬Έμ„œλŠ” μ‹ λ’°λ₯Ό μžƒμ–΄κ°€κ³  μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

μ–΄λ–»κ²Œ ν•˜λ©΄ 사내에 μžˆλŠ” λͺ¨λ“  API λ₯Ό ν•œ κ³³μ—μ„œ λ³Ό 수 μžˆμ„κΉŒ?#

μ–΄λ–»κ²Œ ν•˜λ©΄ μ—¬κΈ° μ €κΈ° ν©μ–΄μ ΈμžˆλŠ” API λ₯Ό ν•œ κ³³μ—μ„œ λ³Ό 수 μžˆμ„κΉŒμš”? κ·Έλž˜μ„œ 사내에 μžˆλŠ” λͺ¨λ“  κΈ°λŠ₯κ³Ό λ¦¬μ†ŒμŠ€λ₯Ό μ΅œλŒ€ν•œ ν™œμš©ν•  수 μžˆλŠ” ν™˜κ²½μ„ λ§Œλ“€ 수 μžˆμ„κΉŒμš”? μ²˜μŒμ—λŠ” 각 μ„œλ²„μ— μžˆλŠ” SPRING REST DOCS 으둜 μƒμ„±λœ html λ¬Έμ„œλ₯Ό λͺ¨μ•„μ„œ ν•œ μ›Ή μ„œλ²„μ—μ„œ ν˜ΈμŠ€νŒ…μ„ κ³ λ €ν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€λ§Œ, κ·Έ 각각의 λ¬Έμ„œ 링크λ₯Ό λ‹΄κ³  μžˆλŠ” 별도 νŽ˜μ΄μ§€κ°€ ν•„μš”ν–ˆκ³ , κ·Έ κ³³μ—λŠ” Notion 링크도 λ“€μ–΄κ°€μ•Ό ν–ˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•˜λ©΄ 관리가 잘 될 수 μžˆμ„κΉŒμš”? λͺ¨λ“  API μ„œλ²„κ°€ μ½”λ“œλ₯Ό κΈ°λ°˜ν•˜μ—¬ 같은 ν˜•νƒœ(μΈν„°νŽ˜μ΄μŠ€)둜 API μŠ€νŽ™μ„ μ •μ˜ν•œ 결과물을 λ§Œλ“€μ–΄ λ‚Ό μˆ˜λŠ” μ—†μ„κΉŒμš”?

이 μ‹œμ μ—μ„œ REST DOCS 을 잠깐 ν¬κΈ°ν•˜κ³  λ‹€λ₯Έ λŒ€μ•ˆμ„ 찾아보기 μ‹œμž‘ν–ˆμŠ΅λ‹ˆλ‹€. API λΈ”λ£¨ν”„λ¦°νŠΈ, μŠ€μ›¨κ±° κ³Ό 같은 μ˜€ν”„μ†ŒμŠ€λΆ€ν„°, 졜근 자주 λ³΄μ΄λŠ” λ¦¬λ“œλ―Έλ‹·μ»΄, Stoplight 유료 μ†”λ£¨μ…˜λ„ κ²€ν† ν•΄λ³΄μ•˜μ£ . 유료 μ†”λ£¨μ…˜μ„ μ•Œμ•„λ³΄λ‹ˆ μ •μ˜λœ API λ₯Ό λ―Έλ €ν•œ μ‚¬μ΄νŠΈλ‘œ ν˜ΈμŠ€νŒ…ν•΄μ£ΌλŠ” 역할을 μ£Όλ‘œν•˜κ³  API Spec μžμ²΄λŠ” κ°œλ°œμžκ°€ λ§Œλ“€μ–΄μ„œ λ„£μ–΄μ•Όν–ˆμŠ΅λ‹ˆλ‹€. (μ–΄μ°Œλ³΄λ©΄ λ‹Ήμ—°ν•œ 이야기...) 그런데 λ¬Έμ„œλ₯Ό μ½λ‹€λ³΄λ‹ˆ OAS 3, Swagger Support λΌλŠ” 문ꡬ가 μ—¬κΈ°μ €κΈ°μ„œ λ³΄μ΄λ”κ΅°μš”. SwaggerλŠ” μ‚¬μš©ν•΄λ΄€κ³ , OAS 3 λŠ” Open Api Specification v3 의 μΆ•μ•½μ–΄μ˜€μŠ΅λ‹ˆλ‹€. μ—¬κΈ°μ—μ„œ 해결방법이 μ–΄λ ΄ν’‹ λ³΄μ΄λŠ” 것 κ°™μ•˜μŠ΅λ‹ˆλ‹€.

그래, 관건은 API Specification.#

μœ„μ—μ„œ μ–ΈκΈ‰ν•œ 'λͺ¨λ“  API μ„œλ²„κ°€ μ½”λ“œλ₯Ό κΈ°λ°˜ν•˜μ—¬ 같은 ν˜•νƒœ(μΈν„°νŽ˜μ΄μŠ€)둜 API μŠ€νŽ™μ„ μ •μ˜ν•œ 결과물을 λ§Œλ“€μ–΄ λ‚Ό μˆ˜λŠ” μ—†μ„κΉŒμš”?' 은 μ €λ§Œ κ³ λ―Όν–ˆλ˜ 것은 μ•„λ‹ˆμ˜€μŠ΅λ‹ˆλ‹€. 이미 μ˜›λ‚ λΆ€ν„° 선배듀이 κ³ λ―Όν•΄μ™”κ³ , 그에 λŒ€ν•œ μ •μ˜λ„ λ§Œλ“€μ–΄ λ†“μ•˜μŠ΅λ‹ˆλ‹€.

OpenAPI v3.0 was released in July, 2017, by the OpenAPI Initiative, a consortium of member companies who want to standardize how REST APIs are described. There are various other approaches to API description

OpenAPI v3.0은 REST API μ„€λͺ… 방식을 ν‘œμ€€ν™”ν•˜λ €λŠ” νšŒμ›μ‚¬ μ»¨μ†Œμ‹œμ—„μΈ OpenAPI Initiativeμ—μ„œ 2017λ…„ 7월에 μΆœμ‹œν–ˆμŠ΅λ‹ˆλ‹€. API μ„€λͺ…에 λŒ€ν•œ λ‹€μ–‘ν•œ μ ‘κ·Ό 방식이 μžˆμŠ΅λ‹ˆλ‹€.

Stoplight - How to choose your api specification

μ—¬κΈ°μ„œ μ†Œκ°œν•˜λŠ” μ ‘κ·Ό 방식은 OpenAPI, JSON Schema, API Blueprint, RAML 이며, μΆ”μ²œν•˜λŠ” 녀석은 OpenAPI v3 μž…λ‹ˆλ‹€. μ—¬κΈ° μ €κΈ°μ„œ 봀던 OAS3 λ°”λ‘œ κ·Έ 녀석이죠. OAS3 에 λŒ€ν•΄μ„œ μžμ„Ένžˆ μ•Œκ³  μ‹Άλ‹€λ©΄ μ—¬κΈ° μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

이제 각 API μ„œλ²„κ°€ μ–΄λ–€ ν˜•μ‹μœΌλ‘œ ν‘œμ€€ν™”λœ 결과물을 μƒμ‚°ν•΄μ•Όν•˜λŠ”μ§€λŠ” κ²°μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€. OAS μŠ€νŽ™μ— λ§žλŠ” ν˜•νƒœλ‘œ yaml λ˜λŠ” json νŒŒμΌμ„ λ–¨κΆˆμ£Όλ©΄, κ·Έ νŒŒμΌμ„ 이쁘게 보여쀄 도ꡬ와 μ„œλΉ„μŠ€λŠ” λ„λ €μžˆμŠ΅λ‹ˆλ‹€. 뿐만 μ•„λ‹ˆλΌ OSA3 λŠ” 기계, 도ꡬ가 이해할 수 μžˆλ„λ‘ μ„€κ³„λ˜μ—ˆλ‹€κ³  ν•˜λ‹ˆ, 일단 OAS μŠ€νŽ™ 파일만 있으면 뭐든지 ν•  수 μžˆμ„ 것 κ°™λ„€μš”. 자 그럼 μ–΄λ–»κ²Œ 이 νŒŒμΌμ„ API μ„œλ²„λ‘œλΆ€ν„° 얻을 수 μžˆμ„κΉŒμš”? μ‚¬λžŒμ΄ ν•˜λ‚˜ν•˜λ‚˜ μž‘μ„±ν•  ν˜•νƒœλŠ” μ•„λ‹Œλ° 말이죠.

μŠ€μ›¨κ±°λ₯Ό λ‹€μ‹œ 써야 ν• κΉŒ?#

OAS λŠ” API μŠ€νŽ™μ„ λ‚˜νƒ€λ‚΄λŠ” κ°€μž₯ λŒ€ν‘œμ μΈ 포맷이라 각쒅 μ–Έμ–΄μ—μ„œ μ—¬λŸ¬κ°€μ§€ ν”„λ‘œμ νŠΈλ“€μ΄ μ§€μ›ν•©λ‹ˆλ‹€. μ—¬κΈ°μ„œλŠ” λŒ€ν‘œμ μœΌλ‘œ Spring μͺ½μ— SpringDoc Project 와 Nodejs μͺ½μ—λŠ” Swagger-jsdoc μ΄λΌλŠ” ν”„λ‘œμ νŠΈκ°€ μžˆμŠ΅λ‹ˆλ‹€. νŠΉνžˆλ‚˜ μŠ€μ›¨κ±°λŠ” OAS 적극 μ§€μ›ν•˜κ³  있으며, 이전뢀터 μžλ°”/μŠ€ν”„λ§ μ§„μ˜μ—μ„œλ„ 많이 μ“°μ΄λŠ” ν”„λ‘œμ νŠΈ μž…λ‹ˆλ‹€. μ–΄μ°Œλ³΄λ©΄ SPRINT REST DOC 보닀 더 많이 μ“°μ΄λŠ” 녀석일지도 λͺ¨λ₯΄κ² λ„€μš”. μŠ€ν”„λ§μ—μ„œ μ–΄λ…Έν…Œμ΄μ…˜ λͺ‡ 개둜 기본적인 λ¬Έμ„œλ₯Ό λšλ”± λ§Œλ“€μ–΄μ£ΌλŠ” λ„κ΅¬λ‹ˆκΉŒμš”. λ‹€λ§Œ μ˜ˆμ „μ— μŠ€μ›¨κ±°λ₯Ό 써 λ³Έ μž…μž₯μ—μ„œ 컨트둀러 μ½”λ“œμ— API μ„€λͺ…λ₯Ό μœ„ν•œ μ½”λ“œκ°€ λ„ˆλ¬΄ 덕지덕지 λΆ™μ–΄μžˆμ–΄μ„œ 가독성이 λ–¨μ–΄μ§€λŠ” 단점이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. μ œν’ˆ μ½”λ“œμ™€ 동기화도 보μž₯ν•˜μ§€ μ•Šκ΅¬μš”.

Rest Docs 이 Open API Spec 을 λ–¨κΆˆμ€„ 수만 μžˆλ‹€λ©΄.#

REST DOCS μ‚¬μš©μ„ ν¬κΈ°ν•˜κ³  μ—¬κΈ°κΉŒμ§€ μ™”μ§€λ§Œ, λ‹€μ‹œκΈˆ REST DOCS 이 κ·Έλ¦¬μ›Œμ§€λŠ” μˆœκ°„μž…λ‹ˆλ‹€. REST DOCS 이 μ™„μ„±λœ HTML λ¬Έμ„œλ₯Ό μ΅œμ’… 결과물둜 μƒμ„±ν•˜λŠ”κ²Œ μ•„λ‹ˆλΌ, OPEN API Spec 으둜 생성할 수만 μžˆλ‹€λ©΄, λ§Žμ€ 것듀이 해결될텐데 말이죠. 그런데! κ·Έλ ‡κ²Œ λͺ…ν™•ν•œ λ°©ν–₯을 작고 ꡬ글링을 ν•˜λ‹ˆ, 저와 같은 κ³ λ―Ό 끝에 κ·Έ 해닡을 μ–»μ–΄ λ‚Έ λΆ„ 이 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 그리고 μ—¬κΈ°μ„œ λ°œκ²¬ν•œ 이 λͺ¨λ“  것을 ν•΄κ²°ν•΄μ£ΌλŠ” restdocs-api-spec ν”„λ‘œμ νŠΈ! κΈ°μ‘΄ ν΄λž˜μŠ€λ“€μ„ λž©ν•‘ν•΄μ„œ λ§Œλ“€μ–΄λ†”μ„œ Import 만 λ°”κΏ”μ€˜λ„ κΈ°μ‘΄ μ½”λ“œμ— 큰 변화없이 적용이 κ°€λŠ₯ν–ˆμŠ΅λ‹ˆλ‹€. 이제 이 라이브러리λ₯Ό μ‚¬μš©ν•΄μ„œ REST DOCS ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό λ§Œλ“€λ©΄, OPENAPI 3 μŠ€νŽ™μœΌλ‘œ 파일이 뚝 λ–¨μ–΄μ§‘λ‹ˆλ‹€. 유레카!

마치며#

λΆ„μ‚°λœ API λ¬Έμ„œλ“€, 이 λ¬Έμ„œλ₯Ό ν•œ 곳으둜 λͺ¨μœΌκ³ μž ν–ˆλ˜ 갈망(?) κ³Ό OAS 와restdocs-api-spec μ΄λΌλŠ” 라이브러리λ₯Ό λ§Œλ‚˜κ²Œ λ˜κΈ°κΉŒμ§€μ— 여정을 μ†Œκ°œν•΄λ“œλ ΈμŠ΅λ‹ˆλ‹€. λ‹€μŒ κΈ€μ—μ„œλŠ” 본격적으둜 restdoc-api-spec 을 μ‚¬μš©ν•΄μ„œ OAS νŒŒμΌμ„ μƒμ„±ν•˜κ³ , μŠ€μ›¨κ±°μ—μ„œ νŒŒμΌμ„ 보기 쒋은 ν˜•νƒœλ‘œ μ„œλΉ™ν•˜λŠ” 뢀뢄을 μ μ–΄λ³Όκ»˜μš”. κΈ΄ κΈ€ μ½μ–΄μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€.