
팀원들과 Rest Docs와 Swagger 중 무엇을 고민할지 상의했다.
결과적으로는 나의 생각과는 다르게 Rest Docs를 선택했다.
Swagger도 튜토리얼 정도만 해봤기 때문에 둘의 장단점을 명확하게 공감하지는 못한다.
그래서 이번에는 팀원들의 의견을 따라 Rest Docs를 사용하며, 장단점을 느껴보자.

하지만 팀원 분은 프로덕션 코드에 의존되는 것 자체가 가독성이 좋지 않다고 했고,
@ApiModel을 사용해서 추상화를 한다고 해도 결국 추상화를 위한 클래스가 필요하기 때문에 별도로 클래스를 관리하는 것이
개발할 때 찾아가기 불편하다고 했다. 하지만 생각해보니 이 부분은 Rest Docs도 똑같은 것 같다.
결국 Rest Docs도 깔끔하게 하려면 추상화가 필요하다. 그래서 아직까지는 Swagger가 좋지 않을까 생각하고 있다.

우리 프로젝트는 문서화 도구로 Markdown이 아닌 AsciiDoc을 사용했다.
ATDD 교육 과정에서 사용했었는데 이유는 include의 여부이다.
Markdown은 문법이 굉장이 편하다. 그리고 많이 사용하는 문법이다.
반면 AsciiDoc은 문법은 조금 불편하지만 include가 가능하기 때문에 html을 작성하는 것처럼
재활용이 가능하다. 그래서 AsciiDoc을 채택하게 되었다.
테스트에서 공통으로 사용될 셋팅을 만든다.
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.request.ParameterDescriptor;
import org.springframework.test.context.ActiveProfiles;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.document;
import static org.springframework.restdocs.restassured3.RestAssuredRestDocumentation.documentationConfiguration;
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(RestDocumentationExtension.class)
public class Documentation {
@LocalServerPort
int port;
protected RequestSpecification spec;
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
RestAssured.port = port;
this.spec = new RequestSpecBuilder()
.addFilter(documentationConfiguration(restDocumentation))
.build();
}
RequestSpecification given(String identifier, ParameterDescriptor[] parameterDescriptors, FieldDescriptor[] fieldDescriptors) {
return RestAssured.given(this.spec).log().all()
.filter(document(identifier, preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
requestParameters(parameterDescriptors),
relaxedResponseFields(fieldDescriptors)
)
);
}
}
protected RequestSpecification specRest Assured를 사용하기 때문에 사용한다.Rest Assured의 given() 메서드의 리턴 타입이 RequestSpecification이다.RestDocumentationContextProvider, RestDocumentationExtension.classRestDocumentationContextProvider의 구현체로 JUnitRestDocumentation을 제공한다.@BeforeEach에서 주입이 필요하다.document@ExtendWith(RestDocumentationExtension.class)rest docs를 확장하여 조금 더 편리하게 사용하기 위해 테스트에서 공통으로 사용할 기능을 지원한다.
mockito로 예를 들어보자면 아래와 같다.
공통으로 사용하도록 @Mock을 사용하여 공통화할 수 있다.
@ExtendWith(MockitoExtension.class)
public class MockitoExtensionTest {
@Mock
private LineRepository lineRepository;
@Mock
private StationService stationService;
@Test
void findAllLines() {
// given
when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
LineService lineService = new LineService(lineRepository, stationService);
...
}
공통화를 못하고 각 테스트마다 사용해야 한다.
@DisplayName("단위 테스트 - mockito를 활용한 가짜 협력 객체 사용")
public class MockitoTest {
@Test
void findAllLines() {
// given
LineRepository lineRepository = mock(LineRepository.class);
StationService stationService = mock(StationService.class);
when(lineRepository.findAll()).thenReturn(Lists.newArrayList(new Line()));
LineService lineService = new LineService(lineRepository, stationService);
...
}
build.gradle 셋팅
plugins {
id 'org.springframework.boot' version '2.6.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "org.asciidoctor.jvm.convert" version "3.3.2"
id 'java'
}
configurations {
asciidoctorExt
}
group = 'project'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// handlebars
implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0'
// rest docs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.5.RELEASE'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured:2.0.5.RELEASE'
testImplementation 'io.rest-assured:rest-assured:3.3.0'
}
// snippets 파일이 저장될 경로 변수 설정
ext {
snippetsDir = file('build/generated-snippets')
}
// 테스트 실행 시 snippets를 저장할 경로로 snippetsDir 사용
test {
useJUnitPlatform()
}
task testDocument(type: Test) {
useJUnitPlatform()
filter {
includeTestsMatching "*.documentation.*"
}
}
// API 문서 생성
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
dependsOn testDocument
}
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
task : gradle을 통해 실행되는 단위
ext : 전역 변수 셋팅
asciidoctor : testDocument를 의존하여 테스트를 실행하고, snippetsDir에서 snippets을 참조하여
문서를 생성한다.
bootJar : jar 빌드 시 asciidoctor를 참조하여 문서를 생성하고, snippetsDir에 있는 html5파일을
static/docs로 복사한다. 복사하는 이유는 api 요청으로 문서 접근을 위함
copyDocument : from 디렉토리에 있는 API 문서 파일을 into로 복사한다.
복사하는 이유는 api 요청으로 문서 접근을 위함. 테스트 시에는 이걸 쓰고, 배포 시에는 bootjar를 씀.
testDocument : restdocs 전용 테스트 실행 task로 **.documnetation.*에 해당하는 테스트만 실행한다.*
includeTestMatching : 실행할 테스트 패턴 정의
- 우리는 Documentation이라는 RestDocs 전용 부모 클래스를 상속받아 사용하기 때문에
**.documnetation.*로 지정*
- https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/TestFilter.html
테스트(testDocument)를 수행시켜 snippet을 생성한다.
build.gradle에서 설정한 snippetsDir에 생성 됨.gradle로 asciidoctor task를 수행시켜 문서 파일을 생성
src/docs/asciidoc/index.adoc 있어야 함.asciidoctor가 testDocument를 의존하기 때문에 asciidoctor를 바로 실행해도 됨.build > asciidoc > html5 > index.html에 문서가 생성된 것을 오픈하여 잘 만들어졌는지 확인한다.
task copyDocument를 실행하여 배포할 디렉토리도 복사하기