spring / / 2022. 12. 31. 20:34

Spring REST docs 만들기

Spring REST docs 작성법

Spring으로 작성된 코드를 아래형식으로 문서화 파일을 만들어보자.

image-20221231200951820

소개

Spring REST docs는 restful 서비스의 문서를 정교하고 읽기 쉽게 하는데 도움을 주는데 목적이 있다.
기본적으로 asciidoctor를 사용하지만 markdown으로 구성을 할 수도 있다.
Spring MVC의 테스트 프레임워크로 작성된 테스트케이스로 스니펫(snippets)을 사용한다.

Spring의 기본적인 설명을 하면 내용이 너무 길어지므로 REST docs 작성을 위한 내용이 아니면 제외하였다.

필요사항

  • Java 8
  • Spring Framework 5

빌드 환경 구성

빌드환경은 maven으로 구성을 해보겠다.

gradle로 구성할 경우 spring 공식문서를 참조하길 바란다.

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started-build-configuration

[pom.xml]

  <dependencies> 
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.7.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.restdocs</groupId>
            <artifactId>spring-restdocs-mockmvc</artifactId>
            <version>2.0.5.RELEASE</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.7.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin> 
                <groupId>org.asciidoctor</groupId>
                <artifactId>asciidoctor-maven-plugin</artifactId>
                <version>2.2.2</version>

                <executions>
                    <execution>
                        <id>generate-docs</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>process-asciidoc</goal>
                        </goals>
                        <configuration>
                            <backend>html</backend>
                            <doctype>book</doctype>
                            <attributes>
                                <snippets>${project.build.directory}/generated-snippets</snippets>
                            </attributes>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
  • spring-boot-starter-web는 spring controller를 만들기 위한 의존성으로 사용된다.
  • spring-restdocs-mockmvc는 restdocs를 작성하기 위한 것

서비스 개발

Spring REST docs를 사용하기 위해서는 controller서비스가 필요하다. 그래서 테스트 서비스를 만들어 볼 것이다.
사용예제는 사용자(user) 서비스를 만들어서 CRUD에 해당되는 서비스를 restdocs로 만드는 예제로 사용하겠다.

api

  • 리스트: GET /users
  • 조회: GET /users/{userId}
  • 등록: POST /users
  • 수정: PUT /users/{userId}
  • 삭제: DELETE /users/{userId}

service 작성

@Service
public class UserService {

    public List<UserRdo> findList() {
        return Arrays.asList(UserRdo.sample());
    }

    public UserRdo find(String userId) {
        return UserRdo.sample();
    }

    public void register(UserCdo userCdo) {
        // userStore.register();
    }

    public void modify(String userId, UserUdo userUdo) {
        // userStore.modify();
    }

    public void remove(String userId) {
        // userStore.remove();
    }
}

UserService.java이다.

실제 서비스에서 DB에 저장하는 부분은 제외하였다. userStore를 통해 RDB나 NoSQL에 저장하는 부분을 구현하면 된다.

UserRdo, UserCdo, UserUdo는 데이타를 직접적으로 전달하는 서비스 객체이다.

UserCdo.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UserCdo {

    private String userId;

    private String name;

    private int age;

    private String description;
}

UserRdo.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UserRdo {

    private String userId;

    private String name;

    private int age;

    private String description;

    public static UserRdo sample() {
        return new UserRdo("hong", "홍길동", 20, "설명내용...");
    }
}

UserUdo.java

@NoArgsConstructor
@Getter
public class UserUdo {

    private String name;

    private int age;

    private String description;
}

controller 작성

위에서 정의한 API를 기반으로 UserController를 작성해 보겠다.

@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping // 리스트
    public List<UserRdo> findList() {
        return userService.findList();
    }

    @GetMapping("{userId}") // 조회
    public UserRdo find(@PathVariable String userId) {
        return userService.find(userId);
    }

    @PostMapping // 등록
    public void register(@RequestBody UserCdo userCdo) {
        userService.register(userCdo);
    }

    @PutMapping("{userId}") // 수정
    public void modify(@PathVariable String userId, @RequestBody UserUdo userUdo) {
        userService.modify(userId, userUdo);
    }

    @DeleteMapping("{userId}") // 삭제
    public void remove(@PathVariable String userId) {
        userService.remove(userId);
    }
}

테스트 구현

서비스 작성이 끝나면 해당 서비스를 문서화하기 위한 테스트 케이스를 만들어 보겠다.

UserControllerTest.java

@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
public class UserControllerTest {
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @BeforeEach
    public void setUp(
        WebApplicationContext webApplicationContext,
        RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }

    @Test
    public void findList() throws Exception {
        when(userService.findList())
                .thenReturn(Arrays.asList(UserRdo.sample()));

        this.mockMvc.perform(
                get("/users")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(document("findList",
                        responseFields(
                                fieldWithPath("[].userId").description("아이디"),
                                fieldWithPath("[].name").description("이름"),
                                fieldWithPath("[].age").description("나이"),
                                fieldWithPath("[].description").description("설명")
                        )
                ));
    }

    @Test
    public void find() throws Exception {
        when(userService.find(anyString()))
                .thenReturn(UserRdo.sample());

        this.mockMvc.perform(
                get("/users/{userId}", "hong")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(document("find",
                        pathParameters(
                                parameterWithName("userId").description("아이디")
                        ),
                        responseFields(
                                fieldWithPath("userId").description("아이디"),
                                fieldWithPath("name").description("이름"),
                                fieldWithPath("age").description("나이"),
                                fieldWithPath("description").description("설명")
                        )
                ));
    }

    @Test
    public void register() throws Exception {
        this.mockMvc.perform(
                post("/users")
                        .content("{\"userId\": \"hong\", \n\"name\": \"홍길동\", \n\"age\": 20, \n\"description\": \"설명\" }")
                        .contentType(MediaType.APPLICATION_JSON_VALUE)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(document("register",
                        requestFields(
                                fieldWithPath("userId").type(JsonFieldType.STRING).description("아이디"),
                                fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                                fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"),
                                fieldWithPath("description").type(JsonFieldType.STRING).description("설명")
                        )
                ));
    }

    @Test
    public void modify() throws Exception {
        this.mockMvc.perform(
                        put("/users/{userId}", "hong")
                                .content("{\n\"name\": \"홍길동\", \n\"age\": 20, \n\"description\": \"설명\" }")
                                .contentType(MediaType.APPLICATION_JSON_VALUE)
                                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(document("modify",
                        pathParameters(
                                parameterWithName("userId").description("아이디")
                        ),
                        requestFields(
                                fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                                fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"),
                                fieldWithPath("description").type(JsonFieldType.STRING).description("설명")
                        )
                ));
    }

    @Test
    public void remove() throws Exception {
        this.mockMvc.perform(
                        delete("/users/{userId}", "hong")
                                .contentType(MediaType.APPLICATION_JSON_VALUE)
                                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(document("remove",
                        pathParameters(
                                parameterWithName("userId").description("아이디")
                        )
                ));
    }
}

mockMvc를 통해 해당 API를 호출하여 문서화를 위한 파라미터, 필드 등을 정의한다.

Request/Response Payloads, QueryParameters 등 기본적인 내용은 Spring 공식문서를 참고하기 바란다.
https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#documenting-your-api-request-response-payloads

위의 테스트를 실행하기 위해서는 @SpringBootTest를 실행하여야 하기 때문에 test에 @SpringApplication이 필요하다.

test/하위에 Application.java를 만들어주자.

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

adoc 파일 작성

위의 테스트 케이스를 통해서 snippets 파일을 만들어 주지만 문서화를 위한 adoc파일을 정의해 주어야 한다.

src/main/asciidoc하위에 adoc파일을 정의하자.

image-20221231201052867

index.adoc

:doctype: book

= API Document

include::doc/user.adoc[leveloffset=+1]

user.adoc

= User

== findList
include::{snippets}/findList/curl-request.adoc[]
include::{snippets}/findList/http-response.adoc[]
include::{snippets}/findList/response-body.adoc[]

== find
include::{snippets}/find/curl-request.adoc[]
include::{snippets}/find/path-parameters.adoc[]
include::{snippets}/find/http-response.adoc[]
include::{snippets}/find/response-body.adoc[]

== register
include::{snippets}/register/curl-request.adoc[]
include::{snippets}/register/request-fields.adoc[]

== modify
include::{snippets}/modify/curl-request.adoc[]
include::{snippets}/modify/path-parameters.adoc[]
include::{snippets}/modify/request-fields.adoc[]

== remove
include::{snippets}/remove/curl-request.adoc[]
include::{snippets}/remove/path-parameters.adoc[]

실행

위에서 작성한 코드에서 만들어지는 요소는 2가지이다.

  • snippets 파일 (mvn test)
  • html 파일 : snippets파일을 통해 adoc에서 정의한 내용을 html로 만든다. (mvn prepare-package)

위와 같이 adoc 파일을 만들었으면 maven을 통해 문서를 만들어보자.

mvn prepare-package

위의 명령어를 실행하면 target 하위에 generated-snippets와 generated-docs가 생기는 것을 확인할 수 있다.

image-20221231202022979

최종적으로는 user.html을 통해 api가 문서화가 된 것을 확인할 수 있다.


user.html

image-20221231200951820
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유