[Spring REST Docs + Swagger] RequestPart 문서화하기

이창윤·2024년 1월 16일
1
post-thumbnail

API 문서화 방법 중 대표적인 두가지는 Spring REST DocsSwagger이다. 둘 다 사용해본 결과 REST Docs는 프로덕션 코드에 영향을 주지 않는다는 점이 좋았고, Swagger는 문서가 깔끔하고 문서상에 API를 테스트할 수 있는 기능이 있어 좋았다.

이번 프로젝트에서는 둘의 장점을 살릴 수 있는 방법이 있어 적용 중이다.

해당 방법을 적용하고 나서 처음으로 POST, PUT 요청에 대한 문서화를 하게 됐다.


@PostMapping("/reports")
    public ResponseEntity<Void> reportCreation(
            @RequestPart(value = "facilityReport") FacilityReportRequest facilityReportRequest,
            @RequestPart(value = "imageFile", required = false) MultipartFile multipartFile,
            @Auth UserInfo userInfo
    ) {
        facilityReportService.reportCreation(facilityReportRequest, multipartFile, userInfo);
        return ResponseEntity.noContent().build();
    }

새로운 편의시설에 대한 제보를 할 수 있는 기능

    @PutMapping("/reports/{facilityId}")
    public ResponseEntity<Void> reportModification(
            @PathVariable final Long facilityId,
            @RequestPart(value = "facilityReport") FacilityReportRequest facilityReportRequest,
            @RequestPart(value = "imageFile", required = false) MultipartFile multipartFile,
            @Auth UserInfo userInfo
    ) {
        facilityReportService.reportModification(facilityId, facilityReportRequest, multipartFile, userInfo);
        return ResponseEntity.noContent().build();
    }

그리고 기존 편의시설 정보에 새로운 정보를 추가하거나 잘못된 정보를 수정/삭제하는 제보를 할 수 있는 기능을 추가하고, 문서화 단계로 넘어갔다.

@RequestPartMultipartFile과 Request DTO를 동시에 받을 수 있게 구성을 했는데, 문서화 과정에서 상당히 어려움을 겪었다.

문서화 과정

1. @RequestPart 요청

처음에는 아래와 같이 post요청을 보내도록 작성하고 Body에 DTO와 이미지 파일을 전달하려고 했다.

mockMvc.perform(
		RestDocumentationRequestBuilders.post(DEFAULT_URL + "/reports")
        		.content(objectMapper.writeValueAsString(facilityReport))
                .header("Authorization", "Bearer accessToken")
        )

테스트 실행 결과 Required part is not present라는 에러 메시지를 받아서 Part는 다른 방법으로 처리해야 하는구나 했다.

RestDocumentationRequestBuilders 클래스를 살펴보니 multipart라는 메소드가 있었다.

Postman에서 요청을 보낼 때 처럼 이미지 파일은 Content-Type을 multipart/form-data로, DTO는 application/json으로 보내야 하는데, 따로 설정하는 방법을 찾기 힘들었다.

여러 블로그를 참고한 결과 file의 매개변수로 필요한 MockMultipartFile을 생성하는 과정에서 contentType을 지정할 수 있다는 것을 알았다.

그래서 아래와 같이 테스트용 이미지 파일을 생성하는 메소드를 추가했다.

protected MockMultipartFile getMockMultipartFile() {
        String name = "imageFile";
        String contentType = "multipart/form-data";
        String path = "test.jpg";

        return new MockMultipartFile(name, path, contentType, path.getBytes(StandardCharsets.UTF_8));
    }

그리고 나서 아래와 같이 Request DTO도 MockMultipartFile로 변환했다. 여기서는 content-type을 application/json으로 설정해줬다.

MockMultipartFile facilityReportToPart = new MockMultipartFile(
                "facilityReport",
                null,
                "application/json",
                objectMapper.writeValueAsString(facilityReport).getBytes(StandardCharsets.UTF_8)
        );

MockMultipartFile 두개를 추가한 다음 테스트 실행 결과 MockHttpServletRequest는 제대로 생성이 되는데 아래와 같은 오류가 발생했다.
Request DTO의 필드에 대한 설명을 문서에 추가하고 싶었는데, 요청/응답에 포함되지 않은 필드에 대해 문서화 코드를 추가하면 오류가 발생하도록 막아놓은 것 같다.

그래서 request Body에 직렬화한 request DTO를 추가했다. 그 결과 requestFields에 작성한 필드의 스니펫이 제대로 생성되는 것을 확인할 수 있었다.

2. Custom Annotation

HandlerMethodArgumentResolver를 사용해 커스텀 어노테이션으로 사용자 인증 정보를 가져올 수 있도록 구현을 했다.

하지만 테스트 과정에서 해당 ArgumentResolver가 동작을 해서 인증 오류를 발생시켰다.

그래서 ArgumentResolver를 @MockBean으로 가져오고 supportsParameter, resolveArgument 등의 오버라이딩 된 메소드를 조작하는 코드를 추가했다.

when(authArgumentResolver.supportsParameter((MethodParameter) notNull()))
		.thenReturn(true);

when(authArgumentResolver.resolveArgument(
		(MethodParameter) notNull(),
		(ModelAndViewContainer) notNull(),
		(NativeWebRequest) notNull(),
		(WebDataBinderFactory) notNull()
)).thenReturn(userInfo);

3. @RequestPart + PUT 요청

수정 제보 API도 같은 방법으로 문서화를 했다. 하지만 다음과 같은 오류가 발생했다.
RestDocumentationRequestBuilders의 multipart 메소드는 HTTP Method의 기본값이 POST인데 PUT 요청을 받는 메소드에 사용해서 그런 것이었다.
RestDocumentationRequestBuilders의 multipart 메소드가 MockMultipartHttpServletRequestBuilder를 반환하기 때문에 다음과 같이 커스텀 해주고

MockMultipartHttpServletRequestBuilder customRestDocumentationRequestBuilder = 
	(MockMultipartHttpServletRequestBuilder) multipart(HttpMethod.PUT, DEFAULT_URL+"/reports/{facilityId}", facilityId);

테스트를 실행한 결과오류에서 친절하게 RestDocumentationRequestBuilders를 사용해야 한다고 알려줬다.

조금 더 찾아보니 같은 상황을 겪으신 분이 정리해놓은 블로그가 있어서 참고를 했다.

RestDocs를 위해 테스트해야 하는 경우 MockMultipartHttpServletRequestBuilder에 with() 메소드를 사용해 POST 이외의 HTTP Method를 사용할 수 있도록 커스텀하는 방법을 알게되었다.

MockMultipartHttpServletRequestBuilder customRestDocumentationRequestBuilder = 
		RestDocumentationRequestBuilders.multipart(DEFAULT_URL+"/reports/{facilityId}", facilityId);

customRestDocumentationRequestBuilder.with(new RequestPostProcessor() {
		@Override
		public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
				request.setMethod("PUT");
                return request;
            }
        });
        

mockMvc.perform(
		customRestDocumentationRequestBuilder
				.file(imageFile)
				.file(facilityReportToPart)
				.content(objectMapper.writeValueAsString(facilityReport))
				.header("Authorization", "Bearer accessToken")
)

with() 메소드의 매개변수로 필요한 RequestPostProcessor의 생성과 동시에 postProcessRequest를 오버라이딩해서 PUT을 사용하게끔 변경했다.

테스트가 모두 통과하는 것을 확인하고 API 문서를 생성한 결과 원하는 항목이 제대로 추가되었다!!

아쉬운 점은 파일을 Request body에 추가해서 업로드도 할 수 있도록 아래와 같이 restdocs.payload.FieldDescriptor의 type() 메소드에 MultipartFile을 넣어 type을 지정했는데 빌드 과정에서 오류가 발생했다.

.requestFields(
		fieldWithPath("imageFile").description("편의시설 사진").type(MultipartFile.class).optional(),

Swagger를 사용하면 MultipartFile 타입인 매개변수는 자동으로 string($binary)가 매핑되는 것 같은데 지금 사용한 방법으로는 방법을 찾지 못했다.
ePages Github 깃허브를 찾아보니 아직 Multipartfile은 지원하지 않는 것 같다.

참고

https://wonkang.tistory.com/161?category=957132
https://techblog.woowahan.com/2597/
https://dadadamarine.github.io/java/spring/2019/04/26/spring-controller-test3.html

3개의 댓글

comment-user-thumbnail
7일 전

안녕하세요 혹시 이부분 전체 코드 확인이 가능할까요..!
동일한 문제로 해결을 못 하고 있습니다 ㅠ

2개의 답글