취준 관리 서비스 뽀각을 개발하면서 Spring REST Docs를 도입해 API 문서 생성을 하고 있었다.
REST Docs의 장단점은 명확하다.
장점 : 프로덕션 코드에 어노테이션 등이 침투하지 않는다.
단점 : 테스트 코드가 성공해야만 문서 생성이 가능하다.
테스트 코드도 강제로 짜고 좋지 뭐.. 하며 호기롭게 시작한 REST Docs 적용은 테스트 코드 지옥에 우리를 빠트렸다..
문제점
뽀각에는 노션의 페이지처럼 마크다운 문법으로 글을 작성할 수 있는 카드라는 아이템이 있는데 이를 단건조회하는 테스트 코드이다.
API 하나에 미친 듯한 코드 줄의 양이 보이는가..?
이는 JSON 상하차를 해야하는 서비스 초반 우리 개발 속도에 발목을 잡았다.
팩토리 클래스 생성
이를 REST Docs 문법에 맞는 테스트 코드를 생성해주는 팩토리 클래스를 만듦으로써 해결했다.
REST Docs가 문서화를 하기 위해서는 테스트 코드에 아래와 같은 데이터들이 필요하다.
여기서 가장 문제는 3번과 4번의 Request Body, Response Body를 표현하는 DTO 클래스였다.
DTO 클래스는 API마다 내부의 필드 변수들의 타입, 개수가 모두 달랐던 것이다.
이를 위와 같이 일일이 모두 작성해주어야 한다.
REST DocsRest는 DocumentationResultHandler라는 클래스로 API 응답 결과를 받아 문서화하려는 Request, Response 내용과 일치하는지 비교한다.
DocumentationResultHandler라는 클래스를 해당 DTO에 맞게 런타임에서 동적으로 생성해주면 해결될 것이다.
Java Reflection을 통한 DTO의 필드 정보 추출
DTO 속 필드 값들의 주 타입 케이스는 3가지였다.
1. 기본 타입
2. 컬렉션 타입
3. 클래스 타입
게다가 DTO별로 필드 개수도 달랐으므로 메서드의 파라미터 케이스를 나누기에는 경우의 수가 무궁무진했다.
이를 해결한 방법은 Java Reflection이다.
Java Reflection은 런타임에 클래스, 인터페이스, 메서드, 필드 등의 정보를 동적으로 조사하고 조작할 수 있는 기능을 제공하는 Java API이다.
Field[] declaredFields = dto.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
String fieldName = field.getName();
}
이런 식으로 클래스의 정보를 접근제한자와 상관없이 가져올 수 있다.
아래 코드는 Reflection을 통해 DTO 속 필드 값들을 처리하는 코드의 일부분이다.
public <T> FieldDescriptor[] getFields(T dto) {
List<FieldDescriptor> fields = new ArrayList<>();
generateFieldDescriptors(dto, "", fields);
return fields.toArray(new FieldDescriptor[0]);
}
public <T> FieldDescriptor[] getFieldsList(T dto) {
List<FieldDescriptor> fields = new ArrayList<>();
generateFieldDescriptors(dto, "[].", fields);
return fields.toArray(new FieldDescriptor[0]);
}
private <T> void generateFieldDescriptors(T dto, String pathPrefix, List<FieldDescriptor> fields) {
if (isSimpleType(dto)) {
return;
}
Field[] declaredFields = dto.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
String fieldPath = pathPrefix + field.getName();
JsonFieldType fieldType = determineFieldType(field.getType());
Object fieldValue = getFieldValue(dto, field);
FieldDescriptor descriptor = fieldWithPath(fieldPath)
.type(fieldType)
.description(field.getName())
.optional();
if (fieldType != JsonFieldType.OBJECT && fieldType != JsonFieldType.ARRAY) {
descriptor.attributes(
Attributes.key("example").value(fieldValue != null ? fieldValue.toString() : "null"));
}
fields.add(descriptor);
// 리스트 타입 처리
if (fieldType == JsonFieldType.ARRAY && fieldValue instanceof List<?> list) {
if (!list.isEmpty()) {
Object firstElement = list.getFirst();
generateFieldDescriptors(firstElement, fieldPath + "[].", fields);
}
}
// 오브젝트 타입 처리
if (fieldType == JsonFieldType.OBJECT && fieldValue != null) {
generateFieldDescriptors(fieldValue, fieldPath + ".", fields);
}
}
}
private <T> Object getFieldValue(T dto, Field field) {
try {
return field.get(dto);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to access field: " + field.getName(), e);
}
}
리스트와 오브젝트 타입은 필드 정보를 가져오는 메서드를 재귀함수를 태워처리했다.
결과
팩토리 클래스를 적용한 테스트 코드는 아래와 같다.
아까의 괴랄한 코드 길이는 모두 사라졌다.
restDocsFactory가 내가 생성한 팩토리 클래스이다.
DTO를 한번만 생성하면 해당 DTO를 사용하는 모든 테스트 코드의 팩토리 클래스에 파라미터로 넣어주면 된다.
이제 DTO 클래스만 생성하면 REST docs 문법을 알지 못해도 테스트 코드 작성이 가능해졌다.
상세 코드
마치며..
해당 팩토리 클래스를 적용하고 나뿐만이 아니라 팀원들의 테스트 코드 작성 시간이 1/5은 줄어든 것 같다.
바쁜 와중에 REST Docs 문법까지 배워야했던 팀원들에게 마음의 짐을 덜어줄 수 있어 기뻤다.
바로 체감이 되는 생산성 향상 경험을 할 수 있었던 것 같아 뿌듯하다.
'spring' 카테고리의 다른 글
[SpringBoot] Sentry On-premise 구축기 (2) | 2024.09.29 |
---|---|
[스프링] service 인터페이스 도입과 접근제한자 (1) | 2024.09.20 |
[spring security] 왜 500번 에러도 401(Unauthorized)에러가 될까? (0) | 2024.04.07 |
이미지 저장/조회 서버 만들기(3) - AWS Presigned URL 이미지 업로드 (0) | 2024.02.26 |
이미지 저장/조회 서버 만들기(2) - 이미지 파일 어떻게 받아오지? (0) | 2024.02.12 |