Skip to content

Latest commit

 

History

History
118 lines (95 loc) · 7.94 KB

File metadata and controls

118 lines (95 loc) · 7.94 KB

03. ArgumentResolver와 ResultHandler

무엇을 / 왜

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 → 뷰 순으로 가장 적합한 핸들러를 고른다. 둘 다 전략+컴포지트 패턴이라 새 파라미터 타입이나 반환 타입 지원은 구현 하나를 추가하는 일로 끝난다.