본문으로 건너뛰기
김신건의 로그

[Spring MVC] Argument Resolver: Custom HandlerMethodArgumentResolver

· 수정 · 📖 약 1분 · 274자/단어 #spring #mvc #argument-resolver #handler #extension
HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler, @AuthenticationPrincipal, @RequestAttribute, @SessionAttribute, Custom annotation, ArgumentResolver

정의

HandlerMethodArgumentResolver = 컨트롤러 메서드 인자를 어떻게 채울지 결정 하는 확장점. Spring 의 수십 개 표준 resolver + 커스텀 resolver 추가 가능.

Filter chain 의 일반 직관. ArgumentResolver 도 체인 형태로 적합한 resolver 가 처리 (Chain of Responsibility 패턴).

표준 ArgumentResolver

어노테이션 / 타입resolver
@PathVariablePathVariableMethodArgumentResolver
@RequestParamRequestParamMethodArgumentResolver
@RequestBodyRequestResponseBodyMethodProcessor
@RequestHeaderRequestHeaderMethodArgumentResolver
@CookieValueServletCookieValueMethodArgumentResolver
@ModelAttributeModelAttributeMethodProcessor
Model, ModelMapModelMethodProcessor
LocaleLocaleResolver
HttpServletRequestServletRequestMethodArgumentResolver
Authentication, PrincipalAuthenticationPrincipalArgumentResolver
MultipartFileRequestPartMethodArgumentResolver
Pageable (Spring Data)PageableHandlerMethodArgumentResolver

흐름

sequenceDiagram
    autonumber
    participant DS as DispatcherServlet
    participant Adapter as RequestMappingHandlerAdapter
    participant Resolver as 각 ArgumentResolver
    participant Handler as Controller method
    participant ReturnValueHandler

    DS->>Adapter: handle(request, handler)
    Adapter->>Adapter: 인자 목록 검사
    loop 각 인자
        Adapter->>Resolver: supportsParameter(param)
        Resolver-->>Adapter: yes / no
        alt yes
            Adapter->>Resolver: resolveArgument(...)
            Resolver-->>Adapter: value
        end
    end
    Adapter->>Handler: invoke(args[])
    Handler-->>Adapter: return value
    Adapter->>ReturnValueHandler: handleReturnValue(return)

Custom ArgumentResolver: 예시

1. 어노테이션 정의

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser { }

2. Resolver 구현

@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final UserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class)
            && parameter.getParameterType().equals(User.class);
    }

    @Override
    public Object resolveArgument(
        MethodParameter parameter,
        ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest,
        WebDataBinderFactory binderFactory
    ) throws Exception {
        Authentication auth = (Authentication) webRequest.getUserPrincipal();
        if (auth == null || !auth.isAuthenticated()) return null;

        String username = auth.getName();
        return userService.findByUsername(username);
    }
}

3. 등록

@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final CurrentUserArgumentResolver currentUserResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(currentUserResolver);
    }
}

4. 사용

@RestController
public class UserController {

    @GetMapping("/me")
    public User me(@CurrentUser User user) {
        return user;
    }
}

사례: API Pagination

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParsedPagination {
    int defaultSize() default 20;
    int maxSize() default 100;
}

public record Pagination(int page, int size, String sort) { }

@Component
public class PaginationResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType() == Pagination.class;
    }

    @Override
    public Object resolveArgument(...) {
        ParsedPagination ann = parameter.getParameterAnnotation(ParsedPagination.class);
        int defSize = ann != null ? ann.defaultSize() : 20;
        int maxSize = ann != null ? ann.maxSize() : 100;

        HttpServletRequest req = webRequest.getNativeRequest(HttpServletRequest.class);
        int page = parseInt(req.getParameter("page"), 1);
        int size = Math.min(parseInt(req.getParameter("size"), defSize), maxSize);
        String sort = req.getParameter("sort");

        return new Pagination(page, size, sort);
    }
}

@GetMapping("/users")
public List<User> list(@ParsedPagination Pagination p) {
    return userService.find(p);
}

ReturnValueHandler: 반환값 처리

@Component
public class ApiResponseReturnValueHandler implements HandlerMethodReturnValueHandler {

    private final HandlerMethodReturnValueHandler delegate;

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return returnType.hasMethodAnnotation(ApiResponse.class);
    }

    @Override
    public void handleReturnValue(
        Object returnValue,
        MethodParameter returnType,
        ModelAndViewContainer mavContainer,
        NativeWebRequest webRequest
    ) throws Exception {
        Map<String, Object> wrapped = Map.of(
            "success", true,
            "data", returnValue,
            "timestamp", Instant.now()
        );
        delegate.handleReturnValue(wrapped, returnType, mavContainer, webRequest);
    }
}

모든 컨트롤러 응답을 공통 래퍼 ({success, data, timestamp}) 로.

표준 vs Custom 비교

flowchart TD
    Q{언제 Custom?}
    Q --> Use1["1. 도메인 객체 자동 주입 (CurrentUser)"]
    Q --> Use2["2. 복잡 파라미터 파싱 (Pagination)"]
    Q --> Use3["3. 공통 컨텍스트 (Tenant, Trace ID)"]
    Q --> Use4["4. 외부 시스템 lookup (옛 ID → 최신 ID)"]
    NoUse[X 안 쓸 때]
    NoUse --> N1[1. @RequestParam, @PathVariable 로 충분]
    NoUse --> N2[2. 표준 어노테이션 1개로 표현 가능]

옛 방식 vs 새 방식

// ❌ 매 컨트롤러마다 코드 반복
@GetMapping("/me")
public User me(HttpServletRequest req) {
    String username = (String) req.getSession().getAttribute("username");
    if (username == null) throw new UnauthorizedException();
    return userService.findByUsername(username);
}

// ✓ Custom resolver 로 매 메서드 한 줄
@GetMapping("/me")
public User me(@CurrentUser User user) {
    return user;
}

흔한 함정

WARNING

  1. supportsParameter 너무 광범위 = 다른 인자도 처리 시도. 어노테이션 + 타입 둘 다 검사.
  2. Resolver 에서 비즈니스 로직 = 단순 값 채우기 만. 비즈니스는 service.
  3. HttpServletRequest 직접 의존 = 테스트 어려움. NativeWebRequest 추상화.
  4. Resolver 등록 안 함 = 적용 안 됨. WebMvcConfigurer.addArgumentResolvers.

관련 위키

이 글의 용어 (5개)
[Spring MVC] Form Handling: @ModelAttribute, DataBinder, conversionspring
정의 Form Handling = HTML form 의 요청 파라미터 → Java 객체 변환 + 검증 + 에러 처리. 핵심: + + . 기본 흐름 자세한 BindingResult…
[Spring MVC] Model + BindingResult: 뷰 모델과 바인딩 오류 처리spring
정의 Model = 컨트롤러 → 뷰 (Thymeleaf, JSP) 로 데이터 전달. BindingResult = 요청 파라미터 → 객체 바인딩 결과 + 오류. 둘 다 Spring…
[Spring] DispatcherServlet: Front Controller 내부spring
정의 가 / 사용법 을 다룬다면, 이 페이지는 그 모든 요청을 받아 처리하는 의 내부 메커니즘 을 다룬다. 은 본질적으로 하나의 Servlet (). Front Controlle…
[Spring] HandlerInterceptor: preHandle, postHandle, afterCompletionspring
정의 HandlerInterceptor 는 내부에서 handler (Controller 메서드) 실행 전후 에 끼어드는 Spring MVC 전용 메커니즘. Filter 와 비슷하…
[Spring] Spring MVC: @RestController, @RequestMappingspring
정의 Spring MVC는 Servlet 기반 동기 웹 프레임워크. 이 요청을 받아 핸들러(컨트롤러 메서드)에 라우팅. REST API와 전통 MVC(HTML) 모두 지원. We…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기