Documenting Form Parameters With Spring REST Docs
1. 소개
폼 인코딩된 엔드포인트는 로그인 및 구독 흐름에서 자주 사용되지만, 요청 문서가 정확하지 않으면 오해하기 쉽습니다. API 소비자는 어떤 매개변수가 필수인지, 어떤 것이 선택 사항인지, 어떤 값이 유효한지, 그리고 추가로 전달된 매개변수 중 어떤 것이 의도적으로 무시되는지를 알 필요가 있습니다.
이 튜토리얼에서는 [Spring REST Docs]와 [MockMvc] 테스트를 사용하여 Spring MVC 엔드포인트의 폼 파라미터를 문서화하는 방법을 다룹니다.
관련은 있지만, 쿼리 파라미터는 다른 사용 사례를 가지며 [다른 REST Docs 스니펫으로 문서화됩니다] 따라서 여기서는 쿼리 파라미터를 다루지 않습니다.
2. 폼 파라미터를 문서화해야 하는 이유
폼 제출에서는 모든 것이 키-값 쌍으로 시작합니다. 문서가 없다면 모호함이 생길 수 있습니다:
- 모든 필드가 필수인가?
- 제약이 있는 필드는 어떤 값을 허용하는가?
- 무시해도 되는 파라미터가 있는가?
Spring MVC는 이러한 처리를 도와주지만, API 소비자는 여전히 정확한 세부 정보를 필요로 합니다. Spring REST Docs는 문서화 대상 테스트가 통과한 후 스니펫을 생성합니다.
REST Docs가 임의의 어설션을 자동으로 문서로 변환해 주지는 않는다는 점에 유의하세요. 우리는 여전히 각 파라미터를 프로그래밍적으로 설명해야 합니다. 그 이점은 문서가 테스트를 통과한 경우에만 생성된다는 점입니다.
참고: 이 글에서는 폼 파라미터만 다룹니다. 쿼리 파라미터는 다음과 같은 차이가 있습니다:
- 폼 파라미터는 요청 본문이 application/x-www-form-urlencoded로 인코딩된 경우에 속합니다.
- 쿼리 파라미터는 URL의 일부로 존재합니다(예: /newsletter/subscriptions?frequency=weekly). 용도가 다르며 [다른 REST Docs 스니펫]으로 문서화됩니다.
3. 폼 기반 엔드포인트 구성하기
폼 필드를 수신하고 페이로드를 검증한 뒤 JSON 응답을 반환하는 작은 뉴스레터 구독 API를 만들어 보겠습니다.
기본적으로 우리의 폼 모델은 application/x-www-form-urlencoded 데이터의 요청 필드를 나타냅니다. Spring은 매개변수 이름을 JavaBean 프로퍼티 이름과 일치시켜 이 객체에 바인딩합니다.
먼저 구독 폼을 정의해 보겠습니다. 우리는 [Bean Validation] 애너테이션을 사용하여 필수 및 제약 값을 강제합니다:
(코드 블록은 원문 그대로 유지됩니다 — 코드 내부는 번역하지 않았습니다.)
패턴으로 frequency의 유효 값을 명시했습니다. 클라이언트가 weekly나 monthly가 아닌 값을 보내면 비즈니스 로직이 실행되기 전에 검증에서 실패합니다. Spring REST Docs는 이 제약을 자동으로 추론하지 않으므로 결과 문서에 유효 값 목록을 자동으로 추가하지 않습니다.
4. 테스트 설정 및 요청 구성
REST Docs의 핵심 아이디어는 테스트된 동작에서 스니펫을 생성하는 것입니다. 따라서 REST Docs 구성과 함께 MockMvc를 사용하여 지원되는 모든 폼 필드를 포함하는 요청을 생성합니다.
4.1. 테스트 설정 및 스니펫 이름 지정
테스트 클래스는 [@WebMvcTest]를 사용하며, JUnit이 Spring REST Docs의 RestDocumentationContext를 관리할 수 있게 RestDocumentationExtension을 지정해야 합니다.
setUp() 메서드는 각 테스트를 위한 컨텍스트를 준비합니다:
- apply(documentationConfiguration(restDocumentation))는 webAppContextSetup()으로 빌드된 MockMvc 인스턴스에 Spring REST Docs를 연결하여 요청과 응답을 캡처합니다.
- alwaysDo(document(“{method-name}”))는 모든 mockMvc.perform() 호출에 대해 실행되는 기본 문서 생성 동작을 추가합니다. {method-name} 플레이스홀더는 현재 테스트 메서드 이름을 캡처합니다.
명시적인 스니펫 디렉터리 이름을 원하면 테스트 자체에서 document(“newsletter-subscribe”)처럼 고정 식별자를 설정할 수 있습니다. 그러면 target/generated-snippets/newsletter-subscribe 아래에 스니펫이 생성됩니다.
4.2. 폼 요청
테스트의 각 부분을 분해해 보겠습니다. 먼저 폼 필드를 포함한 간단한 모의 페이로드를 사용합니다. 폼 페이로드를 전송하려면 param() 메서드를 사용해 키-값 쌍을 엔드포인트에 보냅니다:
(코드 블록은 원문 그대로 유지됩니다.)
여기서 topics를 param(“topics”, “spring”, “testing”)으로 두 번 전송한 것에 주목하세요. 이것이 다중 값 폼 필드를 처리하는 방식입니다.
5. 폼 파라미터 문서화
이제 문서화를 시작할 준비가 되었습니다.
5.1. 폼 파라미터 설명하기
폼 페이로드를 문서화하려면 formParameters()를 사용합니다. 이름과 설명을 정의하고 선택적 매개변수에는 optional()을 사용합니다:
(코드 블록은 원문 그대로 유지됩니다.)
frequency의 유효 값이 변경되면 parameter 설명을 수동으로 업데이트해야 합니다. @Pattern 애너테이션이 이를 추론해주지 않습니다.
가장 중요한 점: optional()은 Spring REST Docs가 매개변수를 문서화하는 방식에만 영향을 미칩니다. Bean Validation에 영향을 주지 않습니다. @NotBlank 같은 제약은 폼 객체를 Spring이 검증할 때 별도로 적용됩니다.
필수 매개변수를 추가하거나 제거하면 보통 두 가지 중 하나가 발생합니다: 요청 검증 동작이 변경되어 테스트가 실패하거나, 문서화된 매개변수와 요청이 더 이상 일치하지 않아 디스크립터 매칭이 실패합니다.
5.2. Bean Validation 제약 처리하기
제약 조건을 수동으로 설명하는 것을 피하려면 ConstraintDescriptions를 통해 검증 메타데이터를 읽고 descriptionsForProperty()를 호출하여 파라미터 설명에 포함시킬 수 있습니다.
(코드 블록은 원문 그대로 유지됩니다.)
이제 docFormParams()로 돌아가 constraintsFor() 메서드를 사용할 수 있습니다:
(코드 블록은 원문 그대로 유지됩니다.)
이렇게 하면 다음과 같은 설명을 얻습니다:
Delivery frequency. Constraints: Must match the regular expression weekly|monthly, Must not be blank
따라서 모든 검증 애너테이션에 대한 설명이 포함됩니다. 애너테이션이 변경되면 설명도 함께 변경됩니다.
5.3. 무시된(ignored) 파라미터 정의하기
일부 클라이언트는 API 문서에 포함시키고 싶지 않은 추가 폼 값을 보냅니다. 예를 들어 프런트엔드 트래킹 파라미터는 비즈니스 로직과 무관합니다. 기본적으로 REST Docs는 문서화되지 않은 파라미터를 발견하면 SnippetException으로 실패하므로, 이를 명시적으로 무시해야 합니다.
먼저 postSubscription()로 돌아가 나중에 무시할 trackingId 파라미터를 전송해 보겠습니다:
(코드 블록은 원문 그대로 유지됩니다.)
이제 docFormParams()에서 trackingId를 무시되는 파라미터로 추가합니다:
(코드 블록은 원문 그대로 유지됩니다.)
이렇게 하면 해당 파라미터는 요청에 포함될 수 있지만 최종 API 문서에는 포함되지 않습니다.
6. 테스트 실행 및 스니펫 생성
마지막으로 위의 모든 부분을 하나의 모의 요청으로 결합해 보겠습니다:
(코드 블록은 원문 그대로 유지됩니다.)
이 단일 테스트는 엔드포인트를 검증하고 모든 폼 파라미터에 대한 필드 수준 문서를 정의합니다. 가장 중요한 점은 계약을 한 곳에서 유지하도록 도와준다는 것입니다.
문서화된 테스트를 실행하면 Spring REST Docs는 document() 메서드에서 지정한 이름 아래인 target/generated-snippets/newsletter-subscribe에 스니펫 파일을 씁니다.
6.1. 생성된 폼 스니펫
폼 POST 테스트 실행에 대해 생성된 스니펫은 우리가 parameterWithName() 호출로 제공한 설명을 사용하여 파라미터를 구조화된 형식으로 보여줍니다:
(마크다운 스니펫 블록은 원문 그대로 유지됩니다.)
이 스니펫은 Maven 빌드 중 Asciidoctor 플러그인을 사용해 AsciiDoc 페이지에 포함할 수 있습니다.
주된 장점은 스니펫이 실제로 테스트에서 실행된 요청으로부터 나온다는 점입니다. 테스트된 동작이나 디스크립터가 더 이상 일치하지 않으면 테스트나 스니펫 생성이 실패하여 코드와 문서를 함께 업데이트하도록 강제합니다.
7. 엄격 모드 vs 느슨한 모드의 파라미터 문서화
기본적으로 formParameters(…)는 엄격(strict) 모드입니다. 즉, 문서화되지 않은 파라미터는 ignored로 표시되지 않으면 스니펫에서 실패합니다.
하지만 때로는 더 가벼운 제약을 원할 수 있습니다. 예를 들어, 추가 키를 허용하면서 핵심 필드만 문서화하고 싶을 때가 있습니다. 그럴 경우 formParameters() 대신 relaxedFormParameters()로 느슨한(relaxed) 모드에 진입합니다. 다음은 relaxedFormParameters()를 사용한 새 헬퍼 메서드입니다:
(코드 블록은 원문 그대로 유지됩니다.)
다음은 이에 대응하는 테스트 메서드입니다:
(코드 블록은 원문 그대로 유지됩니다.)
이 테스트는 폼을 제출하고 상태가 created인지 확인한 뒤 newsletter-subscribe-relaxed 폴더에 문서 스니펫을 생성합니다.
7.1. 파라미터 모드에 대한 빠른 참고
모든 파라미터 처리 모드를 정리하면 다음과 같습니다:
| 모드 | 사용 방법 | 결과 요약 |
|---|---|---|
| 필수 파라미터 | parameterWithName(“email”) | 문서화된 요청에 반드시 존재해야 함 |
| 선택적 파라미터 | parameterWithName(“topics”).optional() | 생략 가능, 스니펫 실패 없음 |
| 무시된(ignored) 파라미터 | parameterWithName(“trackingId”).ignored() | 요청에 존재할 수 있으나 공개된 계약에서는 제외됨 |
| 느슨한 모드(relaxed mode) | relaxedFormParameters(…) | 문서화되지 않은 파라미터를 허용함 |
실무에서는 안정된 계약에는 엄격 모드를 사용하고, 알려진 비계약 입력에 대해 ignored()를 추가하며, 유연성이 필요할 때만 느슨한 모드를 사용하는 것이 일반적입니다.
8. 결론
이 글에서는 Spring REST Docs와 테스트 우선 워크플로우를 사용하여 폼 인코딩된 Spring MVC 엔드포인트를 문서화하는 방법을 다뤘습니다. Spring REST Docs를 사용하면 테스트된 동작을 문서화하고 테스트 사이클의 일부로 스니펫을 재생성하여 문서의 비동기화를 방지할 수 있습니다.
전용 폼 모델을 만들고 입력 필드를 검증했으며 application/x-www-form-urlencoded 요청을 소비하는 POST 엔드포인트를 노출했습니다. 최종적으로 formParameters()를 사용하여 각 키를 설명하고 선택적 및 무시된 필드를 포함했습니다.
필수 파라미터, 선택적 파라미터, 무시된 파라미터, 엄격 모드와 느슨한 모드의 차이를 다루었고 REST Docs가 문서 스니펫을 저장하는 위치를 정의하는 방법도 보았습니다.
항상 그렇듯이 소스 코드는 GitHub에서 확인할 수 있습니다: https://github.com/eugenp/tutorials/blob/master/spring-5-rest-docs



