plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
도입하게된 이유
테스트코드 작성을 하고 결과물을 통해 문서작성을 한번에 할 수 있다는 아주 큰 장점이 도입을 하게된 계기다.
적극적으로 테스트코드를 활용하면 좋겠다 생각하던 찰나에 컨퍼런스 및 서칭을 통해 아주 적합하다고 판단했다.
그 동안 안일했던 생각들
- 컨트롤러에서는 크게 예외가 발생하지 않겠지
- 예외가 발생해도 금방 고치면 되지 않아??
- Swagger 로 api 잘 돌아가는지 테스트 하면 되잖아~
- 문서화? swagger 면 쉽고 예쁘게 만들 수 있자나
위와 같은 생각을 갖고 개발을 진행할 때 api 문서화는 지금까지 swagger 로 작성하였다. 하지만, 결국 다음과 같은 이슈들은 계속 반복될 수 밖에 없었다.
- 코드에 덕지덕지 붙는 데코레이터로 코드 가독성 저하
- 데코레이터 누락 발생으로 인한 문서 최신화 안됨
- 검증되지 않은 api 의 문서화
버그가 나올 수 밖에 없다.
현재 사용중인 환경
Java 21, Gradle 8.10.2
Gradle Setting
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id "org.asciidoctor.jvm.convert" version "3.3.2" // Asciidoc 플러그인 설치
}
configurations {
asciidoctorExt // Asciidoc 확장자 추가
}
dependencies {
//빌드 되면
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
//restdoc 생성용 디펜던시
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
ext {
snippetsDir = file('build/generated-snippets') // snippets 생성 위치
}
tasks.named('test') {
useJUnitPlatform()
outputs.dir snippetsDir // 테스트에 대한 출력이 snipptesDir에 저장되도록 설정
}
asciidoctor {
configurations 'asciidoctorExt'
baseDirFollowsSourceFile()
inputs.dir snippetsDir
dependsOn test // 테스트가 수행될 때 동작하도록 추가
}
asciidoctor.doFirst {
delete file('src/main/resources/static/docs') // 먼저 docs 안에 파일을 삭제한다.
}
tasks.register('copyDocument', Copy) { // index.html을 base asciidocs 으로부터 생성해준다.
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
bootJar {
dependsOn copyDocument //build 과정에서 copyDocument 가 수행되도록 추가
}
...
위와 같이 설정을 진행해주고 테스트 코드를 작성하여 파일이 제대로 생성되는지 확인해 봅시다.
구색을 갖추기위해 간단하게 Company 를 생성하는 예제를 통해 진행해보도록 하겠습니다.
@Builder
public record CompanyCreateRequest(
@NotEmpty(message = "companyName is required") String companyName,
@NotEmpty(message = "address is required") String address) {
}
@Getter
@Builder
public class CompanyCreateResponse {
private Long id;
private String companyName;
private String companyAddress;
public static CompanyCreateResponse from(Company company) {
return CompanyCreateResponse.builder()
.id(company.getId())
.companyName(company.getName())
.companyAddress(company.getAddress())
.build();
}
}
.... import 구문
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/company")
public class CompanyController {
private final CompanyService companyService;
@PostMapping("")
public ResponseEntity<CompanyCreateResponse> createCompany(
@RequestBody
@Validated CompanyCreateRequest request) {
Company createdCompany = companyService.createCompany(Company.create(request));
return ResponseEntity.status(HttpStatus.CREATED)
.body(CompanyCreateResponse.from(createdCompany));
}
}
위와 같은 객체가 있고 요청의 흐름에 대해 테스트코드를 작성해 보겠습니다.
컨트롤러 테스트 코드
... import
//static import
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureRestDocs
@SpringBootTest
@AutoConfigureMockMvc
class CompanyControllerTest {
@Autowired
private MockMvc mockMvc;
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
@Transactional
public void companyCreateRequest() throws Exception{
//given
CompanyCreateRequest request = CompanyCreateRequest
.builder()
.companyName("회사이름")
.address("서울특별시")
.build();
//when
ResultActions preform = mockMvc.perform(
post("/api/company")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)));
//then
preform.andExpect(status().isCreated())
.andExpect(jsonPath("$.companyName").value("회사이름"))
.andExpect(jsonPath("$.companyAddress").value("서울특별시"))
.andExpect(jsonPath("$.id").value(1));
//docs
preform.andDo(document("company/create-company", //생성될 문서의 path
requestFields(//요청할 때 사용하는 필드명
fieldWithPath("companyName")
.description("회사명"),
fieldWithPath("address")
.description("주소")
),
responseFields(//응답 받을 때 사용하는 필드명
fieldWithPath("id")
.description("생성된 키"),
fieldWithPath("companyAddress")
.description("주소지"),
fieldWithPath("companyName")
.description("회사 이름")
)
));
}
}
응답에 대해 제대로 요청 받았는지 테스트하고 그리고 올바른 requestField, responseField 들이 입력되어 있는지 체크가 되면 테스트가 통과하고 문서가 생성됩니다.
위 생성된 adoc 파일들은 자동으로 사용될 수 있도록 문서화가 되는게 아닌 적절하게 조합하여 문서를 만들어주어야 됩니다.
= Product
:toc: left
:source-highlighter: highlightjs
:sectlinks:
== Company
=== Create
include::{snippets}/company/create-company/http-request.adoc[]
==== Request payload
include::{snippets}/company/create-company/request-fields.adoc[]
==== Response payload
include::{snippets}/company/create-company/response-fields.adoc[]
=== 응답
include::{snippets}/company/create-company/http-response.adoc[]
index.adoc 파일이며 src/docs/asciidoc/index.adoc 으로 위치해 있고 snippets 들을 참조하여 생성하면 됩니다.
서버를 기동한 후 다음으로 접근하면 생성된 index.html 을 볼 수 있습니다.
http://localhost:8080/docs/index.html#_company
이렇게 해도 무언가 탐탁지 않다.
이유는 다음과 같다.
- 빌드 후 추가되는 adoc 파일들을 index.adoc 에 수동으로 추가해야된다.
- api 개수가 많아지면 정리가 안될 것 같다.
- 디자인을 한땀한땀 해줘야 된다.
그래서 swaager 와 restdoc 을 결합할 수 있는 방법이 있다는 아주 좋은 라이브러리들이 있어 결과물을 swagger-ui 로 표현하여
swagger 의 장점인 깔끔한 디자인 + 바로 테스트할 수 있는 기능
restdoc 의 장점인 테스트코드 작성 및 코드와 문서의 분리
를 통해 ui 를 구성해보는걸 다음 포스팅에서 진행하도록 할게요!
감사합니다.
'Java & Spring' 카테고리의 다른 글
Awaitility로 비동기 이벤트 테스트 하기: Spring @Async와 함께 쓰는 법 (3) | 2025.07.29 |
---|---|
Java Enum 다형성으로 Notification 처리 리팩토링하기 - 조건문 없는 전략 설계 (1) | 2025.07.26 |
Spring Cloud Config 도입기: 구성부터 실시간 설정 반영까지 (3) | 2025.07.24 |
Spring Rest Docs 적용 3 - Controller Test 작성을 통한 문서화 (2) | 2025.07.22 |
Spring Rest Docs 적용 2 - Swagger 연동 (1) | 2025.07.21 |