자바 백엔드 입문 30편. 22편의 @PathVariable·@RequestParam 뒷단에서 동작하는 HandlerMethodArgumentResolver 메커니즘과 직접 @LoginUser 커스텀 어노테이션 만들기를 통역사 비유로 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 30편이에요. 29편 요청 데이터 추출에서 @PathVariable·@RequestParam·@RequestBody 같은 어노테이션이 "마법처럼 메서드 매개변수를 채워주는" 결과만 봤죠. 이번 30편은 그 뒷단에서 일하는 HandlerMethodArgumentResolver 의 정체와 직접 만드는 패턴을 풀어 가요.
ArgumentResolver가 어렵게 들리는 이유
22편을 읽고 나면 — @PathVariable Long id 가 어떻게 URL의 123 을 자바 Long 으로 만들어주는지 "마법" 으로 남아 있어요. 또 "이 자동 매핑을 우리가 만들어 쓸 수도 있나?" 가 안 잡혀요.
이 글에서는 통역사 비유로 풀어요. HTTP 요청 = "외국어 손님", 컨트롤러 메서드 매개변수 = "한국어만 알아듣는 직원", ArgumentResolver = "손님의 말을 적절히 통역해 직원에게 전달하는 통역사". 끝까지 따라오시면 @LoginUser 같은 커스텀 어노테이션 직접 만드는 표준 패턴이 한 그림에 들어와요.
HandlerMethodArgumentResolver — 매개변수 추출의 표준 인터페이스
Spring MVC가 컨트롤러 메서드를 호출할 때 — "이 매개변수를 어떻게 채울까" 의 책임을 HandlerMethodArgumentResolver 인터페이스에 위임해요.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception;
}
두 메서드만 구현하면 끝.
supportsParameter— "이 매개변수를 내가 처리할 수 있나?" 판단 (true / false)resolveArgument— "진짜 값을 만들어 반환"
Spring Boot가 시작 시 — 기본 ArgumentResolver들을 자동 등록해요. @PathVariable·@RequestParam·@RequestBody·@RequestHeader·@ModelAttribute 등 — 모두 각각의 ArgumentResolver가 처리.
컨트롤러 호출 시 동작 흐름
브라우저가 GET /api/orders/123 보냈을 때 흐름을 더 자세히.
[1] DispatcherServlet → HandlerAdapter
[2] HandlerAdapter가 메서드의 모든 매개변수를 순회
[3] 각 매개변수마다 등록된 ArgumentResolver들에 "supportsParameter?" 질문
[4] true 반환한 첫 Resolver가 "resolveArgument" 호출 → 값 반환
[5] 모든 매개변수 채운 후 컨트롤러 메서드 호출
예 — getOrder(@PathVariable Long id, @RequestHeader String token) 같은 메서드.
- 첫 매개변수
Long id→PathVariableMethodArgumentResolver가 "@PathVariable박혔네" 인식 → URL에서123추출 →Long 123반환 - 두 번째 매개변수
String token→RequestHeaderMethodArgumentResolver가 "@RequestHeader박혔네" 인식 → 헤더에서 추출 →String반환
이 모든 게 메서드 호출 직전에 자동으로 일어나요.
직접 만들기 — @LoginUser 어노테이션
가장 자주 만나는 커스텀 ArgumentResolver 패턴 — "현재 로그인된 사용자를 메서드 매개변수로 자동 주입". 두 파일이 필요해요.
1. 마커 어노테이션
package com.example.myshop.auth;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER) // 매개변수에 박을 수 있음
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
4편 어노테이션 패턴 — 메타데이터만 정의.
2. ArgumentResolver 구현
package com.example.myshop.auth;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
@Component
@RequiredArgsConstructor
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final UserService userService;
@Override
public boolean supportsParameter(MethodParameter parameter) {
// @LoginUser 박혔고 타입이 User인 매개변수만 처리
return parameter.hasParameterAnnotation(LoginUser.class)
&& parameter.getParameterType().equals(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
String token = request.getHeader("Authorization");
if (token == null) {
throw new UnauthorizedException("인증 토큰 없음");
}
return userService.findByToken(token.replace("Bearer ", ""));
}
}
3. WebMvcConfigurer에 등록
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserResolver);
}
}
이 한 줄로 Spring이 우리 Resolver를 인식해 동작 준비. 기본 Resolver 목록 뒤에 추가됨.
4. 사용하기
@RestController
public class OrderController {
@GetMapping("/my-orders")
public List<Order> myOrders(@LoginUser User user) { // ← 자동 주입
return orderService.findByUser(user);
}
@PostMapping("/orders")
public Order create(@LoginUser User user, // ← 매번 박을 필요 없음
@RequestBody @Valid OrderRequest req) {
return orderService.create(user, req);
}
}
매번 @RequestHeader("Authorization") String token 받아 userService.findByToken(token) 호출하는 보일러플레이트가 @LoginUser User user 한 줄로 끝. 회사 시스템에 컨트롤러가 50개면 50번의 중복 제거.
실전 — @LoginUser 외 자주 만드는 5가지
회사 시스템에서 자주 만나는 커스텀 ArgumentResolver.
| 어노테이션 | 자동 주입 |
|---|---|
@LoginUser |
인증 사용자 객체 |
@CurrentTenant |
멀티 테넌트 시스템의 현재 테넌트 |
@ClientIp |
클라이언트 실제 IP (프록시 헤더 분석 포함) |
@UserAgent |
User-Agent 헤더 파싱 + 디바이스 정보 객체 |
@RequestContext |
요청 시각·로케일·트레이스 ID 묶은 객체 |
모두 같은 패턴 — 어노테이션 정의 + ArgumentResolver 구현 + WebConfig 등록. 한 번 만들어두면 컨트롤러 100개에서 한 줄로 활용.
supportsParameter — 조건 명확히 박기
supportsParameter 의 조건이 너무 느슨하면 — 다른 매개변수까지 처리하려고 시도해서 충돌 가능. 보통 두 가지 조건 AND.
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(LoginUser.class) // ① 어노테이션
&& parameter.getParameterType().equals(User.class); // ② 타입
}
①만 있으면 다른 타입(String·Long)에도 박혀 NPE 가능. ②만 있으면 다른 컨트롤러의 User 매개변수까지 잡힘. AND 조합이 안전.
기존 Resolver보다 먼저 동작시키기
addArgumentResolvers 로 등록하면 — 기본 Resolver 다음에 추가돼요. 같은 매개변수 패턴을 기본 Resolver가 먼저 처리할 수 있어요.
우리 Resolver를 먼저 동작시키려면 — RequestMappingHandlerAdapter 의 ResolverComposite 앞에 박아야 해요. 다만 보통 supportsParameter 조건이 다르면 충돌이 없어서 — addArgumentResolvers 한 줄로 충분해요.
Spring Security가 박혀 있으면 — @AuthenticationPrincipal 어노테이션이 비슷한 일을 자동으로 해줘요. @LoginUser를 직접 만드는 건 "Spring Security를 안 쓰는 경량 인증" 또는 "Spring Security 위에 추가 가공 필요할 때". 회사마다 정책이 달라요.
ArgumentResolver vs HandlerInterceptor
19편에서 다룬 HandlerInterceptor와 ArgumentResolver 둘 다 "요청을 전처리" 하는데, 역할이 달라요.
| HandlerInterceptor | ArgumentResolver | |
|---|---|---|
| 시점 | 컨트롤러 호출 전·후 (preHandle·postHandle) | 메서드 매개변수 채울 때 |
| 결과 | 흐름 제어 (continue / 차단) | 메서드 매개변수에 객체 주입 |
| 활용 | 인증 검증·로깅·요청 카운팅 | 인증 사용자 자동 주입·요청 컨텍스트 |
같이 쓸 수도 있어요 — Interceptor가 "인증 검증" 까지만 하고, ArgumentResolver가 "인증된 사용자 객체 주입" 처리. 책임 분리.
한 줄 정리 — HandlerMethodArgumentResolver = 컨트롤러 메서드 매개변수를 자동 채우는 표준 인터페이스. @LoginUser 같은 커스텀 어노테이션 만들 때 핵심. supportsParameter + resolveArgument 두 메서드 + WebConfig 등록 3단계.
시험 직전 한 번 더 — ArgumentResolver 입문자가 매번 헷갈리는 것
HandlerMethodArgumentResolver= 컨트롤러 매개변수 자동 채우기 표준- 두 메서드 =
supportsParameter(처리 여부) +resolveArgument(실제 값) - Spring Boot 기본 =
@PathVariable·@RequestParam·@RequestBody·@RequestHeader등 자동 등록 - 우리가 직접 만들면 =
@LoginUser·@CurrentTenant같은 커스텀 어노테이션 - 만드는 3단계 = 어노테이션 + Resolver 구현 + WebConfig 등록
- 어노테이션 =
@Target(PARAMETER) @Retention(RUNTIME) - Resolver =
@Component박아 Bean으로 WebMvcConfigurer.addArgumentResolvers에 박아야 활성화supportsParameter조건 = 어노테이션 + 타입 AND (충돌 회피)- ArgumentResolver Bean에 다른 Service·Repository 주입 OK (생성자 주입)
- 컨트롤러 메서드 호출 직전에 자동 실행
- 보일러플레이트 (매번 토큰 추출·사용자 조회) 한 줄로 압축
- 자주 만나는 5개 =
@LoginUser·@CurrentTenant·@ClientIp·@UserAgent·@RequestContext - Spring Security 사용 시 =
@AuthenticationPrincipal자동 제공 - 직접 만드는 건 "경량 인증 또는 추가 가공 필요할 때"
HandlerInterceptor와 다름 = 흐름 제어 vs 매개변수 주입- 둘 다 같이 쓰면 책임 분리 — Interceptor가 검증, Resolver가 주입
- 등록 후 컨트롤러 100개에서 한 줄로 활용 가능
- "마법처럼 동작" 의 정체 = ArgumentResolver
- 한국 회사 백엔드 = 보통 5~10개의 커스텀 ArgumentResolver
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 25편 — DispatcherServlet 요청 처리 흐름
- 26편 — Filter vs Interceptor 비교
- 27편 — @Controller @RequestMapping
- 28편 — @RestController와 JSON 응답
- 29편 — RequestParam PathVariable RequestBody
다음 글: