티스토리 뷰
이전 포스팅에서 Swagger 입문기에 대해 글을 썼었다. 이후 신규 프로젝트에 API 명세서 역할로 Swagger를 사용 중이다.
요즘 들어 Swagger의 장단점을 몸소 겪고 있는데, 추후에 관련 내용을 정리 후 포스팅 해보려고 한다.
(궁금하신 분은 아래 포스팅 참고!)
2024년12월 Swagger 입문기
2줄 요약프런트엔드 개발자와 협업을 위해 Swagger 적용각 어노테이션별 특징 및 사용법 정리 개발환경Spring boot : 3.3.2Swagger(=OpenAPI): 2.6.0Java: 17 배경설명한동안 신규 프로젝트의 기능 개발에만
better-tomorrow-than-today.tistory.com
3줄 요약
- 장점: 테스트 강제, 소스코드 침범 안 함
- 단점: 설정 겁나 복잡함
- 테스트 코드 작성하는 회사라면 Swagger보다 Spring Rest Docs 가 더 좋은 듯
환경 구성 및 Spring Rest Docs 설정
1. 의존성
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'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
2. build.gradle 설정
※ build.gradle 문서 전체(접은 글 참고)
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
asciidoctorExt
}
repositories {
mavenCentral()
}
ext {
set('snippetsDir', file("build/generated-snippets"))
}
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'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Rest docs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' // 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
ext { // 전역변수
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
}
asciidoctor{
inputs.dir snippetsDir
configurations 'asciidoctorExt'
dependsOn test
}
bootJar{
dependsOn asciidoctor
from("${asciidoctor.outputDir}"){
into 'static/docs'
}
}
- asciidoctor 플러그인 추가
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.asciidoctor.jvm.convert' version '3.3.2' // asciidoctor 추가
}
- asciidoctorExt Configuration 추가
configurations {
compileOnly {
extendsFrom annotationProcessor
}
asciidoctorExt //asciidoctor Extension 선언
}
- Rest Docs 의존성 추가
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'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// Rest docs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' // asciidoctor extension 연결
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
- 기타 설정
ext { // 전역변수 : snippets의 경로 선언
snippetsDir = file('build/generated-snippets')
// snippets : 문서들의 조각
}
test { // 테스트가 끝난 결과물을 snippet 디렉토리로 선언
outputs.dir snippetsDir
}
asciidoctor{ // task
inputs.dir snippetsDir
configurations 'asciidoctorExt' // 앞에서 선언한 Extension 설정
sources { // 특정 adoc(index.html)만 html로 만든다.
include("**/index.adoc")
}
baseDirFollowsSourceFile() // 다른 adoc 파일을 include 할 때 경로를 baseDir로 맞춘다.
dependsOn test // dependsOn : 작업순서, 의미 : 테스트가 수행된 이후에 assciidoctor를 수행한다
}
bootJar{ // jar를 만드는 과정
dependsOn asciidoctor // asciidoctor 이후에 수행, 전체 빌드 순서 : 테스트 -> asciidoctor -> bootJar
from("${asciidoctor.outputDir}"){ // 문서를 정적 파일로 보기 위해 소스코드 내에 static/docs 하위에 위치
into 'static/docs'
}
}
- Intellij plugin 추가(asciidoctor 미리 보기 지원)
간단한 API 개발
게시글 저장 및 조회하는 간단한 API를 만들었다. 세부 코드는 아래 깃허브에서 확인이 가능하다.
https://github.com/gomshiki/rest-docs-practice
GitHub - gomshiki/rest-docs-practice
Contribute to gomshiki/rest-docs-practice development by creating an account on GitHub.
github.com
테스트 코드 작성
1) Spring Rest Docs 설정(추상클래스 - RestDocsSupport)
- @ExtendWith : Rest Docs 익스텐션 추가
- MockMvcBuilders.standaloneSetup() : Spring 의존성을 사용하지 않음
@ExtendWith(RestDocumentationExtension.class)
// @SpringBootTest : api 명세 문서를 만드는데 스프링 서버를 띄울 필요가 없기에 사용하지 않음
public abstract class RestDocsSupport {
protected MockMvc mockMvc;
protected ObjectMapper objectMapper = new ObjectMapper();
// standaloneSetup() 적용 시 Spring 의존성을 사용하지 않음
@BeforeEach
void setUp(RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.standaloneSetup(initController()) // 문서로 만들 Controller 를 주입해서 넣어주면 됨.
.apply(MockMvcRestDocumentation.documentationConfiguration(provider))
.build();
}
protected abstract Object initController(); // 추상 메서드로 선언하고, 하위 구현 클래스에서 해당 클래스를 주입받도록 설정
}
2) 테스트 코드 작성 - PostControllerDocsTest
- mockMvc를 통한 presentation 테스트 코드 작성 후 Rest Docs을 위한 코드 작성(MockMvcRestDocument 이용)
- preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()) : json을 더 보기 좋게 수정
- PayloadDocumentation.requestFields() : Request Dto에 정의한 필드 정의
- PayloadDocumentation.fieldWithPath("title") : 필드명
- ... type(JsonFieldType.STRING) : 해당 필드의 타입 (숫자일 경우 JsonFieldType.NUMBER)
- ... description("글 제목") : 해당 필드 의미
package com.restdocspractice;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.PayloadDocumentation;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
class PostControllerDocsTest extends RestDocsSupport {
// Mock 설정
private final PostService postService = Mockito.mock(PostService.class);
@Override
protected Object initController() {
return new PostController(postService);
}
@DisplayName("게시글을 등록한다.")
@Test
void createPost() throws Exception {
// given
String title = "모각코 인원 모집";
String writer = "김준성";
String contents = "주말에 모각코 하실분?";
PostRequest request = new PostRequest(title, writer, contents);
BDDMockito.given(
postService.create(any(PostRequest.class)))
.willReturn(new PostResponse(title, writer, contents));
// when // then
mockMvc.perform(
MockMvcRequestBuilders.post("/api/v1/posts")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(jsonPath("$.title").value(title))
.andExpect(jsonPath("$.writer").value(writer))
.andExpect(jsonPath("$.contents").value(contents))
// RestDoc 작성을 위한 메서드 체이닝
.andDo(MockMvcRestDocumentation.document("post-create", // 식별자
preprocessRequest(prettyPrint()), // 한줄로 표현된 json을 보기좋게 변경
preprocessResponse(prettyPrint()), // 한줄로 표현된 json을 보기좋게 변경
PayloadDocumentation.requestFields(
// Controller 요청 DTO 필드 정의
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("글 제목"),
PayloadDocumentation.fieldWithPath("writer").type(JsonFieldType.STRING)
.description("작성자"),
PayloadDocumentation.fieldWithPath("contents").type(JsonFieldType.STRING)
.optional() // templates/request-fields.snippet 을 통해 Optional 체크 설정
.description("글 내용")
),
PayloadDocumentation.responseFields(
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING)
.description("글 제목"),
PayloadDocumentation.fieldWithPath("writer").type(JsonFieldType.STRING)
.description("작성자"),
PayloadDocumentation.fieldWithPath("contents").type(JsonFieldType.STRING)
.description("글 내용")
)
));
}
}
3) request-fields.snippet과 response-fields.snippet 작성
- request와 response 필드 중 Optional 적용
- 파일 경로 : test/resources/org/springframework/restdocs/templates
- request-fields.snippet
==== Request Fields
|===
|Path|Type|Optional|Description
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}O{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}
|===
- response-fields.snippet
==== Response Fields
|===
|Path|Type|Optional|Description
{{#fields}}
|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{#optional}}O{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}
{{/fields}}
|===
4) build.gradle 에 정의한 task 실행(asciidoctor)
- 실행 전 clean으로 build 폴더 삭제 후 asciidoctor 실행
- build/generated-snippets/post-create에 테스트코드에서 적용했던 RestDoc 코드가. adoc 파일로 생성
5) asciidoc 문서 작성
- 경로 : src/docs/asciidoc
- index.adoc 및 post.adoc 작성
- 앞서 생성된. adoc 파일을 Include 하는 형식으로 문서 작성
- index.adoc
ifndef::snippets[]
:snippets: ../../build/generated-snippets
endif::[]
= 게시판 REST API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
// 링크
[[POST-API]]
== Post API
// 다른 adoc 파일 include
include::post.adoc[]
- post.adoc
[[post-create]]
=== 게시글 등록
==== HTTP Request
include::{snippets}/post-create/http-request.adoc[]
include::{snippets}/post-create/request-fields.adoc[]
==== HTTP Response
include::{snippets}/post-create/http-response.adoc[]
include::{snippets}/post-create/response-fields.adoc[]
5) 빌드 및 실행 후 접속해 보기
- build.gradle 에서 bootJar 정의한 이유 : 프런트 개발자가 RestDocs 엔드포인트로 접속할 수 있도록 하가 위함!
- task - build 클릭 : build/libs/rest-docs-practice-0.0.1-SNAPSHOT.jar 생성 확인
- 터미널에서 생성된 jar 경로 이동 후 java -jar rest-docs-practice-0.0.1-SNAPSHOT.jar 실행
- http://localhost:8080/docs/index.html 접속
정리
- Spring Rest Docs를 사용하기 위해선 asciidoctor 가 필요하다.
- Rest Docs는 MockMvc와 RestAssured를 이용한다.
- 테스트 코드를 작성 후 RestDocs 관련 코드를 작성해야 한다.
- asciidoctor 빌드를 후 index.adoc 문서를 작성한다. 이 문서를 기반으로 build/docs/asciidoc/index.html 이 생성된다.
- build.gradle에서 설정한 bootJar로 서버 실행 후 해당 api 문서에 접근이 가능하다.
이상 전달 끝!
참고 자료
인프런 강의(박우빈-Practical Testing: 실용적인 테스트 가이드)
Practical Testing: 실용적인 테스트 가이드
Spring REST Docs
www.inflearn.com
'프레임워크 > Spring & Spring boot' 카테고리의 다른 글
DTO와 Entity 간 다양한 변환 전략 톺아보기 (0) | 2025.03.02 |
---|---|
동시성 해결하다가 Advisory Lock 을 알게된 건에 대하여(Feat. PostgreSql) (0) | 2025.02.16 |
2024년12월 Swagger 입문기 (0) | 2024.12.22 |
기업정보 조회 API 개발 기록(2024.09.02 ~ 2024.09.30) (0) | 2024.10.06 |
[JPA] 학습테스트로 알아보는 영속과 준영속 (feat. EntityManager) (0) | 2024.07.02 |
- Total
- Today
- Yesterday
- 취업리부트코스
- thymeleaf
- BFS
- Spring
- 재기동
- 코딩테스트
- Comparator
- 나만의챗봇
- Comparable
- 챗봇
- 객체정렬
- spring boot
- 글또
- JWT
- 항해99
- 백준
- BufferedWriter
- 코드트리
- springboot
- 전자정부프레임워크
- 유데미
- 자바
- Java
- script
- dxdy
- 취리코
- RASA
- NLU
- 개발자취준
- BufferedReader
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |