Spring Rest Docs 적용

2024. 10. 17. 21:13·Java & Spring
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"

}

도입하게된 이유

테스트코드 작성을 하고 결과물을 통해 문서작성을 한번에 할 수 있다는 아주 큰 장점이 도입을 하게된 계기다.

 

적극적으로 테스트코드를 활용하면 좋겠다 생각하던 찰나에 컨퍼런스 및 서칭을 통해 아주 적합하다고 판단했다.

 

그 동안 안일했던 생각들

  1. 컨트롤러에서는 크게 예외가 발생하지 않겠지
  2. 예외가 발생해도 금방 고치면 되지 않아?? 
  3. Swagger 로 api 잘 돌아가는지 테스트 하면 되잖아~
  4. 문서화? swagger 면 쉽고 예쁘게 만들 수 있자나

위와 같은 생각을 갖고 개발을 진행할 때 api 문서화는 지금까지 swagger 로 작성하였다. 하지만, 결국 다음과 같은 이슈들은 계속 반복될 수 밖에 없었다.

 

  1. 코드에 덕지덕지 붙는 데코레이터로 코드 가독성 저하
  2. 데코레이터 누락 발생으로 인한 문서 최신화 안됨
  3. 검증되지 않은 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 들이 입력되어 있는지 체크가 되면 테스트가 통과하고 문서가 생성됩니다.

 

생성된 request, response 그리고 요청에 대한 adoc 파일들

 

 

위 생성된 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.html

 

 

 

이렇게 해도 무언가 탐탁지 않다. 

 

이유는 다음과 같다.

  1. 빌드 후 추가되는 adoc 파일들을 index.adoc 에 수동으로 추가해야된다.
  2. api 개수가 많아지면 정리가 안될 것 같다.
  3. 디자인을 한땀한땀 해줘야 된다.

 

그래서 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
'Java & Spring' 카테고리의 다른 글
  • Java Enum 다형성으로 Notification 처리 리팩토링하기 - 조건문 없는 전략 설계
  • Spring Cloud Config 도입기: 구성부터 실시간 설정 반영까지
  • Spring Rest Docs 적용 3 - Controller Test 작성을 통한 문서화
  • Spring Rest Docs 적용 2 - Swagger 연동
jaess
jaess
jaess 님의 블로그 입니다.
  • jaess
    개발하는 개발자
    jaess
  • 전체
    오늘
    어제
    • 분류 전체보기 (9)
      • 회고 (1)
      • API 설계 (1)
      • 생각정리 (0)
      • Java & Spring (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    springboot
    비동기 메시징
    TransactionalEventListener
    코드리팩토링
    RestDoc
    Spring Cloud Config Client
    REST API
    백엔드개발
    Awaitility
    Spring Event
    ifelse지옥탈출
    java
    Spring Actuator
    이벤트 리스너
    무중단설정변경
    비동기이벤트
    junit5
    Spring cloud
    Spring Cloud Config Server
    스프링이벤트
    개발자
    분기처리개선
    CleanCode
    Spring boot
    Spring
    @Async
    OOP (Object Oriented Programming)
    비동기 테스트
    Git 설정 관리
    Spring Cloud Config
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
jaess
Spring Rest Docs 적용
상단으로

티스토리툴바