T_era

매번 인증 코드를 넣어야하는데 이를 하나로 해결해주는 ArgumentResolver를 사용해보자 본문

Programing/Spring

매번 인증 코드를 넣어야하는데 이를 하나로 해결해주는 ArgumentResolver를 사용해보자

블스뜸 2025. 5. 22. 16:49

토큰과 세션을 통해 사용자 인증을 하는 기능을 구현했는데 URI마다 매번 인증코드를 넣는 것이 아쉬워서 어떤 방법이 있을지 찾아보다가 ArgumentResolver라는 기능을 알게 되었다. 이 ArgumentResolver가 어떤 동작을 해서 이 문제를 해결하는지 또 어떻게 적용시키는지 정리해보았다

Spring의 ArgumentResolver는 요청 파라미터를 메소드의 인자로 변환하는 기능을 제공한다.
이는 HandlerMethodArgumentResolver 인터페이스를 구현하여 사용한다.

ArgumentResolver의 역할

  • 요청 데이터 바인딩: 클라이언트로부터 전달된 HTTP 요청의 다양한 데이터(쿼리 파라미터, HTTP 바디, 헤더, 쿠키, 세션 등)를 컨트롤러 메소드의 특정 타입의 인자로 자동으로 변환하여 바인딩한다. 스프링에서 기본으로 제공하는 @RequestParam, @RequestBody, @CookieValue, @RequestHeader 등은 모두 내부적으로 ArgumentResolver를 통해 동작한다.
  • 중복 코드 제거: 여러 컨트롤러에서 공통적으로 필요한 데이터 추출 및 가공 로직을 ArgumentResolver에 캡슐화하여 컨트롤러 코드의 중복을 줄이고 간결하게 유지한다. 예를 들어, 로그인한 사용자 정보를 세션이나 JWT 토큰에서 추출하여 컨트롤러 메소드의 특정 객체로 자동 주입하는 등의 경우에 유용하다.
  • 컨트롤러의 책임 분리: 컨트롤러는 비즈니스 로직에만 집중하고, 요청 파라미터 처리 및 바인딩에 대한 책임은 ArgumentResolver에게 위임하여 컨트롤러의 역할을 명확하게 분리한다.

ArgumentResolver 동작 방식

  1. 요청 수신: 클라이언트의 HTTP 요청이 DispatcherServlet으로 전달된다.
  2. 핸들러 매핑: DispatcherServlet은 HandlerMapping을 통해 요청 URL에 해당하는 컨트롤러 메소드를 찾는다.
  3. 핸들러 어댑터: DispatcherServlet은 찾은 컨트롤러를 실행할 HandlerAdapter를 결정한다.
  4. ArgumentResolver 호출: HandlerAdapter는 컨트롤러 메소드의 파라미터들을 분석하고, 등록된 HandlerMethodArgumentResolver들을 순회하며 각 파라미터를 지원하는 ArgumentResolver를 찾는다.
  5. supportsParameter() 호출: 각 HandlerMethodArgumentResolver는 supportsParameter(MethodParameter parameter) 메소드를 통해 현재 파라미터를 자신이 처리할 수 있는지 여부를 반환한다.
  6. resolveArgument() 호출: supportsParameter()에서 true를 반환한 ArgumentResolver의 resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 메소드가 호출된다. 이 메소드에서 실제 요청으로부터 필요한 데이터를 추출하고, 원하는 타입의 객체로 변환하여 반환한다.
  7. 파라미터 주입: resolveArgument()가 반환한 객체가 컨트롤러 메소드의 해당 파라미터에 주입되어 메소드가 실행된다.

ArgumentResolver 구현 방법

  1. HandlerMethodArgumentResolver 인터페이스 구현:
    • supportsParameter(MethodParameter parameter): 이 메소드에서 어떤 파라미터 타입이나 어노테이션에 대해 ArgumentResolver가 동작할지 조건을 정의한다. 예를 들어, @LoginUser라는 커스텀 어노테이션이 붙고 User 타입인 경우에만 처리하도록 설정할 수 있다.
    • resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory): 이 메소드에서 실제 요청(NativeWebRequest를 통해 HttpServletRequest에 접근 가능)으로부터 데이터를 추출하고, 파라미터 타입에 맞게 가공하여 반환한다.
  2. Spring MVC에 등록:
    • 구현한 ArgumentResolver를 스프링 설정 클래스(WebMvcConfigurer를 구현한 클래스)에서 addArgumentResolvers() 메소드를 오버라이드하여 등록한다.

예시

// 1. 사용자 정보를 담을 DTO
public class LoginUserDto {
    private Long id;
    private String username;
    // getter, setter
}

// 2. 커스텀 어노테이션 정의
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}

// 3. HandlerMethodArgumentResolver 구현체
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // @LoginUser 어노테이션이 붙어있고, LoginUserDto 타입인 경우에만 지원
        boolean hasLoginUserAnnotation = parameter.hasParameterAnnotation(LoginUser.class);
        boolean hasLoginUserDtoType = LoginUserDto.class.isAssignableFrom(parameter.getParameterType());
        return hasLoginUserAnnotation && hasLoginUserDtoType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
        HttpSession session = request.getSession(false); // 세션이 없으면 새로 생성하지 않음

        if (session == null) {
            return null; // 세션이 없으면 null 반환 (예: 비로그인 상태)
        }

        // 세션에서 로그인 사용자 정보 추출
        return session.getAttribute("loginMember"); // "loginMember"는 세션에 저장된 키
    }
}

// 4. ArgumentResolver 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final LoginUserArgumentResolver loginUserArgumentResolver;

    public WebConfig(LoginUserArgumentResolver loginUserArgumentResolver) {
        this.loginUserArgumentResolver = loginUserArgumentResolver;
    }

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

// 5. 컨트롤러에서 사용
@RestController
public class MyController {

    @GetMapping("/api/myinfo")
    public String getMyInfo(@LoginUser LoginUserDto loginUser) {
        if (loginUser == null) {
            return "로그인된 사용자가 없습니다.";
        }
        return "현재 로그인 사용자 ID: " + loginUser.getId() + ", 이름: " + loginUser.getUsername();
    }
}