T_era
매번 인증 코드를 넣어야하는데 이를 하나로 해결해주는 ArgumentResolver를 사용해보자 본문
토큰과 세션을 통해 사용자 인증을 하는 기능을 구현했는데 URI마다 매번 인증코드를 넣는 것이 아쉬워서 어떤 방법이 있을지 찾아보다가 ArgumentResolver라는 기능을 알게 되었다. 이 ArgumentResolver가 어떤 동작을 해서 이 문제를 해결하는지 또 어떻게 적용시키는지 정리해보았다
Spring의 ArgumentResolver는 요청 파라미터를 메소드의 인자로 변환하는 기능을 제공한다.
이는 HandlerMethodArgumentResolver 인터페이스를 구현하여 사용한다.
ArgumentResolver의 역할
- 요청 데이터 바인딩: 클라이언트로부터 전달된 HTTP 요청의 다양한 데이터(쿼리 파라미터, HTTP 바디, 헤더, 쿠키, 세션 등)를 컨트롤러 메소드의 특정 타입의 인자로 자동으로 변환하여 바인딩한다. 스프링에서 기본으로 제공하는 @RequestParam, @RequestBody, @CookieValue, @RequestHeader 등은 모두 내부적으로 ArgumentResolver를 통해 동작한다.
- 중복 코드 제거: 여러 컨트롤러에서 공통적으로 필요한 데이터 추출 및 가공 로직을 ArgumentResolver에 캡슐화하여 컨트롤러 코드의 중복을 줄이고 간결하게 유지한다. 예를 들어, 로그인한 사용자 정보를 세션이나 JWT 토큰에서 추출하여 컨트롤러 메소드의 특정 객체로 자동 주입하는 등의 경우에 유용하다.
- 컨트롤러의 책임 분리: 컨트롤러는 비즈니스 로직에만 집중하고, 요청 파라미터 처리 및 바인딩에 대한 책임은 ArgumentResolver에게 위임하여 컨트롤러의 역할을 명확하게 분리한다.
ArgumentResolver 동작 방식
- 요청 수신: 클라이언트의 HTTP 요청이 DispatcherServlet으로 전달된다.
- 핸들러 매핑: DispatcherServlet은 HandlerMapping을 통해 요청 URL에 해당하는 컨트롤러 메소드를 찾는다.
- 핸들러 어댑터: DispatcherServlet은 찾은 컨트롤러를 실행할 HandlerAdapter를 결정한다.
- ArgumentResolver 호출: HandlerAdapter는 컨트롤러 메소드의 파라미터들을 분석하고, 등록된 HandlerMethodArgumentResolver들을 순회하며 각 파라미터를 지원하는 ArgumentResolver를 찾는다.
- supportsParameter() 호출: 각 HandlerMethodArgumentResolver는 supportsParameter(MethodParameter parameter) 메소드를 통해 현재 파라미터를 자신이 처리할 수 있는지 여부를 반환한다.
- resolveArgument() 호출: supportsParameter()에서 true를 반환한 ArgumentResolver의 resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) 메소드가 호출된다. 이 메소드에서 실제 요청으로부터 필요한 데이터를 추출하고, 원하는 타입의 객체로 변환하여 반환한다.
- 파라미터 주입: resolveArgument()가 반환한 객체가 컨트롤러 메소드의 해당 파라미터에 주입되어 메소드가 실행된다.
ArgumentResolver 구현 방법
- HandlerMethodArgumentResolver 인터페이스 구현:
- supportsParameter(MethodParameter parameter): 이 메소드에서 어떤 파라미터 타입이나 어노테이션에 대해 ArgumentResolver가 동작할지 조건을 정의한다. 예를 들어, @LoginUser라는 커스텀 어노테이션이 붙고 User 타입인 경우에만 처리하도록 설정할 수 있다.
- resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory): 이 메소드에서 실제 요청(NativeWebRequest를 통해 HttpServletRequest에 접근 가능)으로부터 데이터를 추출하고, 파라미터 타입에 맞게 가공하여 반환한다.
- 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();
}
}'Programing > Spring' 카테고리의 다른 글
| 스프링 시큐리티 인증 및 인가 흐름 (0) | 2025.05.26 |
|---|---|
| 쿼리를 직접적으로 작성하는 건 아쉬워서 DSL을 사용해봤다 근데 왜 업데이트가 반영이 안되지? (1) | 2025.05.23 |
| @Entity 엔티티에서 사용하는 어노테이션과 이유 (0) | 2025.05.20 |
| Spring Data JPA 인터페이스 메서드 자동 실행 원리 (0) | 2025.05.16 |
| Spring Data JPA Query Methods 및 JPA Auditing 정리 (0) | 2025.05.16 |