HTTP 요청에서 데이터를 꺼내 자바 세계로 들이는 두 가지 길이 있다.
- 멀티파트 —
multipart/form-data로 올라온 파일 업로드. 바이트 스트림을 파싱해MultipartFile디스크립터로 만들고, 임시 저장과 정리까지 책임져야 한다. (web.multipart) - 요청 바인딩 — 쿼리 파라미터/폼 필드/경로 변수/헤더/쿠키를 자바 객체 프로퍼티나 메서드 인자에 채워 넣는 일. (
web.bind,web.method)
이 둘은 "원시 서블릿 요청 → 타입 있는 자바 값"이라는 같은 방향을 향하므로 한 장에 묶는다. 그리고 그 바탕에 깔린 요청 추상화 NativeWebRequest(web.context.request)도 여기서 본다. 참고로 web.bind.annotation의 @GetMapping/@RequestBody 같은 애너테이션은 여기 정의되지만, 실제 해석은 spring-webmvc/spring-webflux가 한다 — 이 모듈은 계약(애너테이션)과 바인딩 엔진까지를 담당한다.
멀티파트 (web.multipart)
MultipartResolver isMultipart(req) / resolveMultipart(req) / cleanup
└ StandardServletMultipartResolver (Servlet 3.0 Part API 기반)
MultipartHttpServletRequest extends HttpServletRequest + MultipartRequest
└ getFile(name) → MultipartFile (getBytes/getInputStream/transferTo)
요청 바인딩 (web.bind)
DataBinder (spring-context)
└ WebDataBinder : "_field" 마커, "!field" 기본값 등 웹 규칙
└ ServletRequestDataBinder : ServletRequest 파라미터 → 대상 객체
매핑 계약 (web.bind.annotation)
@RequestMapping / @GetMapping … / @RequestParam / @PathVariable /
@RequestHeader / @CookieValue / @RequestBody / @RequestPart / @ModelAttribute
요청 추상화 (web.context.request)
RequestAttributes ──► WebRequest ──► NativeWebRequest (getNativeRequest)
└ ServletWebRequest (서블릿 구현)
RequestContextHolder : 현재 스레드의 요청을 ThreadLocal로 노출
public interface MultipartResolver {
boolean isMultipart(HttpServletRequest request);
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request);
void cleanup(MultipartHttpServletRequest request);
}세 메서드가 업로드 생명주기 전부다. isMultipart로 multipart/form-data 여부를 판정하고, resolveMultipart로 요청을 파싱해 MultipartHttpServletRequest로 감싸 반환한다. 이 래퍼 덕분에 컨트롤러는 getFile("avatar")로 파일을, 표준 getParameter로 일반 폼 필드를 똑같이 꺼낸다. 요청이 끝나면 cleanup이 임시 파일을 지운다. 표준 구현은 StandardServletMultipartResolver로, 서블릿 컨테이너의 Part API에 위임한다(별도 라이브러리 불필요).
MultipartFile은 업로드된 파일 한 건의 디스크립터다(소스: web/multipart/MultipartFile.java). getOriginalFilename(), getContentType(), getBytes()/getInputStream()으로 내용을 읽고, transferTo(dest)로 영구 저장소에 옮긴다. 소스 주석은 클라이언트가 준 파일명을 그대로 쓰지 말라고(경로 조작 방지) 명시적으로 경고한다. 내용은 메모리 또는 임시 디스크에 저장되며 요청 종료 시 사라진다.
WebDataBinder는 spring-context의 DataBinder를 웹용으로 확장한다(소스: web/bind/WebDataBinder.java). HTML 폼 특유의 문제를 두 가지 규칙으로 푼다.
- 필드 마커(
_접두사) — 체크박스는 해제 시 아예 파라미터가 전송되지 않는다. 그래서_active=on처럼 숨은 마커를 같이 보내, 마커만 있고 실제 값이 없으면 "false/빈 값"으로 바인딩한다. (DEFAULT_FIELD_MARKER_PREFIX = "_") - 필드 기본값(
!접두사) —!name=anonymous처럼 보내면, 실제name이 없을 때 이 기본값을 쓴다. (DEFAULT_FIELD_DEFAULT_PREFIX = "!")
ServletRequestDataBinder(소스 라인 83~)는 한 발 더 나아가 ServletRequest의 파라미터 맵을 직접 받아 대상 객체에 바인딩한다. @ModelAttribute로 폼을 객체에 매핑하는 그 엔진이다. 멀티파트 요청이면 MultipartFile도 같은 경로로 객체 프로퍼티에 바인딩될 수 있다(ByteArrayMultipartFileEditor/StringMultipartFileEditor 같은 프로퍼티 에디터 경유).
web.bind.annotation 패키지는 우리가 매일 쓰는 매핑 애너테이션의 정의처다.
- 메서드 매핑:
@RequestMapping과 단축형@GetMapping/@PostMapping/@PutMapping/@DeleteMapping/@PatchMapping. - 인자 바인딩:
@RequestParam,@PathVariable,@MatrixVariable,@RequestHeader,@CookieValue,@RequestBody,@RequestPart,@ModelAttribute,@RequestAttribute,@SessionAttribute. - 응답/상태:
@ResponseBody,@ResponseStatus,@RestController(=@Controller+@ResponseBody). - 전역 처리:
@ControllerAdvice/@RestControllerAdvice,@ExceptionHandler,@InitBinder,@CrossOrigin(04장 CORS와 연결).
이들은 단순 메타데이터일 뿐, 실제로 인자를 채우는 ArgumentResolver/ReturnValueHandler는 spring-webmvc/spring-webflux에 있다. 다만 그 해석에 필요한 공통 인프라(web.method의 HandlerMethod, web.method.support의 ModelAndViewContainer 등, web.method.annotation의 일부 리졸버)는 이 모듈에서 공유된다 — 서블릿과 리액티브가 같은 부품을 쓰도록.
web.context.request는 "현재 요청"을 프레임워크 중립적으로 추상화한다.
RequestAttributes (속성 get/set + 스코프)
└ WebRequest (헤더/파라미터/locale/principal 접근)
└ NativeWebRequest (getNativeRequest(Class) 로 내부 객체 노출)
└ ServletWebRequest (서블릿 HttpServletRequest 래핑)
WebRequest는 "요청 메타데이터를 읽기 위한" 일반 인터페이스로, 주로 인터셉터가 핸들링 로직과 무관하게 요청을 들여다볼 때 쓴다(소스 주석). NativeWebRequest는 필요 시 getNativeRequest(HttpServletRequest.class)로 원본을 꺼낼 탈출구를 준다. 이 추상화 덕에 콘텐츠 협상(04장)이나 캐시 처리 코드가 서블릿/리액티브 양쪽에서 같은 시그니처로 동작한다.
RequestContextHolder는 현재 요청을 ThreadLocal로 노출해, 컨트롤러 인자로 받지 않아도 어디서든 현재 RequestAttributes에 접근하게 한다. RequestScope/SessionScope(스프링 빈 스코프) 구현도 이 패키지에 있어, request/session 스코프 빈의 생명주기가 여기에 연결된다. web.context.request.async는 서블릿 비동기(DeferredResult/Callable) 처리를 떠받친다.
<form enctype="multipart/form-data">로 파일+텍스트를 올리는 요청이 객체에 도달하기까지:
POST /profile Content-Type: multipart/form-data; boundary=...
├ part "name" = "kim"
└ part "avatar" = <binary, filename=cat.png>
│
▼ DispatcherServlet 진입부
MultipartResolver.isMultipart(req) → true
MultipartResolver.resolveMultipart(req)
│
▼ 파싱
MultipartHttpServletRequest 로 래핑
├ getParameter("name") → "kim" (일반 필드)
└ getFile("avatar") → MultipartFile (파일 디스크립터)
│
▼ 핸들러 인자 바인딩
@ModelAttribute ProfileForm ← ServletRequestDataBinder 가
name 채움 + (MultipartFile 프로퍼티면) avatar 바인딩
@RequestPart MultipartFile avatar ← 직접 주입도 가능
│
▼ 핸들러 실행 후
MultipartResolver.cleanup(req) → 임시 파일 삭제
핵심은 **파싱(resolver) → 래핑(MultipartHttpServletRequest) → 바인딩(DataBinder/리졸버) → 정리(cleanup)**의 4단계, 그리고 파일과 일반 필드가 같은 요청 뷰에서 함께 노출된다는 점이다.
MultipartResolver.resolveMultipart— 원시 요청을 파일+파라미터로 파싱해 표준 인터페이스로 노출. 업로드 처리의 단일 진입점.MultipartFile.transferTo(dest)— 임시 저장된 업로드를 영구 위치로 이동. 업로드의 종착점.WebDataBinder의 마커(_)/기본값(!) 규칙 — HTML 폼의 누락 파라미터 문제를 바인딩 규약으로 흡수.ServletRequestDataBinder.bind(request)— 서블릿 파라미터를 대상 객체에 채우는 실제 바인딩 호출.NativeWebRequest.getNativeRequest(Class)— 중립 추상에서 필요 시 원본 요청을 꺼내는 탈출구.RequestContextHolder.getRequestAttributes()— 인자 전달 없이 현재 요청에 접근.
- 래퍼로 통일:
MultipartHttpServletRequest가 파일/필드를 같은 요청 객체에 노출해, 컨트롤러가 멀티파트 여부를 거의 의식하지 않게 한다. - 폼 현실을 규약으로:
WebDataBinder의_/!접두사는 HTML 폼의 구조적 한계(체크박스 미전송 등)를 프레임워크 규약으로 해결한다. - 계약/구현 분리: 매핑 애너테이션과 공통 바인딩 부품은
spring-web에, 실제 디스패치는 상위 모듈에 둬 서블릿/리액티브가 같은 계약을 공유한다. - 요청 추상화:
NativeWebRequest계층이 프레임워크 중립 코드(협상·캐시·인터셉터)의 토대가 된다. - 확장점: 커스텀
MultipartResolver,@InitBinder메서드로WebDataBinder커스터마이즈, 프로퍼티 에디터/컨버터 등록이 표준 경로다.
이 장은 원시 HTTP 요청을 타입 있는 자바 값으로 들이는 두 길을 다뤘다. MultipartResolver는 업로드를 파싱→래핑→정리의 생명주기로 관리하며 MultipartFile로 노출하고, WebDataBinder/ServletRequestDataBinder는 폼·파라미터를 객체에 바인딩하되 HTML 폼의 누락 문제를 _/! 규약으로 흡수한다. web.bind.annotation은 매핑 애너테이션 계약을 정의하고(해석은 상위 모듈), NativeWebRequest/RequestContextHolder는 서블릿/리액티브 중립적 요청 뷰와 스코프를 제공한다. 모두 "요청 → 타입 있는 값"이라는 한 방향을 향한다.