Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Spring REST Docs (20240124) #33

Merged
merged 4 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions _posts/2023-11-21-web-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ tags: [웹 API, REST, GraphQL, WebSocket, 기술세미나]
우선 Application은 고유한 기능을 가진 모든 소프트웨어, Interface는 애플리케이션 간의 서비스 계약이라고 할 수 있습니다. 그리고 계약은 요청과 응답을 사용하여 애플리케이션이 서로 통신하는 방법이라고 할 수 있죠.

좀 더 쉬운 설명을 위해 이미지를 가져와봤습니다. 식당을 예시로 들어볼게요.<br>
1) 손님은 점원이 가져다준 메뉴판으로 메뉴를 주문하고, 점원이 주문을 요리사에게 전달
2) 요리사는 주문받은 요리를 만들어 점원에게 주고, 손님은 점원이 가져다준 요리로 맛있게 식사
1. 손님은 점원이 가져다준 메뉴판으로 메뉴를 주문하고, 점원이 주문을 요리사에게 전달
2. 요리사는 주문받은 요리를 만들어 점원에게 주고, 손님은 점원이 가져다준 요리로 맛있게 식사

여기서 점원이 바로 API와 같은 역할을 한다고 볼 수 있습니다.

Expand All @@ -45,17 +45,17 @@ tags: [웹 API, REST, GraphQL, WebSocket, 기술세미나]

### 주요 특징
REST API의 주요 특징은 다음과 같습니다.
1) **Stateless** (무상태)
1. **Stateless** (무상태)
- 서버는 요청이 오가는 동안 클라이언트의 상태를 저장하지 않음
2) **Cacheable** (캐시 가능성)
2. **Cacheable** (캐시 가능성)
- 클라이언트가 응답을 캐시하여 네트워크 부하를 줄이고 성능을 향상시킬 수 있음
3) **Uniform Interface** (통일된 인터페이스)
3. **Uniform Interface** (통일된 인터페이스)
- API 디자인이 통일되어 있어 사용하기 쉬움
- 즉, 애플리케이션 요구사항별로 다른 형식이 아닌, 표준화된 형식으로 정보를 전송할 수 있도록 구성 요소 간 통합된 인터페이스를 가짐
4) **Server-Client** (서버-클라이언트 구조)
4. **Server-Client** (서버-클라이언트 구조)
- 클라이언트와 서버가 각각 독립적으로 발전할 수 있음
- 예를 들어, 웹 브라우저는 사용자에게 웹페이지를 보여주고, 서버는 데이터를 제공
5) **Layered System** (계층화된 시스템)
5. **Layered System** (계층화된 시스템)
- 시스템이 계층으로 나뉘어 있음
- 각 계층은 특정 역할을 수행하며, 상위 계층은 하위 계층의 구현을 알 필요가 없음
- 예를 들어, 클라이언트는 데이터를 요청하면 중간에 여러 계층을 거쳐 서버에 도달할 수 있지만 클라이언트는 중간 계층의 존재를 몰라도 됨
Expand All @@ -69,13 +69,13 @@ REST API의 주요 특징은 다음과 같습니다.
아래 JSON 응답을 참고해 설명해보겠습니다.<br>
(Fetch는 *웹 페이지를 구성하기 위해서 다양한 서버에 요청을 보내고 데이터를 받아오는 작업* 이라고 생각하시면 됩니다.)

1) Over-Fetching
1. Over-Fetching
- 원하는 응답 데이터 → 도서 ID, 도서명, 저자명만 필요, 그러나 해당 API Response 값에 가격, 출판사, ISBN 등이 포함되어 있다면 다 받아야 함
- 즉, Over-Fetching은 필요한 데이터 이상으로 서버에서 데이터를 받아오게 되는 것을 의미하며, 필요없는 데이터까지 받아와 서버와 자원이 낭비됨
2) Under-Fetching
2. Under-Fetching
- 도서 상세 페이지에서 도서 정보와 리뷰 목록을 보여주려고 함, 그러나 API가 도서 정보와 도서 리뷰에 대해 각각 다른 End-Point를 사용한다면 필연적으로 2번의 API 호출 발생
- 즉, 한 번의 요청으로 필요한 데이터를 모두 받아오지 못해 여러 번의 요청을 수행하는 것을 의미하며, 네트워크의 지연이 발생할 수 있고 사용자는 느린 로딩 속도로 인해 불편함을 겪을 수 있음
3) 다양한 엔드포인트
3. 다양한 엔드포인트
- REST API는 여러 엔드포인트가 존재하며, 각자의 역할을 하고 있으므로 클라이언트는 다양한 엔드포인트를 요청해야 함
```json
{
Expand Down Expand Up @@ -107,13 +107,13 @@ REST API의 주요 특징은 다음과 같습니다.

### 주요 특징
GraphQL API의 주요 특징은 다음과 같습니다.
1) **유연하고 강력한 데이터 쿼리 언어**
1. **유연하고 강력한 데이터 쿼리 언어**
- 클라이언트가 필요한 데이터의 구조와 양을 정확하게 명시할 수 있는 강력한 쿼리 언어를 제공하므로 과도한 데이터 전송이나 다수의 요청을 최소화할 수 있음
- 즉, REST API의 오버 페칭과 언더 페칭과 같은 이슈가 발생하지 않음
2) **단일 엔드포인트**
2. **단일 엔드포인트**
- REST API에서는 각 엔드포인트마다 데이터를 요청해야 했지만, GraphQL은 단일 엔드포인트를 사용하여 클라이언트가 단일 요청으로 여러 데이터를 가져올 수 있음
- ex) @PostMapping("/graphql")
3) **실시간 데이터 업데이트**
3. **실시간 데이터 업데이트**
- 실시간 데이터 업데이트를 지원
- 일반적으로 Subscription이라 불리는 메커니즘을 통해 이루어짐
```text
Expand Down Expand Up @@ -156,17 +156,17 @@ query {

### 주요 특징
WebSocket API의 주요 특징은 다음과 같습니다.
1) **양방향 통신**
1. **양방향 통신**
- 클라이언트-서버간 양방향 통신 가능
- 서버가 클라이언트에게 데이터를 푸시하고, 클라이언트가 서버에게 데이터를 전송할 수 있음
2) **실시간성**
2. **실시간성**
- 연결을 유지하면서 데이터를 실시간으로 전송
- 단방향 통신과는 달리, 데이터의 지연 시간을 최소화하여 실시간 응용 프로그램을 구축하는 데 적합
3) **단일 연결 유지**
3. **단일 연결 유지**
- WebSocket은 한 번의 연결을 설정하고 유지함으로써 여러 요청에 대한 새로운 연결을 맺지 않아도 됨
4) **효율적인 데이터 전송**
4. **효율적인 데이터 전송**
- 연결을 유지하면서 계속 데이터를 주고받기 때문에, 새로운 연결을 설정할 필요가 없어 헤더의 오버헤드가 감소함
5) **이벤트 기반 모델**
5. **이벤트 기반 모델**
- 이벤트 기반의 모델을 사용하여 메시지를 수신하고 처리할 수 있음

## 5. API 비교
Expand Down
272 changes: 272 additions & 0 deletions _posts/2024-01-24-spring-rest-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
---
layout: post
title: 이번엔 Spring REST Docs를 써볼까?
author: 김영롱
categories: 기술세미나
banner:
image: https://github.com/Kernel360/blog-image/blob/main/2024/0124/spring-rest-docs.png?raw=true
background: "#000"
height: "100vh"
min_height: "38vh"
heading_style: "font-size: 4.25em; font-weight: bold; text-decoration: underline"
tags: [API 문서, Spring REST Docs, 기술세미나]
---
안녕하세요, *Lune* 입니다.

이번엔 **Spring REST Docs**를 기술 세미나 주제로 가져와봤는데요.

Spring REST Docs를 프로젝트에서 사용하게 된 이유부터 적용기까지 공유해보려고 합니다.

## 1. API 문서의 필요성
조금 뻔한 얘기로 시작해볼까요?<br>
개발을 하면서 API 문서들이 필요한 이유는 무엇일까요? 제가 생각해봤을 때, 그리고 검색을 해봤을 때 아래와 같은 이유들이 있었습니다.

1. API 사용법 이해
2. 개발자 간의 효율적인 협업
3. 빠른 문제 해결 및 버그 대응
4. 개발 생산성 향상

개인적으로는 일단 2번이 제일 와닿습니다. 협업할 때 같은 API 문서를 보며 얘기하면 소통이 더 잘된다고 느낀 적이 많았거든요.

## 2. API 문서를 만드는 여러 방법들
자, 그럼 이번에는 API 문서를 만들 수 있는 방법들에 대해 이야기해볼게요.

1. 구글 공유 문서 (Docs / Sheets)
2. Notion
3. GitBook
4. Swagger
5. Spring REST Docs

위에 리스트 말고도 더 많은 방법들이 있겠지만, 제가 사용해봤거나 사용사례를 본 적 있는 것 위주로 작성해봤습니다.

### 구글 공유 문서 / Notion / GitBook
코드베이스가 아니라 수동으로 직접 문서를 작성하는 방식입니다. 장단점을 알아보자면 다음과 같습니다.

장점 | 단점
-----------------|------------------
쉬운 협업 및 공유 | API 문서 작성 기능 제한적
커스텀 스타일 적용 쉬운 편 | 비용 문제 발생 가능
사용자 친화적인 UI/UX | API 문서 자동화 어려움

위 내용이 3가지 모두에 동일하게 적용된다고 할 수는 없지만, 보편적인 장단점 위주로 설명해보려고 했으니 조금 안 맞다고 생각이 들어도 이해해주세요 👐

### Swagger / Spring REST Docs
코드 내에 API 문서화를 위한 작업을 진행하는 방식입니다. 장단점을 알아보자면 다음과 같습니다.

장점 | 단점
----------------|-----------------
자동 문서 생성 | 러닝커브 존재 및 설정 복잡
일관된 형식과 스타일 유지 | 커스터마이제이션 한계
실시간 업데이트 | 의존성 및 업그레이드 어려움

여기서 *커스터마이제이션 한계* 란 문서의 외관이나 기능을 개발자가 원하는 대로 조정하는 데에 한계가 있다는 것을 의미합니다.

## 3. Spring REST Docs를 선택한 이유
현재 진행 중인 프로젝트에서는 왜 Spring REST Docs를 선택하게 되었을까요?

우선 첫 시작은 GitBook 이었답니다. 기능 개발이 들어가지 않은 상태에서 프론트엔드 개발자와의 빠른 협업을 위해 GitBook으로 API 문서를 만들기 시작했었죠. 그런데 초반에 잘 알아보지 않은 탓에 얼마 지나지 않아 무료 버전의 한계를 맞닥뜨리게 되었습니다. 무료 버전에서는 여러 사람이 문서 편집을 할 수 없어 공동 작업이 더 이상 불가능하게 되어버렸거든요 🥲

그 다음으로 생각한 건 API 문서 자동화가 가능한 Swagger와 Spring REST Docs였습니다. 그래서 그 둘을 비교해봤습니다.

### Swagger

장점 | 단점
----------------|---------------------------
어노테이션 기반 문서 생성 | 프로덕션 코드에 작업 필요
화면에서 API 테스트 | 프로덕션 코드와 동기화 안 되어 있을 수 있음
비교적 쉬운 적용 |

### Spring REST Docs

장점 | 단점
-----------------|---------------
프로덕션 코드에 영향 없음 | 적용이 어려운 편
테스트 성공 시 문서 생성 | 테스트 코드 양이 늘어남
API 문서 최신 상태 유지 |

(제목에 드러나있듯이) 결과적으로 Spring REST Docs를 사용하기로 결정을 했습니다. 이번 프로젝트에서는 테스트 코드를 작성하기로 했었고, 프로덕션 코드에 영향이 없고 늘 현행화가 가능하다는 점에 이끌렸거든요.

## 4. Spring REST Docs 적용기
이제부터는 Spring REST Docs를 적용했던 과정을 하나씩 설명해보겠습니다.<br>
참고로 저는 Asciidoctor & MockMvc 방식을 사용했는데요. 공식 문서에 따르면 문서 작성을 위해 Asciidoctor와 Markdown을 지원하고 있고 MockMvc, WebTestClient, REST Assured 방식의 테스트 예시를 보여주고 있습니다.

### 1) build.gradle 설정
build.gradle에 추가되어야 하는 내용 및 설명은 아래 코드로 대체합니다.
```groovy
plugins {
// Asciidoctor 플러그인 적용
id "org.asciidoctor.jvm.convert" version "3.3.2"
}

ext {
// 생성된 snippets 출력 위치를 정의하는 snippetsDir 속성을 구성
snippetsDir = file('build/generated-snippets')
}

configurations {
// asciidoctorExt 구성을 선언
asciidoctorExt
}

dependencies {
// asciidoctorExt 구성에서 spring-restdocs-asciidoctor에 대한 종속성을 추가
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
// MockMvc 테스트 방식을 사용
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

tasks.named('test') {
// 테스트 작업을 실행하면 출력이 snippetsDir에 기록된다는 것을 Gradle이 인식하도록 함
outputs.dir snippetsDir
}

asciidoctor {
// asciidoctor 작업을 구성
dependsOn test
configurations 'asciidoctorExt'
baseDirFollowsSourceFile()
inputs.dir snippetsDir
}

asciidoctor.doFirst {
// asciidoctor 작업이 수행될 때 가장 먼저 수행
delete file('src/main/resources/static/docs')
}

task copyDocument(type: Copy) {
// html 파일 복사
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}

bootJar {
dependsOn copyDocument
}
```

### 2) 테스트 코드 작성
첫 번째 *테스트 코드* 외에는 구현 방식에 따라 다를 수 있으므로 가볍게 봐주시면 됩니다.
```java
/** 테스트 코드 **/
result.andExpect(status().isOk())
.andDo(document(
"commoncode/get-common-codes",
getDocumentRequest(),
getDocumentResponse(),
// pathParameters, queryParameters, requestFields, responseFields는 필요 시 각각 작성
pathParameters(
parameterWithName("codeName").description("코드명")
),
responseFields(beneathPath("value").withSubsectionId("value"),
fieldWithPath("codeNo").type(JsonFieldType.NUMBER).description("코드번호"),
fieldWithPath("codeName").type(JsonFieldType.STRING).description("코드명"),
fieldWithPath("description").type(JsonFieldType.STRING).description("설명").optional()
)
));
```
```java
/** Utils 만들어 사용 **/
public interface RestDocumentUtils {

static OperationRequestPreprocessor getDocumentRequest() {
return preprocessRequest(modifyUris().scheme("http")
.host("spring.restdocs.test") // 실제 host 아님
.removePort(), prettyPrint());
}

static OperationResponsePreprocessor getDocumentResponse() {
return preprocessResponse(prettyPrint());
}
}
```
```java
/** 추상 클래스 작성 **/
@WebMvcTest({
CommonCodeController.class,
})
@AutoConfigureRestDocs // 통합 테스트에서 Spring REST Docs를 활성화하고 구성하는 데 사용
public abstract class ControllerTest {

@Autowired
protected MockMvc mockMvc;

@Autowired
protected ObjectMapper objectMapper;

@MockBean
protected CommonCodeService commonCodeService;
}

// 아래와 같이 상속받아 사용
// class CommonCodeControllerRestDocsTest extends ControllerTest {
```

### 3) 테스트 성공
위와 같이 작성한 테스트 코드가 통과하게 된다면 build/generated-snippets 경로 하위에 adoc 확장자 파일들이 여러 개 생성된 것을 확인할 수 있습니다. 파일을 하나씩 선택해서 보면 Asciidoc 문법에 맞게 작성된 내용과 미리보기를 확인할 수 있답니다.

![gradle-build](https://github.com/Kernel360/blog-image/blob/main/2024/0124/gradle-build.png?raw=true)

### 4) API 문서 틀 작성
위에서 본 문서 조각들(snippets)을 그대로 활용할 수 있다면 좋겠지만, HTML 형태의 API 문서로 만들어 주기 위해서는 아직 추가적인 작업이 남아있습니다. 한 데 모아주는 작업을 해줘야 하는데요. 우선 저는 *index.adoc* 이라는 파일을 만들어 API별 adoc 파일을 include 하는 구조를 사용했습니다. 참고로 index.adoc 파일은 *src/docs/asciidoc 하위* 에 만들어주었는데 이 경로가 gradle을 사용할 경우의 기본 경로랍니다.

```asciidoc
// index.adoc
= API Document
// Asciidoc 문서의 구조와 스타일 정의
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:

// adoc 파일 include
include::overview.adoc[]
include::sample-api.adoc[]
```
```asciidoc
// sample-api.adoc
== 샘플 API

[[공통코드-조회]]
=== 공통코드 조회

==== Request
include::{snippets}/commoncode/get-common-codes/path-parameters.adoc[]

===== HTTP Request 예시
include::{snippets}/commoncode/get-common-codes/http-request.adoc[]

==== Response
include::{snippets}/commoncode/get-common-codes/response-fields-value.adoc[]

===== HTTP Response 예시
include::{snippets}/commoncode/get-common-codes/http-response.adoc[]
```

### 5) API 문서 생성
이제 거의 마무리 단계인데요. gradle의 bootJar 작업을 실행시키면 build.gradle에 작성한 copyDocument 작업을 거쳐서 미리 지정한 resources/static/docs 경로 하위에 build 내에 생성되어 있던 html 파일이 복사되어 들어오고, 서버를 띄웠을 때 도메인 하위 /docs/index.html 경로로 접속해 API 문서를 확인할 수 있게 된답니다.

![api-doc](https://github.com/Kernel360/blog-image/blob/main/2024/0124/api-doc.png?raw=true)

### 6) API 문서 살펴보기
최종적으로 만들어진 문서의 형태는 다음과 같습니다. 좌측에 ToC가 있어 원하는 API로의 이동이 간편하고 화면도 깔끔하지 않나요?

![index-html](https://github.com/Kernel360/blog-image/blob/main/2024/0124/index-html.png?raw=true)

## 5. 정리
지금까지 저와 함께 API 문서부터 Spring REST Docs 적용기까지 살펴봤는데요. 읽기에 어떠셨을지 궁금합니다 😄

Spring REST Docs를 적용하면서 여러 차례 시행착오를 거치며 보낸 시간이 생각나네요. 처음 접하는 부분이 많아 공식 문서, 영상, 블로그 등을 참고했는데 그 과정에서 조금씩 이해하게 되고 원하는 결과를 만들어낼 수 있어서 개인적으로 뿌듯하고 좋았던 경험이었습니다.

아 그리고 이번 세미나를 준비하면서 추가적인 정보를 찾다가 알게 된 건데 Swagger와 Spring REST Docs의 장점만 취해서 API 문서화를 할 수 있는 방법도 있다고 해요. 가능하다면 다음에는 이 방식을 사용하거나 시간이 될 때 변경해볼 수 있으면 좋을 것 같다는 생각이 드네요.

## 6. 참고자료
- [Spring REST Docs](https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/)
- [Spring REST Docs에 날개를… (feat: Popup)](https://techblog.woowahan.com/2678)
- [Spring REST Docs를 사용한 API 문서 자동화](https://hudi.blog/spring-rest-docs)
- [RestDocs로 API 문서화하기](https://velog.io/@junho5336/RestDocs%EB%A1%9C-API-%EB%AC%B8%EC%84%9C%ED%99%94%ED%95%98%EA%B8%B0)
- [[ 스프링 기반 REST API ] 스프링 REST Docs 소개](https://youtu.be/BFme2t9KSwA?si=ziyQ3jC1l-tQ57Md)
- [[10분 테코톡] 승팡, 케이의 REST Docs](https://youtu.be/BoVpTSsTuVQ?si=VG3mhS5b32l1EY_s)
- [[Spring] restdocs + swagger 같이 사용하기](https://velog.io/@hwsa1004/Spring-restdocs-swagger-%EA%B0%99%EC%9D%B4-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0)