RestDocs의 Custom Snippet으로 에러코드 쉽게 문서화하기

배경

RestDocs로 테스트를 통과한 API에 대해서 문서화를 할 수 있다.
우리 프로젝트에서는 정상 요청 흐름을 중점으로 API 문서화했다.
하지만 해당 API에서 발생 가능한 애외 상황에 대한 응답도 정리해줘야 했다.
그래서 우리는 백엔드 단에서 발생하는 예외에 매핑되는 예외 코드를 만들어서 예외 상황 발생 시 해당 예외 코드를 바디에 담아서 반환하도록 해서 대해 API 사용자들이 어떤 문제가 발생해는 지 알 수 있도록 했다.
문제는 특정 API에 해당하는 예외 코드들을 어떻게 문서화하는 지 였다.

코드 예시

restDocs 환경 설정은 생략했다.

예시 컨트롤러

만약 id값이 1보다 작으면 예외를 반환하는 아주 간단한 예시 컨트롤러이다.

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class SimpleController {

@GetMapping("/simple/{id}")
public ResponseEntity<String> getSimple(@PathVariable final Long id) {
if (id < 1) {
throw new IllegalArgumentException("id값은 무조건 1보다 커야 합니다.");
}
final String body = String.format("simple id is %d", id);
return ResponseEntity.ok(body);
}
}

예시 에러코드

사용자에게 알려줄 예외 코드를 enum으로 관리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum ErrorCode {

ILLEGAL_ARGUMENT("40000");

private final String value;

ErrorCode(final String value) {
this.value = value;
}

@JsonValue
public String getValue() {
return value;
}
}

예시 테스트

성공 사례를 확인하는 예시 테스트이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@AutoConfigureRestDocs
@WebMvcTest(SimpleController.class)
@ExtendWith(RestDocumentationExtension.class)
class SimpleControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
void getSimple() throws Exception {
final ResultActions actual = mockMvc.perform(get("/simple/1")
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andDo(document("get-simple"));

actual.andExpect(jsonPath("$").value("simple id is 1"));
}
}

예시 adoc 파일

새로운 adoc 파일을 만들어서 다음과 같이 생성된 스니펫을 넣어주고 테스트를 실행시켜서 성공하면 스니펫이 적용된 html을 얻을 수 있다.

1
operation::get-simple[snippets='http-request,http-response']

에러 코드를 어떻게 문서화할까?

초창기 프로젝트에서 각 API에 에러코드를 명시했던 방법은 매우 간단하다.
그냥 adoc파일에 에러코드를 같이 적어주는 것이다.(…)

1
2
3
4
5
=== 발생 가능한 예외

- 40000 (예시)

operation::get-simple[snippets='http-request,http-response']

이렇게 하면 다음과 같이 그대로 API 예시 요청과 응답에 해당하는 예시코드를 문서화 할 수는 있다. 문제는 API를 추가로 개발하거나 에러 상황이 바뀌게 되면 일일히 adoc 파일에 들어가서 해당 에러코드를 수정해줘야 한다. 그리고 예외 코드를 직접 적어줘야 하니 헷갈리는 여지가 많았다.

RestDocs의 커스텀 스니펫을 활용해보기

커스텀 스니펫은 사용자가 특정한 데이터를 전달받아서 restDocs에 사용되는 스니펫의 형태를 직접 정해서 렌더링할 수 있게 하는 방법이다.

스니펫 형식 만들기

우리가 문서화할 형식을 먼저 만들어보자. 문서에서 에러코드를 어떤 식으로 표현할지를 만들어주면 된다. 우리는 표 형식으로 에러코드를 만들고자 한다.

이때 mustache 문법을 활용해서 전달받은 error-codes를 순회하면서 표를 만들도록 작성했다.
그리고 이 내용을 src/test/resources/org/springframework/restdocs/templates/asciidoctor 이 경로에 저장해주면 된다. 우리는 src/test/resources/org/springframework/restdocs/templates/asciidoctor/error-code-table.snippet 으로 저장했다.

1
2
3
4
5
6
7
|===
|분류|코드
{{#error-codes}}
|{{name}}
|{{value}}
{{/error-codes}}
|===

TemplateSnippet 정의하기

이제 커스텀 스니펫에 어떤 데이터를 넣어서 만들 것인지 정의해보자.

spring framework의 RestDocs에는 TemplatedSnippet이라는 추상 클래스를 지원한다.
RestDocs는 TemplatedSnippet을 기본으로 다양한 스니펫을 만들어서 문서화 한다.

위에서 예시로 봤던 요청 스니펫과 응답 스니펫도 TemplatedSnippet을 상속받은 HttpRequestSnippet이나 HttpResponseSnippet을 통해서 스니펫을 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class TemplatedSnippet implements Snippet {

private final Map<String, Object> attributes = new HashMap<>();

private final String snippetName;

private final String templateName;

protected TemplatedSnippet(String snippetName, Map<String, Object> attributes) {
this(snippetName, snippetName, attributes);
}

protected TemplatedSnippet(String snippetName, String templateName, Map<String, Object> attributes) {
this.templateName = templateName;
this.snippetName = snippetName;
if (attributes != null) {
this.attributes.putAll(attributes);
}
}

// 생략...
}

이제 우리가 원하는 스니펫을 정의해보자

1
2
3
4
5
6
7
8
9
10
public class ErrorCodeSnippet extends TemplatedSnippet {
public ErrorCodeSnippet(ErrorCode... errorCodes) {
super("error-code-table", Map.of("error-codes", errorCodes));
}

@Override
protected Map<String, Object> createModel(final Operation operation) {
return operation.getAttributes();
}
}

스니펫을 생성할 때 에러 코드를 가변 인자로 전달해주면 그 가변인자를 생성자를 통해 전달해주고 TemplateSinppet의 생성자를 호출해서 스니펫을 생성한다. 이때 우리가 스니펫의 이름을 전달해주고 해당 스니펫 속성에 해당하는 Map에 담아 전달해준다.

커스텀 스니펫 적용해보기

이제 adoc 파일에 직접 에러 코드를 적는 방식을 우리의 커스텀 스니펫으로 개선해보자!

먼저 기존의 테스트 코드에 우리의 커스텀 스니펫을 생성해서 문서화할 에러 코드를 생성자의 인자로 전달해준다!

이때 실제 코드가 아닌 자바 enum을 전달해주면 되고 테스트 코드 안에서 문서화 할 내용을 관리해 줄 수 있어서 더 간편한다!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@AutoConfigureRestDocs
@WebMvcTest(SimpleController.class)
@ExtendWith(RestDocumentationExtension.class)
class SimpleControllerTest {

@Autowired
private MockMvc mockMvc;

@Test
void getSimple() throws Exception {
final ResultActions actual = mockMvc.perform(get("/simple/1")
.accept(MediaType.APPLICATION_JSON))
.andDo(print())
.andDo(document("get-simple", new ErrorCodeSnippet(ErrorCode.ILLEGAL_ARGUMENT)));

actual.andExpect(jsonPath("$").value("simple id is 1"));
}
}

그리고 adoc에도 우리가 만든 커스텀 스니펫을 쓰겠다고 적어줘야 한다!

스니펫에 error-code-table이 추가됐음을 확인할 수 있다.

1
2
3
=== 발생 가능한 예외

operation::get-simple[snippets='error-code-table,http-request,http-response']

이렇게 해놓고 테스트를 성공시키면 다음과 같이 문서화가 된다.

참고문서

https://techblog.woowahan.com/2597/

https://docs.spring.io/spring-restdocs/docs/2.0.3.RELEASE/reference/html5/#documenting-your-api-customizing

https://medium.com/@rfrankel_8960/generating-custom-templated-snippets-with-spring-rest-docs-d136534a6f29

Share