2장에서 InvocableHandlerMethod가 "파라미터마다 리졸버로 인자를 만들고, 반환값은 HandlerResult로 묶는다"고 했다. 이 장은 그 양쪽 끝 — 입력단의 HandlerMethodArgumentResolver 와 출력단의 HandlerResultHandler — 을 자세히 본다.
핵심 질문 둘이다. (1) @RequestParam, @PathVariable, @RequestBody, Mono<User> 같은 다양한 파라미터를 어떻게 통일된 방식으로 채우는가. (2) String(뷰 이름), User(JSON 직렬화), ResponseEntity, Mono<ServerResponse> 같은 다양한 반환값을 어떻게 적절한 응답으로 바꾸는가.
HandlerMethodArgumentResolver ← 전략 인터페이스
boolean supportsParameter(param)
Mono<Object> resolveArgument(param, bindingContext, exchange)
▲
│ 두 갈래
├── (비동기 가능) AbstractMessageReaderArgumentResolver
│ @RequestBody / HttpEntity : 본문을 HttpMessageReader로 디코딩
│
└── SyncHandlerMethodArgumentResolver ← 즉시 값(블로킹 없음)
Object resolveArgumentValue(...) → Mono.justOrEmpty 로 감쌈
예: AbstractNamedValueSyncArgumentResolver
@RequestParam / @PathVariable / @RequestHeader /
@CookieValue / @MatrixVariable …
HandlerMethodArgumentResolverComposite ← 여러 리졸버를 묶어 위임
· supportsParameter: 첫 매칭 리졸버를 캐시
· resolveArgument: 그 리졸버에 위임
SyncHandlerMethodArgumentResolver는 "본문 디코딩 없이 즉시 결정되는 값"을 위한 하위 인터페이스다. URI 변수나 쿼리 파라미터처럼 비동기가 필요 없는 인자는 동기 경로로 처리해 오버헤드를 줄인다. 반대로 @RequestBody는 본문 스트림을 HttpMessageReader로 디코딩하므로 진짜 Mono를 반환한다.
대표 리졸버들(모두 result/method/annotation/):
| 애너테이션 | 리졸버 | 하는 일 |
|---|---|---|
@RequestParam |
RequestParamMethodArgumentResolver |
쿼리/폼 파라미터, 값 변환 |
@PathVariable |
PathVariableMethodArgumentResolver |
URI 템플릿 변수 |
@RequestHeader |
RequestHeaderMethodArgumentResolver |
헤더 값 |
@RequestBody |
RequestBodyMethodArgumentResolver |
본문 디코딩(Mono/Flux/객체) |
@ModelAttribute |
ModelAttributeMethodArgumentResolver |
폼 바인딩 + 검증 |
@RequestPart |
RequestPartMethodArgumentResolver |
멀티파트 파트 |
| (타입 기반) | ServerWebExchangeMethodArgumentResolver |
ServerWebExchange, ServerHttpRequest 등 |
AbstractNamedValueArgumentResolver는 "이름 있는 값"(파라미터/헤더/쿠키/패스변수)의 공통 로직 — 이름 결정 → 값 추출 → 누락 시 기본값/필수 검사 → 타입 변환(DataBinder) — 을 한곳에 모은다. 각 애너테이션 리졸버는 "이름을 어디서 어떻게 뽑나"만 구현한다(템플릿 메서드).
HandlerResultHandler ← 전략 인터페이스 (01장)
boolean supports(HandlerResult)
Mono<Void> handleResult(exchange, result)
▲ (order 로 우선순위, 먼저 매칭되는 것이 처리)
│
├── ResponseEntityResultHandler order 0
│ ResponseEntity / HttpHeaders / ErrorResponse →
│ 상태·헤더 적용 후 본문 write
│
├── ServerResponseResultHandler (함수형, 04장)
│ ServerResponse → writeTo(exchange)
│
├── ResponseBodyResultHandler order 100
│ @ResponseBody → HttpMessageWriter 로 직렬화
│ (ProblemDetail 이면 상태코드·instance 보정)
│
└── ViewResolutionResultHandler order LOWEST
String / View / Model / Map / Rendering →
ViewResolver 로 뷰 찾아 렌더링 (06장)
order가 핵심이다. ResponseEntityResultHandler는 0으로 가장 먼저 매칭을 시도하고, ResponseBodyResultHandler는 100이라 더 구체적인 타입을 노리는 핸들러들 뒤에 온다. 가장 일반적인 뷰 해석은 맨 마지막이다.
[입력단] InvocableHandlerMethod.getMethodArgumentValues
파라미터 P
│ resolvers.supportsParameter(P) → 첫 매칭 리졸버 R (캐시)
▼
R.resolveArgument(P, ctx, exchange)
│ · @RequestParam → 동기: 값 추출+변환 → Mono.just
│ · @RequestBody → 비동기: messageReader.read(body) → Mono/Flux
▼
Mono<Object> ──(모든 파라미터)──▶ Mono.zip → Object[] args
[메서드 호출] method.invoke(bean, args) → returnValue
│
▼ new HandlerResult(handler, returnValue, returnType, ctx)
[출력단] DispatcherHandler.handleResult
resultHandlers 순회(order 순)
│ 첫 supports(result)==true 인 핸들러 H
▼
H.handleResult(exchange, result)
│ · ResponseBody → HttpMessageWriter.write(returnValue, ...)
│ · ResponseEntity→ 상태/헤더 적용 후 본문 write
│ · String/View → ViewResolver → View.render
▼
Mono<Void> (응답 완료)
HandlerMethodArgumentResolverComposite.getArgumentResolver()— 파라미터당 매칭 리졸버를ConcurrentHashMap에 캐시한다.supportsParameter가 호출마다 전체 리스트를 훑지 않도록 하는 최적화.AbstractMessageReaderArgumentResolver.readBody()—@RequestBody/HttpEntity의 핵심. 요청 콘텐츠 타입에 맞는HttpMessageReader를 골라 본문을 디코딩하고, 대상 타입이Mono/Flux면 그대로, 단일 객체면Mono로 모아 반환한다.@Valid가 있으면 디코딩 후 검증을 끼운다.ResponseBodyResultHandler.handleResult()— 반환값이ProblemDetail이면 상태 코드와instance(요청 경로)를 보정한 뒤writeBody()로 직렬화한다.supports()는 클래스/메서드의@ResponseBody존재를 캐시로 판단한다.HandlerResultHandlerSupport.selectMediaType()— 출력 핸들러들의 공통 기반.RequestedContentTypeResolver(accept 헤더 등)와 핸들러가 쓸 수 있는 미디어 타입을 비교해 협상한다. 협상 실패 시NotAcceptableStatusException.
- 두 단 모두 전략 + 컴포지트 패턴: 입력은
...ResolverComposite, 출력은DispatcherHandler가 리스트를 순회. 새 리졸버/결과처리기를 추가하는 것이 확장의 표준 방법이다.ArgumentResolverConfigurer(RequestMappingHandlerAdapter에 주입)로 커스텀 리졸버를 등록한다. - 동기/비동기 분리:
SyncHandlerMethodArgumentResolver로 흔한 케이스(URI 변수, 쿼리)를 빠르게 처리하고, 본문 디코딩 같은 진짜 비동기만Mono경로로 간다. - order 기반 우선순위: 출력 핸들러는
order로 "더 구체적인 타입 먼저, 일반 뷰 해석 마지막" 규칙을 강제한다.ResponseEntityResultHandler가@ResponseBody보다 앞서는 이유다. - 에러 응답 표준화:
ProblemDetail/ErrorResponse(RFC 7807) 처리가 결과 핸들러에 내장돼 있어, 예외도 일관된 JSON 본문으로 직렬화된다.
컨트롤러 메서드의 입력단은 HandlerMethodArgumentResolver(들을 묶은 컴포지트)가, 출력단은 HandlerResultHandler(order로 정렬된 리스트)가 책임진다. 입력단은 즉시 값(Sync)과 본문 디코딩(MessageReader)을 구분해 처리하고, 출력단은 ResponseEntity → @ResponseBody → 뷰 순으로 가장 적합한 핸들러를 고른다. 둘 다 전략+컴포지트 패턴이라 새 파라미터 타입이나 반환 타입 지원은 구현 하나를 추가하는 일로 끝난다.