자바 백엔드 입문 30편 — ArgumentResolver와 @LoginUser

2026-05-17자바 백엔드 입문

자바 백엔드 입문 30편. 22편의 @PathVariable·@RequestParam 뒷단에서 동작하는 HandlerMethodArgumentResolver 메커니즘과 직접 @LoginUser 커스텀 어노테이션 만들기를 통역사 비유로 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 30편 — ArgumentResolver와 @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 idPathVariableMethodArgumentResolver"@PathVariable 박혔네" 인식 → URL에서 123 추출 → Long 123 반환
  • 두 번째 매개변수 String tokenRequestHeaderMethodArgumentResolver"@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와의 관계

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

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!