Skip to content

Latest commit

 

History

History
133 lines (97 loc) · 10.5 KB

File metadata and controls

133 lines (97 loc) · 10.5 KB

05. 멀티파트와 요청 바인딩

무엇을 / 왜

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로 노출

MultipartResolver — 업로드 파싱의 전략

public interface MultipartResolver {
    boolean isMultipart(HttpServletRequest request);
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request);
    void cleanup(MultipartHttpServletRequest request);
}

세 메서드가 업로드 생명주기 전부다. isMultipartmultipart/form-data 여부를 판정하고, resolveMultipart로 요청을 파싱해 MultipartHttpServletRequest로 감싸 반환한다. 이 래퍼 덕분에 컨트롤러는 getFile("avatar")로 파일을, 표준 getParameter로 일반 폼 필드를 똑같이 꺼낸다. 요청이 끝나면 cleanup이 임시 파일을 지운다. 표준 구현은 StandardServletMultipartResolver로, 서블릿 컨테이너의 Part API에 위임한다(별도 라이브러리 불필요).

MultipartFile은 업로드된 파일 한 건의 디스크립터다(소스: web/multipart/MultipartFile.java). getOriginalFilename(), getContentType(), getBytes()/getInputStream()으로 내용을 읽고, transferTo(dest)로 영구 저장소에 옮긴다. 소스 주석은 클라이언트가 준 파일명을 그대로 쓰지 말라고(경로 조작 방지) 명시적으로 경고한다. 내용은 메모리 또는 임시 디스크에 저장되며 요청 종료 시 사라진다.

WebDataBinder — 폼/파라미터를 객체에 채우기

WebDataBinderspring-contextDataBinder를 웹용으로 확장한다(소스: 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/ReturnValueHandlerspring-webmvc/spring-webflux에 있다. 다만 그 해석에 필요한 공통 인프라(web.methodHandlerMethod, web.method.supportModelAndViewContainer 등, web.method.annotation의 일부 리졸버)는 이 모듈에서 공유된다 — 서블릿과 리액티브가 같은 부품을 쓰도록.

NativeWebRequest — 서블릿/리액티브 중립 요청 뷰

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는 서블릿/리액티브 중립적 요청 뷰와 스코프를 제공한다. 모두 "요청 → 타입 있는 값"이라는 한 방향을 향한다.