개인 프로젝트에 문서화가 필요할까?
최근에 직접 만든 블로그를 운영해보고 싶어서 퇴근 후에 틈틈이 진행 중인 사이드 프로젝트가 있다. 혼자 진행하다 보니 직접 작성하고, 직접 사용할 텐데 문서화가 필요할까라는 생각이 들어 문서화는 생각하지 않고 개발 중이었다.
하지만 최근에 회사에서 레거시 프로젝트 유지보수를 하면서 문서화의 필요성을 느꼈고, 가장 큰 요인으로는 스스로의 기억력에 자신도 없다. 또한, 앞날은 모르기 때문에 프로젝트의 규모가 예상보다 더 커진다거나, 공개 프로젝트로 변경될 가능성도 생각했을 때 개인 프로젝트라도 문서화를 해둬서 손해 볼 이유는 없다.
다행히도 현재 프로젝트는 인증이나 로깅 같은 초기 공통 로직들을 구현하는 시작 단계에 있어서, 이미 만들어둔 API들을 몰아서 문서화를 해야 되는 끔찍한 일은 없기에 문서화를 도입하기에 문제는 없다.
왜 REST Docs인가?
스프링에서는 문서화를 위해 대표적으로 REST Docs와 Swagger를 사용한다. 하지만, 개인적으로 Swagger는 API 로직이 지저분해지고, API의 신뢰성이 보장되지 않기에 선호하지 않는다. 그래서, 기존에 사용해 봤고, 테스트 코드도 꼼꼼히 작성하면서 진행할 것이기 때문에 REST Docs를 사용하는 것으로 방향을 정했다.
사용해 보기
REST Docs는 API의 테스트를 통해 요청과 응답으로 자동으로 문서 스니펫을 만들고, 해당 스니펫을 작성해 둔 아스키독 파일 양식대로 조합해 문서를 완성하기 때문에, 컨트롤러 단위 테스트에서 문서화를 진행했다.
@Test
@DisplayName("프로필 조회 성공 테스트")
void getMember() throws Exception {
// given
...
// when
ResultActions actions = mockMvc.perform(
get("/members")
.header(AUTHORIZATION, TOKEN)
.accept(APPLICATION_JSON)
);
// then
actions.andDo(print())
.andExpect(status().isOk())
.andExpect(content().string(apiResponse));
// restdocs
actions.andDo(documentHandler.document(
requestHeaders(
headerWithName(AUTHORIZATION).description("액세스 토큰")
),
responseFields(
fieldWithPath("data.memberId").description("회원 아이디"),
fieldWithPath("data.email").description("회원 이메일"),
...
fieldWithPath("code").description("응답 코드"),
fieldWithPath("status").description("응답 상태"),
fieldWithPath("message").description("응답 메시지")
)
));
}
사용하는 방법은 크게 어렵지 않은데, 기존의 테스트 방식에 문서화를 하는 부분만 추가된다. 요청과 응답에 포함되는 헤더와 바디의 정보만 입력해주면 되는데, 오타나 잘못된 경로가 있으면 문서화가 실패하기 때문에 잘 확인하고 적어주기만 하면 된다.
생각보다 귀찮은 문서화
문서화 작업을 하다 보면 요청과 응답의 정보를 일일히 적어주는 귀찮음은 어떻게 해결할 수 없지만, 문서 포맷 설정, 인코딩 설정, 기타 설정, 공통 정보 등 반복적으로 동일한 로직이 사용되는 부분들이 많다.
@MockBean(JpaMetamodelMappingContext.class)
@WebMvcTest({
...
})
@ExtendWith({RestDocumentationExtension.class})
public class ControllerTest {
...
// 공통 전처리
@BeforeEach
void setUp(WebApplicationContext context,
final RestDocumentationContextProvider restDocumentation,
TestInfo testInfo) {
// 기타 공통 설정들
...
documentHandler = document(
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())
);
DefaultMockMvcBuilder mockMvcBuilder = webAppContextSetup(context)
.apply(documentationConfiguration(restDocumentation))
.addFilters(new CharacterEncodingFilter("UTF-8", true));
...
mockMvc = mockMvcBuilder.build();
}
}
매번 문서화 작업마다 반복되는 설정들을 위와 같이 @BeforeEach를 통해 처리하면 조금 더 간편하게 작업이 가능해진다.
actions.andDo(documentHandler.document(
...,
responseFields(
...,
fieldWithPath("code").description("응답 코드"),
fieldWithPath("status").description("응답 상태"),
fieldWithPath("message").description("응답 메시지")
)
));
actions
.andDo(
documentHandler.document(
...,
pageResponseFields(
...,
fieldWithPath("pageInfo").description("페이지네이션 정보"),
fieldWithPath("pageInfo.page").description("현재 페이지"),
fieldWithPath("pageInfo.size").description("페이지 사이즈"),
fieldWithPath("pageInfo.totalPage").description("전체 페이지 수"),
fieldWithPath("pageInfo.totalSize").description("전체 데이터 개수"),
fieldWithPath("pageInfo.first").description("첫 페이지 여부"),
fieldWithPath("pageInfo.last").description("마지막 페이지 여부"),
fieldWithPath("pageInfo.hasNext").description("다음 페이지 존재 여부"),
fieldWithPath("pageInfo.hasPrevious").description("이전 페이지 존재 여부"),
fieldWithPath("code").description("응답 코드"),
fieldWithPath("status").description("응답 상태"),
fieldWithPath("message").description("응답 메시지")
)
)
);
위의 코드처럼 단일 및 페이지 응답에서 매번 반복적인 로직들을 적어주고 있는데, 유틸 클래스를 만들어 분리해주면 좋다.
private static final FieldDescriptor[] pageInfoFields = new FieldDescriptor[]{
fieldWithPath("pageInfo").description("페이지네이션 정보"),
fieldWithPath("pageInfo.page").description("현재 페이지"),
fieldWithPath("pageInfo.size").description("페이지 사이즈"),
fieldWithPath("pageInfo.totalPage").description("전체 페이지 수"),
fieldWithPath("pageInfo.totalSize").description("전체 데이터 개수"),
fieldWithPath("pageInfo.first").description("첫 페이지 여부"),
fieldWithPath("pageInfo.last").description("마지막 페이지 여부"),
fieldWithPath("pageInfo.hasNext").description("다음 페이지 존재 여부"),
fieldWithPath("pageInfo.hasPrevious").description("이전 페이지 존재 여부")
};
private static final FieldDescriptor[] responseStatusFields = new FieldDescriptor[]{
fieldWithPath("code").description("응답 코드"),
fieldWithPath("status").description("응답 상태"),
fieldWithPath("message").description("응답 메시지")
};
페이지 정보와 응답 상태 정보 같은 고정된 공통 값들을 필드로 만들어두고
public static ResponseFieldsSnippet pageResponseFields(FieldDescriptor... responseFields) {
List<FieldDescriptor> allFields = new ArrayList<>();
allFields.addAll(Arrays.asList(responseFields));
allFields.addAll(Arrays.asList(pageInfoFields));
allFields.addAll(Arrays.asList(responseStatusFields));
return responseFields(allFields);
}
public static ResponseFieldsSnippet singleResponseFields(FieldDescriptor... responseFields) {
List<FieldDescriptor> allFields = new ArrayList<>();
allFields.addAll(Arrays.asList(responseFields));
allFields.addAll(Arrays.asList(responseStatusFields));
return responseFields(allFields);
}
API마다 다른 응답 데이터들만 파라미터로 받아 공통 응답들과 합쳐주면 매번 같은 작업을 하는 번거로움을 줄일 수 있다.
마치며
API의 신뢰성을 챙기면서 테스트 과정에서 편리하게 문서화를 할 수 있는 REST Docs지만, 그럼에도 불구하고 아직도 귀찮은 부분들이 많이 존재한다. 요청과 응답에 대해 직접 명시해주는 것도 자동화할 수 있으면 작업 속도가 더욱 향상될거 같은데, 해당 부분도 좀 더 찾아봐야 할 것 같다.
글을 쓰면서도 개선할 수 있을 것 같은 부분을 발견했고, 작업을 하면서 REST Docs를 찾아보는 과정에서도 다양한 활용 사례를 많이 발견해서, 기회가 되면 다음에 더 나아진 REST Docs 활용글을 작성해보겠다. (이런 말 하면 글이 끊기던데...)
'Back-End > Spring' 카테고리의 다른 글
왜 의존 관계를 동적으로 설정해야 할까? (0) | 2024.04.17 |
---|---|
스프링 면접 질문 정리 : 템플릿 (0) | 2024.04.09 |
스프링 면접 질문 정리 : 테스트 (0) | 2024.04.08 |
토비의 스프링으로 면접 준비하기 (0) | 2024.04.08 |
엔티티에서 값을 의미 있게 표현하는 방법은 무엇일까? (0) | 2024.04.05 |