개발자로 후회없는 삶 살기

[테스트] Spring Rest Docs 셋팅 (ClassNotFoundException 에러 해결) 본문

[백엔드]/[spring | 학습기록]

[테스트] Spring Rest Docs 셋팅 (ClassNotFoundException 에러 해결)

몽이장쥰 2025. 1. 14. 16:01

서론

※ 아래 내용을 다룹니다.

  • 스프링 Rest Docs 셋팅
  • ClassNotFound 예외 해결

 

본론

- Rest Docs 사용 목적

API 명세 작성 > 스펙 작성 > 문서 작성

프론트엔드 개발자와 프로젝트를 진행할 때, API 명세를 작성하고 문서화하여 공유하는 것이 일반적이다. 이때 Rest Docs을 사용하면 개발이 완료되기 전에, 간단한 컨트롤러 테스트 코드로 API를 문서화할 수 있다. 또한 테스트가 성공해야만 문서가 만들어지므로, 테스트 코드 작성을 의무적으로 수행하기 위한 팀 규율로 사용할 수도 있다.

 

✅ 도메인 > 서비스 > 컨트롤러 순으로 개발을 하면 문서화 시점이 늦지 않을까?

실무에서는 API 명세에 맞게 컨트롤러의 요청과 응답, 빈 검증만 간단하게 개발하여 Rest Docs로 문서화하고, 도메인과 서비스를 진행하기도 한다.

 

-> 특징

1) 테스트 코드를 사용하여 문서를 만든다.
2) AsciiDoc 문법을 사용하여 문서를 작성한다.

 

-> 장점

1) 프로덕션 코드에 비침투적이라서 안전하다. 프로덕션 코드를 건드리지 않고 테스트 코드만 작성해서 문서를 만들 수 있다.
2) 테스트 코드가 성공해야 문서를 만들 수 있어서, 문서가 만들어졌다는 것은 실제로 그 문서대로 동작하고 있음을 보장한다.

 

-> 단점

코드가 길고, 설정이 어렵다.

 

- gradle 설정

1. asciidoctor plugins 설치

문서 조각을 HTML로 만들어주는 플러그인 asciidoctor

 

2. dependencies

// RestDocs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

 

3. 전역변수 설정

ext { // 전역 변수
    snippetsDir = file('build/generated-snippets')
}

문서화할 문서 조각을 스니펫이라고 한다. 스니펫이 저장될 경로를 전역으로 지정한다.

 

4. asciidoctor 시나리오

test {
    outputs.dir snippetsDir
}

asciidoctor {
    inputs.dir snippetsDir
    configurations 'asciidoctorExt'

    sources { // adoc이 여러개로 분리되어서 include할때, 특정 adoc 파일만 html로 만든다.
        include("**/index.adoc")
    }
    baseDirFollowsSourceFile() // 다른 adoc 파일을 include 할 때 경로를 baseDir로 맞춘다.
    dependsOn test
}

bootJar {
    dependsOn asciidoctor
    from("${asciidoctor.outputDir}") {
        into 'static/docs'
    }
}

테스트를 실행하면 스니펫이 스니펫 dir에 생성된다. asciidoctor로 문서 조각을 사용하여 html을 생성하고 jar에 그 문서를 담아서 배포한다.

 

5. asciidoc 플러그인 설치

인텔리제이 플러그인은 작성한 adoc 파일을 한눈에 보기 쉽게 만들어주니 설치하자.

 

- 테스트 코드 작성

@ExtendWith(RestDocumentationExtension.class)
public abstract class RestDocsSupport {

Rest Docs를 사용하려면 익스텐션이 필요하다.

 

@BeforeEach
void setUp(RestDocumentationContextProvider provider) {
    this.mockMvc = MockMvcBuilders.standaloneSetup(initController())
            .apply(documentationConfiguration(provider))
            .build();
}

@SpringBootTest를 사용하면, 테스트를 돌릴 때마다 스프링부트가 뜨고, 이는 테스트 비용이 커지는 것으로 이어지는 문제점이 있다. 문서를 하나 만드는 데 스프링 부트를 하나 띄우는 것은 올바르지 않다고 판단되어 스프링부트 없이 RestDocs를 사용하는 컨트롤러 단위 테스트를 설정하자. 스프링과 무관한 컨트롤러 단위 테스트라고 볼 수 있다.

 

https://hsb422.tistory.com/entry/%ED%85%8C%EC%8A%A4%ED%8A%B8-Mock

 

[테스트] 컨트롤러 테스트 하는 방법 (with Mockito)

서론※ 아래 내용을 다룹니다.프레젠테이션 레이어를 테스트하는 방법StubbingTest DoubleTest Fixture 클렌징 본론- 프레젠테이션 레이어의 역할외부 세계의 요청을 가장 먼저 받는 레이어로 응

hsb422.tistory.com

테스트 비용은 위 블로그에서 확인할 수 있다.

 

protected abstract Object initController();

standalone으로 스프링부트 없이 단위테스트를 할 땐, 테스트할 컨트롤러 타입을 지정해줘야 한다. 하지만 모든 테스트마다 지정하는 것은 번거로우므로, 추상 메서드를 만들고 하위 클래스에서 넣는 방식으로 한다.

 

@BeforeEach
void setUp(WebApplicationContext context, RestDocumentationContextProvider provider) {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
            .apply(MockMvcRestDocumentation.documentationConfiguration(provider))
            .build();
}

스프링 부트를 사용할 땐 WebApp 컨텍스트를 주입받아서 사용하면 된다.

 

-> 테스트 작성

mockMvc.perform(
                post("/api/v1/products/new")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(APPLICATION_JSON)
        )
        .andDo(print())
        .andExpect(status().isOk())
        .andDo(MockMvcRestDocumentation.document( // 하위에 작성

문서를 만들기 위한 체이닝을 넣어주면 된다. andDo의 document에 RestDocs 문서에 넣을 내용을 추출해야 한다.

 

✅ 어떤 것을 정의를 해야 할까?

Rest Docs는 API 문서라고 했다. 따라서 스니펫은 요청(쿼리 파라미터 등)과 응답에 대한 정의를 한다. 

 

requestFields(
        fieldWithPath("type").type(JsonFieldType.STRING)
                .description("상품 타입"),
        fieldWithPath("sellingStatus").type(JsonFieldType.STRING)
                .optional()
                .description("상품 판매상태"),
        fieldWithPath("name").type(JsonFieldType.STRING)
                .description("상품 이름"),
        fieldWithPath("price").type(JsonFieldType.NUMBER)
                .description("상품 가격")
),
responseFields(
        fieldWithPath("code").type(JsonFieldType.NUMBER)
                .description("코드"),
        fieldWithPath("httpStatus").type(JsonFieldType.STRING)
                .description("상태"),
        fieldWithPath("message").type(JsonFieldType.STRING)
                .description("메시지"),
        fieldWithPath("data").type(JsonFieldType.OBJECT)
                .description("데이터"),
        fieldWithPath("data.id").type(JsonFieldType.NUMBER)
                .description("상품 ID"),
        fieldWithPath("data.productNumber").type(JsonFieldType.STRING)
                .description("상품 번호"),
        fieldWithPath("data.type").type(JsonFieldType.STRING)
                .description("상품 타입"),
        fieldWithPath("data.sellingStatus").type(JsonFieldType.STRING)
                .description("상품 판매상태"),
        fieldWithPath("data.name").type(JsonFieldType.STRING)
                .description("상품 이름"),
        fieldWithPath("data.price").type(JsonFieldType.NUMBER)
                .description("상품 가격")

)

요청은 request dto의 필드의 타입, 그에 대한 설명을 모든 필드에 대해 넣어준다. response도 동일하다.

 

테스트를 실행하거나 gradle의 asciidoctor를 실행하면 andDo의 키 이름으로 폴더가 생기고, 문서 조각이 생성된다.

 

-> index.adoc으로 문서 만들기

gradle에 정의한 대로 index.adoc를 사용하여 html을 만들기 위한 내용을 작성한다. 상위 if 문은 생성된 문서 조각의 경로를 지정하는데, index.adoc 파일에 문서 조각을 가져와서 html을 만들 것이다.

 

src/docs/asciidoc에 index.adoc 파일을 만들고 asciidoctor를 실행하면 build/docs/asciidoc/index.html이 생성된다.

 

-> 전/후처리

rest docs의 특징은 커스터마이징이다.

 

1. JSON Pretty

preprocessRequest(prettyPrint())
preprocessResponse(prettyPrint())

json pretty를 지정할 수 있다.

 

2. Optional 지정

테스트 코드의 Optional을 원하는 field에 설정하면

 

컬럼에 추가할 수 있다.

 

3. index.adoc 분리

index.adoc을 통해 html을 만들게 되는데, API가 많아질수록 adoc도 길어져서, 한눈에 보기 어렵고 유지보수 및 수정이 어려워진다. 따라서 여러 개의 adoc으로 분리하고 build 할 때 하나의 index.html로 모을 수 있다.

 

✅ 문서를 공유하는 방법

bootJar {
    dependsOn asciidoctor
    from("${asciidoctor.outputDir}") {
        into 'static/docs'
    }
}

이렇게 만들어진 문서를 팀원에게 공유하기 위해선 jar 파일 내부에 포함시켜서 localhost:8080/docs/index.html로 공유할 수 있다. 이것은 팀원들끼리 룰로 정하면 될 것이다.

 

🚨 ClassNotFound Test Build Fail 오류 해결

asciidoc build는 반드시 프로젝트 경로에 한글이 있으면 안 된다. 평소에도 바탕하면 하위에 스프링 프로젝트를 설치하여 테스트를 진행해 와서 한글 문제는 아닐 거라고 생각했지만, 스프링 프로젝트 전체 경로에서 한글을 제거하니 asciidoc build를 성공했다.
Comments