📚 Spring REST Docs 란?
Spring Rest Docs는 테스트 코드를 기반으로 자동으로 API 문서를 작성할 수 있게 도와주는 프레임워크이다.
테스트 기반으로 동작하기 때문에 반드시 Test가 통과되어야 문서가 작성된다는 장점이 있다.
📚 Swagger VS REST Docs
자바 문서 자동화에는 주로 Swagger 와 Spring REST Docs이 사용된다.
각 각의 장단점을 살펴보자.
- Swagger
+ API를 테스트 할 수 있는 화면을 제공한다.
+ 적용이 쉽다.
- 어노테이션을 통해 명세를 작성하기 때문에 양이 많아질수록 가독성이 떨어진다.
- 테스트 기반이 아니기에 문서가 100% 정확하지 않을 수 있다. - REST Docs
+ 제품코드에 영향이 없다.
+ 테스트를 성공해야 문서가 만들어진다.
- 적용이 어렵다.
📚 프로젝트 구성
간략하게 개념 및 Swagger와의 차이점을 알아보았다. 지금부터 실제 스프링 프로젝트에 적용시키는 방법에 대해서 알아보자.
우선 전체적인 프로젝트 구성은 다음과 같다.
프로젝트 트리를 구성하고 있는 파일들을 하나씩 알아가 보자.
📚 비즈니스 로직
해당 프로젝트는 '사용자 전체 조회'와 '특정 사용자 조회'를 할 수 있는 단순 기능들로 구성되어 있다.
비즈니스 로직은 최대한 간단하게 구현하였으므로 빠르게 살펴보고 넘어가도록 하겠다.
- Member.java
사용자 도메인 객체로 아이디, 이름, 나이를 가지고 있다.
package app.spring.domain;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
private String name;
private int age;
@Builder
private Member(Long memberId, String name, int age) {
this.memberId = memberId;
this.name = name;
this.age = age;
}
}
- ResponseData.java
응답 객체로 일관된 포맷으로 응답하기 위하여 사용된다. 응답 코드, 응답 메시지, 응답 데이터 항목으로 구성되어 있다.
package app.spring.domain;
import lombok.Builder;
import lombok.Getter;
@Getter
public class ResponseData {
String resultCode;
String resultMessage;
Object resultData;
@Builder
public ResponseData(String resultCode, String resultMessage, Object resultData) {
this.resultCode = resultCode;
this.resultMessage = resultMessage;
this.resultData = resultData;
}
}
- MemberController.java
'사용자 전체 조회'와 '특정 사용자 조회' 기능을 가지고 있는 컨트롤러이다.
package app.spring.controller;
import app.spring.domain.ResponseData;
import app.spring.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
/**
* 회원 목록 조회
*/
@GetMapping("/members")
public ResponseEntity findMembers() {
ResponseData responseData = ResponseData.builder()
.resultCode("0000")
.resultMessage("SUCCESS")
.resultData(memberService.findAllMember())
.build();
return ResponseEntity
.ok()
.body(responseData);
}
/**
* 회원 조회
*/
@GetMapping("/members/{memberId}")
public ResponseEntity findMember(@PathVariable Long memberId) {
ResponseData responseData = ResponseData.builder()
.resultCode("0000")
.resultMessage("SUCCESS")
.resultData(memberService.findMember(memberId))
.build();
return ResponseEntity
.ok()
.body(responseData);
}
}
- MemberService.java
사용자 등록, 전체 조회, 단건 조회 기능이 있는 서비스단의 로직이다.
package app.spring.service;
import app.spring.domain.Member;
import app.spring.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
/**
* 회원 등록
*/
public Long saveMember(Member member) {
return memberRepository.save(member).getMemberId();
}
/**
* 회원 전체 조회
*/
public List<Member> findAllMember() {
return memberRepository.findAll();
}
/**
* 회원 조회
*/
public Member findMember(Long memberId) {
return memberRepository.findByMemberId(memberId);
}
}
- MemberRepository.java
DB에 액세스 할 수 있는 레포지토리이며, JPA를 사용한다.
package app.spring.repository;
import app.spring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByMemberId(Long memberId);
}
- application.yml
DB 및 로그와 관련된 설정을 명시한다.
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/test
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
logging.level:
org.hibernate.SQL: debug
📚 REST Docs 적용
이제 프로젝트의 기본 구성에 대한 설명은 마치고, 이를 기반으로 REST Docs을 적용시켜 보자.
- build.gradle
Dependency 및 설정 정보를 추가해 보자.
plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
// REST Docs : Asciidoctor 플러그인 적용
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
group 'app.spring'
version '1.0-SNAPSHOT'
configurations {
asciidoctorExtensions // REST Docs : dependencies 에서 적용한 것 추가
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// Spring Basic
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// jpa
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// database
runtimeOnly 'com.h2database:h2'
// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// REST Docs
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
ext {
// REST Docs : 아래에서 사용할 변수 선언
snippetsDir = file('build/generated-snippets')
}
test {
useJUnitPlatform()
// REST Docs : 위에서 작성한 snippetsDir 디렉토리를 test의 output으로 구성하는 설정 -> 스니펫 조각들이 build/generated-snippets로 출력
outputs.dir snippetsDir
}
// REST Docs 관련 설정 START
asciidoctor {
dependsOn test // test 작업 이후에 동작하도록 설정
configurations 'asciidoctorExtensions' // 위에서 작성한 configuration 적용
inputs.dir snippetsDir // snippetsDir를 입력으로 구성
}
asciidoctor.doFirst {
// static/docs 폴더 비우기
delete file('src/main/resources/static/docs')
}
// asccidoctor 작업 이후 생성된 HTML 파일을 static/docs 로 copy
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
// REST Docs 관련 설정 END
- MemberRestDocs.java
위에서 설명하였듯이 Spring REST Docs은 테스트 코드 기반으로 작성된다.
때문에 문서를 만들기 위해서는 테스트코드를 작성하는 것이 중요하다.
MemberController.java 에서 보았듯이 해당 프로젝트는 '사용자 전체 조회'와 '특정 사용자 조회' 기능만 있다고 가정하에
두 가지 기능에 대한 API 문서를 제공하는 것으로 가정하겠다.
package app.spring.restdocs;
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.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Description;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
class MemberRestDocs {
@Autowired
private MockMvc mockMvc;
@Test
@Description("회원 목록 조회")
public void findAll() throws Exception {
this.mockMvc.perform(get("/members").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("find-members",
responseFields(
fieldWithPath("resultCode").type(JsonFieldType.STRING).description("응답 코드"),
fieldWithPath("resultMessage").type(JsonFieldType.STRING).description("응답 메시지"),
fieldWithPath("resultData").type(JsonFieldType.ARRAY).description("응답 데이터"),
fieldWithPath("resultData.[].memberId").type(JsonFieldType.NUMBER).description("사용자 ID"),
fieldWithPath("resultData.[].name").type(JsonFieldType.STRING).description("사용자 이름"),
fieldWithPath("resultData.[].age").type(JsonFieldType.NUMBER).description("사용자 나이")
)
));
}
@Test
@Description("회원 조회")
public void findByMemberId() throws Exception {
this.mockMvc.perform(get("/members/40").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("find-member",
responseFields(
fieldWithPath("resultCode").type(JsonFieldType.STRING).description("응답 코드"),
fieldWithPath("resultMessage").type(JsonFieldType.STRING).description("응답 메시지"),
fieldWithPath("resultData").type(JsonFieldType.OBJECT).description("응답 데이터"),
fieldWithPath("resultData.memberId").type(JsonFieldType.NUMBER).description("사용자 ID"),
fieldWithPath("resultData.name").type(JsonFieldType.STRING).description("사용자 이름"),
fieldWithPath("resultData.age").type(JsonFieldType.NUMBER).description("사용자 나이")
)
));
}
}
📚 Snippets 생성
이제 설정과 테스트코드 작성이 모두 끝났으므로 문서를 만들 수 있게 되었다.
Gradle 탭 > Tasks > documentation > asciidoctor를 클릭해 보자.
실행이 완료되었다면 프로젝트가 있는 디렉터리로 접근해 보자.
아래와 같이 build/generated-snippets 경로 하위에 테스트 코드로 작성한 find-member와 find-members 디렉터리가 생성되었으며 그 밑으로 adoc 문서가 만들어진 것을 확인할 수 있을 것이다.
📚 API 문서 생성
이제 만들어진 adoc 문서들을 토대로 조합하여 실제 API 문서를 만들 수 있게 되었다.
API 문서의 틀을 만들어야 한다.
바로 그 틀이 프로젝트 트리에 있었던 파일 중 'index.adoc'이라는 파일이다.
= REST Docs 문서 만들기
사용자 조회
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
:snippets: ./build/generated-snippets
[[Member-API]]
== Member API
[[Member-전체-조회]]
=== Member 전체 조회
==== 요청
include::{snippets}/find-members/http-request.adoc[]
==== 응답
include::{snippets}/find-members/http-response.adoc[]
==== 응답 필드
include::{snippets}/find-members/response-fields.adoc[]
[[Member-단일-조회]]
=== Member 단일 조회
==== 요청
include::{snippets}/find-member/http-request.adoc[]
==== 응답
include::{snippets}/find-member/http-response.adoc[]
==== 응답 필드
include::{snippets}/find-member/response-fields.adoc[]
index.adoc 파일까지 작성이 완료되었다면, 이번에는 Gradle을 빌드해 보자.
빌드가 완료되었다면, 'build.gradle'에서 명시해 준 'src/main/resources/static/docs' 경로에 index.html 파일이 만들어진 것을 확인할 수 있을 것이다.
완성된 API 문서를 열어보면 테스트 코드에서 작성했던 항목들이 명시되어 있는 것을 확인할 수 있다.
References.
1. backtony - Spring REST Docs 적용 및 최적화 하기
2. 컬리 기술블로그
3. 우아한형제들 기술블로그
'Spring' 카테고리의 다른 글
[SpringBoot] 모니터링 환경 구축 #1 - Spring Actuator (0) | 2023.02.26 |
---|---|
[SpringBoot] Bucket4j를 이용한 트래픽 제한 (0) | 2023.02.23 |
[Security] JWT 구현 (0) | 2023.02.16 |
[Security] JWT 소개 (0) | 2023.02.12 |
[Spring] 스프링 AOP (0) | 2023.02.07 |