도입 계기
테크루키 중간발표 때 API 문서 자동화 Tool에 대해 발표한 적이 있다
아무래도 개발자에게 중요한 소양(?) 중 하나가 문서화라고 생각해서 우리팀은 문서화에 대해 광적으로 집착하였다...ㅋㅋㅋ
암튼! 그 때 swagger와 spring rest docs 중 고민하다가 보다 신뢰성 있는 문서화를 위해 spring rest docs를 채택했다고 하였다
그럤더니 피드백으로 "2가지를 동시에 사용할 수 있는데 굳이 하나만 골라야 했나요?🤔" 라는 말씀을 듣고 띵..했다
그래서 바아로 2가지를 동시에 사용하는 방법을 채택했다고 한다~
이제 swagger와 spring rest docs의 장단점을 비교해보고 적용 방법과 느낀 점에 대해 간단히 기록하려고 한다!
Swagger
장점
- API 문서가 자동으로 생긴다
- 어노테이션(annotation)을 통해 문서가 생성되기 때문에 API 현행화가 쉽다
- 화면에서 API를 직접 호출하여 테스트해볼 수 있다
단점
- 프로덕션 코드에 문서화를 위한 코드가 들어간다
- 서버가 실행될 때 문서가 만들어지기 때문에 API 스펙만 분리해서 관리하기 어렵다
- 검증되지 않은 API가 생성될 수 있다
Spring Rest Docs
장점
- 테스트코드 통과 후 API 문서가 생성되기 때문에 신뢰할 수 있다
- 비즈니스 로직에는 API 문서 관련 코드가 없다
- 커스텀이 자유롭다
단점
- 문서가 추가되면 asciido 문서를 일일이 편집해야 한다
- swagger에 있는 API 문서 관련 코드가 없다
- 커스텀이 자유롭다
SwaggerUI + Spring Rest Docs
- 위의 그림에서 첫번째가 기존 Spring rest docs를 사용했을때의 흐름이고, 두번째가 Spring rest docs에 openapi3와 swaggerui를 통해 합쳤을 때의 흐름이다
- 기존에 Spring rest docs만을 사용할 때는 Asciidoctor를 통해 문서를 생성한 반면, 새로운 방법에서는 spring rest docs 실행 결과를 openapi3 스펙으로 출력하고 이를 통해 swagger ui를 생성하는 방식으로 동작한다
- 이 둘을 합치는 방법의 핵심은 아래의 두 플러그인이다
- com.epages.restdocs-api-spec
- Spring Rest Docs의 결과물을 OpneAPI3 스펙으로 변환한다
- org.hidetake.swagger.generator
- OpenAPI3 스펙을 기반으로 SwaggerUI를 생성한다 → HTML, CSS, JS
- com.epages.restdocs-api-spec
OpenAPI3 스펙 출력 설정
// 1. restdocsApiSpecVersion 버전 변수 설정
+ buildscript {
+ ext {
+ restdocsApiSpecVersion = '0.16.2'
+ }
+ }
plugins {
id 'org.springframework.boot' version '2.7.2'
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
// 2. asciidoctor 플러그인 제거, restdocs-api-spec 플러그인 추가
- id 'org.asciidoctor.convert' version '1.5.8'
+ id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
// 3. openapi3 설정
+ openapi3 {
+ setServer("http://localhost:8080") // API 요청을 보낼 서버 주소 설정
+ title = "restdocs-swagger API Documentation" // API 문서 제목
+ description = "Spring REST Docs with SwaggerUI." // API 문서 설명
+ version = "0.0.1" // API 문서 버전
+ format = "yaml" // API 문서 출력 포맷 (default = JSON)
+ }
// 4. 불필요한 설정 제거 (1)
- ext {
- set('snippetsDir', file("build/generated-snippets"))
- }
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// 5. restdocs-api-spec restassured 관련 의존성 추가
- testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
+ testImplementation "com.epages:restdocs-api-spec-restassured:${restdocsApiSpecVersion}"
testImplementation 'io.rest-assured:rest-assured'
}
tasks.named('test') {
// 4. 불필요한 설정 제거 (2)
- outputs.dir snippetsDir
useJUnitPlatform()
}
// 4. 불필요한 설정 제거 (3)
- tasks.named('asciidoctor') {
- inputs.dir snippetsDir
- dependsOn test
- }
위와 같이 build.gradle에 관련 의존성을 추가해준다!
기존 API에 대한 테스트코드 작성
나는 테크루키 프로젝트에서 미리 구현한 댓글 조회 API에 대해 테스트코드를 작성해보았다!
CommentApi.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class CommentApi {
private final CommentCommandService commentCommandService;
private final CommentQueryService commentQueryService;
@GetMapping("/feeds/{feed-id}/comments")
public CommentSelectResponse getComments(@PathVariable("feed-id") Long feedId) {
return commentQueryService.getComments(feedId);
}
}
이제 위의 api에 대한 테스트 코드를 작성해보자!
1) BaseControllerTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(RestDocumentationExtension.class)
public abstract class BaseControllerTest {
protected static final String DEFAULT_RESTDOC_PATH = "{class_name}/{method_name}/";
protected RequestSpecification spec;
@LocalServerPort
int port;
// RestAssured 설정
@BeforeEach
void setUp() {
RestAssured.port = port;
}
// API 스펙 설정
@BeforeEach
void setUpRestDocs(RestDocumentationContextProvider provider) {
this.spec = new RequestSpecBuilder()
.setPort(port)
.addFilter(documentationConfiguration(provider)
.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint())
)
.build();
}
}
- RestAssured와 API 스펙 관련 설정 코드의 중복을 제거하기 위해 BaseControllerTest 라는 추상 클래스를 선언한다
2) CommentApiTest.java
class CommentApiTest extends BaseControllerTest {
@Test
@DisplayName("댓글 조회 테스트")
void getCommentsTest() {
CommentSelectResponse response = new CommentSelectResponse(1L,
List.of(CommentSingleDao.builder()
.email("wldnjs@ssg.com")
.content("댓글입니다")
.createdAt(LocalDateTime.now())
.build())
);
given(this.spec)
.filter(document(DEFAULT_RESTDOC_PATH,
pathParameters(
parameterWithName("feed-id").description("feed id")
),
responseFields(fieldWithPath("commentCount").type(JsonFieldType.NUMBER).description("댓글 개수"),
fieldWithPath("comments").type(JsonFieldType.ARRAY).description("댓글 목록"))
)
)
.accept(MediaType.APPLICATION_JSON_VALUE)
.header("Content-type", "application/json")
.log().all()
.when()
.get("/api/feeds/{feed-id}/comments", 1L)
.then();
}
}
- 추상 클래스 BaseControllerTest를 상속받아 테스트 코드를 작성한다
- RestAssured의 given-when-then 방식을 사용하여 테스트코드를 작성했다 :)
문서 생성 확인
문서 생성 확인을 위한 Gradle Task는 openapi3 이다
- openapi3는 Open API 3.0.1 문서를 yaml 포맷으로 build/api-spec 경로에 출력한다
- 그리고 openapi3는 check를 의존하도록 설정되어 있다
task 의존관계에 따라 openapi3 명령을 수행하면 compile → test → openapi3가 실행되며, 문서가 생성된다
- 명령어 : ./gradlew openapi3
- gradle task를 실행하면 위의 사진처럼 build/api-spec 에 openapi3.yaml 파일이 생성된 것을 볼 수 있다
SwaggerUI 연동 추가
+ import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
+ import org.springframework.boot.gradle.tasks.bundling.BootJar
buildscript {
ext {
restdocsApiSpecVersion = '0.16.2'
}
}
plugins {
id 'org.springframework.boot' version '2.7.2'
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
// 3.1 swagger generator 플러그인 추가
+ id 'org.hidetake.swagger.generator' version '2.18.2'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
repositories {
mavenCentral()
}
// 3.2. swaggerSources 설정 추가
+ swaggerSources {
+ sample {
+ setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml"))
+ }
+ }
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation "com.epages:restdocs-api-spec-restassured:${restdocsApiSpecVersion}"
testImplementation 'io.rest-assured:rest-assured'
// 3.3. Swagger 의존성 추가
+ swaggerUI 'org.webjars:swagger-ui:4.11.1'
}
tasks.named('test') {
useJUnitPlatform()
}
// 3.4. Task 및 설정 추가
// 3.4.1
+ // GenerateSwaggerUI 태스크가, openapi3 task 를 의존하도록 설정
+ tasks.withType(GenerateSwaggerUI) {
+ dependsOn 'openapi3'
+ }
+
// 3.4.2
+ // 생성된 SwaggerUI 를 jar 에 포함시키기 위해 build/resources 경로로 로 복사
+ tasks.register('copySwaggerUI', Copy) {
+ dependsOn 'generateSwaggerUISample'
+
+ def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get()
+
+ from("${generateSwaggerUISampleTask.outputDir}")
+ into("${project.buildDir}/resources/main/static/docs")
+ }
+
// 3.4.3
+ // bootJar 실행 전, copySwaggerUI 를 실행하도록 설정
+ tasks.withType(BootJar) {
+ dependsOn 'copySwaggerUI'
+ }
- 앞에서 생성한 openapi3.yaml 으로 SwaggerUI를 생성하기 위해 위의 의존성들을 build.gradle에 추가해준다
- 위의 의존성을 추가한 뒤, gradle build를 하면 build/swagger-ui-sample에 생성된 SwaggerUI가 build/resources/main/static/docs로 복사된다
SwaggerUI 확인
이후 bootRun 명령어를 통해 이전 단계에서 빌드된 jar 파일을 실행하면, http://localhost:8080/docs/index.html 에서 SwaggerUI를 확인할 수 있다
(나의 경우에는 build.gradle에서 설정한 index.html 파일 복사 경로에 /swagger를 추가해서 url path가 하나 더 추가된 것이다!)
적용 후기
이처럼 swaggerui와 openapi3를 이용하여 swagger와 rest docs의 장점을 모두 갖춘, 신뢰성 있고 테스트 가능한 api 문서화를 할 수 있었다!
생각보다 많이 사용되는 방법인 것 같고, 테스트 코드를 보다 정성스레(?) 짠다면 아주 효율적으로 사용할 수 있을 것 같다!
근데 생각보다 커스텀하는 방식이 어려웠던 것 같다
사실상 spring rest docs에 openapi3를 통해 swagger의 ui만 적용한 것이라 swagger 처럼 단순히 어노테이션만 붙여서 생성되는 것이 아니라 일일이 설정을 다 해줘야 한다는 게 꽤나 번거롭고, 어느 부분을 어떤 방식으로 바꿔야 적용되는지도 알기가 쉽지 않았따..😢
물론 내가 테스트코드 작성이 미숙해서 더욱 어렵게 느껴졌던 걸수도 있다
그래도 적용해봤다는 것에 아주 만족하고, 테스트코드와 문서화의 중요성을 다시 한번 느낄 수 있어서 너무 좋은 경험이었다!
나는 테스트 코드가 익숙한 사람들이라면 2가지 방식을 조합해서 사용하는 이 방식을 추천하고 싶다! 👍
🤔 테스트 코드를 작성해도 rest docs 문서가 생성되지 않는다?
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper
테스트 코드를 작성할 때 document() 함수는 위의 라이브러리를 import하여 사용해야 한다!
만약 테스트코드가 통과를 했는데도 문서가 생성되지 않는다면 임포트한 라이브러리를 다시 확인해보자 :)
참고)
https://taetaetae.github.io/posts/a-combination-of-swagger-and-spring-restdocs/
Swagger와 Spring Restdocs의 우아한 조합 (by OpenAPI Spec)
MSA 환경에서의 API 문서화는 어떤 식으로 구성하는 걸까? 예컨대, 모듈이 10개 있다고 하면 각 모듈마다 API 문서가 만들어질 테고 API 문서를 클라이언트에 제공하기 위해서 각각의 (10개의) URL를
taetaetae.github.io
https://www.youtube.com/watch?v=qguXHW0s8RY
https://jwkim96.tistory.com/274
SwaggerUI + Spring REST Docs 함께 사용하기(feat. Rest Assured)
이 글은 딥다이브한 내용을 쭉 풀어쓴 내용입니다. 시간이 없으신 분은 전체 소스코드를 참고해 주세요. 0. 시도하게된 이유 0.1 Swagger 경험 이전에 TODO List 라는 프로젝트에서 Swagger 를 사용해본
jwkim96.tistory.com
https://blog.naver.com/qjawnswkd/222340413113
Spring RestDocs 와 Swagger 같이 사용하기(OpenAPI Spec 사용)
Spring RestDocs 를 사용하면 테스트 코드를 통과해야지 문서가 생성되어서 좋고 테스트 코드에 작성할 ...
blog.naver.com
'야미로깅' 카테고리의 다른 글
IntelliJ Live Templates : 인텔리제이로 커스텀 템플릿(단축키) 만들어보기! (0) | 2023.05.06 |
---|---|
[Docker] 도커로 M1 Mac에 MYSQL 설치하기 🐳 (0) | 2023.03.22 |
[AWS] 도메인 HTTPS 설정 삽질 기록🪓 (0) | 2022.08.26 |
[OOP] 베이스볼 리팩토링 시 고민할 부분 (0) | 2022.08.22 |
[Git] 강제 Pull을 받는 방법 (0) | 2022.05.01 |
댓글