
SOFTMOA TECHNOLOGY
Spring MVC LocalDate 날짜 파라미터 400 오류 해결 방법
소프트모아가 정리한 기술 기록입니다.
Contents
Spring MVC에서 날짜 쿼리 파라미터 때문에 400 응답이 나온다면, 클라이언트가 보낸 문자열과 서버의 날짜 변환 규칙부터 맞추세요. 날짜만 받는 API에는 LocalDate와 ISO.DATE를 명시하되, 정상 날짜 하나가 통과했다고 검증을 끝내면 안 됩니다. 빈 값과 존재하지 않는 날짜, 오프셋이 붙은 값까지 확인해야 실제로 어떤 입력을 받는지 알 수 있습니다.[1][2]
같은 400이라도 바꿔야 할 곳은 다릅니다
개발자 도구에서 day가 2026-10-03인지 2026/10/03인지 확인하세요. 아래 GET /date-check 예제는 조회나 저장 없이 변환한 날짜만 돌려줍니다.
- MethodArgumentTypeMismatchException이면 문자열을 LocalDate로 바꾸는 단계와 입력값을 확인합니다. 예제에서는 슬래시 날짜와 잘못된 윤년 날짜가 이 예외로 거절됐습니다.
- MissingServletRequestParameterException이면 파라미터 누락인지, 보냈지만 빈 문자열이 변환 후 null이 됐는지 구분합니다. 두 경우 모두 아래 필수 파라미터에서는 400입니다.[2]
- 요청이 JSON 본문이라면 먼저 @RequestBody 경로인지 확인합니다. 본문은 HttpMessageConverter로 읽으므로, 쿼리 파라미터의 날짜 설정과 구분해서 점검해야 합니다.[3]
날짜 계약을 메서드 파라미터에 붙입니다
src/main/java/sample/DateCheckController.java에 저장합니다. 파라미터 이름은 day로 명시했습니다. @DateTimeFormat에는 iso만 지정하세요. pattern을 함께 넣으면 pattern이 우선합니다.[1]
package sample;
import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
@RestController
public class DateCheckController {
@GetMapping("/date-check")
public String check(
@RequestParam("day")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate day) {
return day.toString();
}
}
LocalDate는 시간대와 시각을 저장하지 않습니다.[4] @DateTimeFormat을 인자 없이 붙이는 것도 ISO 지정과는 다릅니다. 기본 style 기반 파싱은 로케일과 JDK의 영향을 받을 수 있어 입력 규칙을 명시하는 편이 낫습니다.[1]
정상값과 경계값을 같은 검사에 넣습니다
src/test/java/sample/DateCheckTest.java에 저장하세요. MVC와 spring-test, JUnit 5가 있는 Gradle 프로젝트 기준입니다. Boot에서는 웹 MVC·테스트 스타터를 확인하되 버전을 임의로 덮어쓰지 마세요.
package sample;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
class DateCheckTest {
@Test
void checksDateInputs() throws Exception {
var mvc = MockMvcBuilders.standaloneSetup(new DateCheckController()).build();
String[] inputs = {
"2026-10-03", "2024-02-29", "2026/10/03", "2026-02-29",
"", "2026-10-03T00:00:00", "2026-10-03+09:00"
};
int[] codes = {200, 200, 400, 400, 400, 400, 200};
for (int i = 0; i < inputs.length; i++) {
mvc.perform(get("/date-check").param("day", inputs[i]))
.andExpect(status().is(codes[i]));
}
mvc.perform(get("/date-check")).andExpect(status().isBadRequest());
mvc.perform(get("/date-check").param("day", "2026-10-03+09:00"))
.andExpect(content().string("2026-10-03"));
}
}
gradle --no-daemon test --tests sample.DateCheckTest
로컬에서는 Spring Framework 6.2.19와 Java 25로 컴파일하고 이 검사를 실행했습니다. 7개 문자열과 파라미터 누락을 합친 8개 입력이 예상한 상태 코드와 일치했습니다. 2026-10-03과 윤년의 2024-02-29는 200, 슬래시 날짜·2026-02-29·빈 값·시각이 붙은 문자열·파라미터 누락은 400이었습니다. 이 결과는 독립 MockMvc 검사이며 운영 서버를 호출한 결과가 아닙니다.
ISO.DATE가 허용한 범위를 확인하세요
한 가지 더 봐야 할 결과가 있습니다. 2026-10-03+09:00도 200이었고, 응답은 2026-10-03이었습니다. ISO 날짜 포매터는 오프셋이 붙은 날짜를 파싱할 수 있으며, LocalDate에 변환한 뒤에는 오프셋을 보관하지 않습니다.[7][4] 따라서 ISO.DATE 지정만으로 정확히 10자리 문자열만 받는다고 설명하면 안 됩니다.
입력을 반드시 10자리로 제한해야 하는 계약이라면, 문자열 모양 검사와 실제 달력 날짜 검사를 별도로 설계하고 그 규칙으로 테스트를 추가하세요. 마감일이 미래인지, 조회 기간이 허용 범위인지도 날짜 파싱 성공과는 다른 업무 조건입니다. 빈 날짜가 정상적인 선택 사항인 API에서는 required=false 등으로 누락 허용을 명시하고, null일 때의 처리도 함께 정해야 합니다.[2]
이 검사는 전역 Formatter, 보안 필터, JSON 설정과 DB 저장을 포함하지 않습니다. 운영 요청만 실패한다면 실제 전송 위치와 등록된 변환기를 확인하고, 해당 구성을 포함한 테스트로 재현하세요.
확인한 공식 문서
- [1] Spring DateTimeFormat
- [2] Spring MVC 타입 변환
- [3] Spring MVC RequestBody
- [4] Java LocalDate
- [7] Java DateTimeFormatter
업무 API와 화면 연동 개발은 소프트모아에서 확인할 수 있습니다.
소프트모아는 해당 시스템을 구축합니다. 문의하기
