Spring Custom Argument Resolver 적용하기

Jaeyoung·2022년 11월 30일
0
post-thumbnail

이제 막 게시판을 만들면서 로그인과 회원가입을 드디어 구현하게 되었다. 하지만 크나큰 귀찮은 점이 생겨 버렸다. 나는 세션ID를 통해 로그인한 유저를 관리하게 되었는데 세션 어트리뷰트에 저장하는게 아닌 따로 다중 로그인을 제어하기 위해 LoginRepository를 만들어서 저장하게 되었다. 그렇다보니 어노테이션을 통해 Method Argument로 받아올 수 없었다. 그래서 Method Argument로 받아오기 위한 방법을 찾아보다 ArgumentResolver에 대해 알게 되었다. 스프링 부트에는 ModelAttribute, RequestBody 등을 처리할 수 있는 ArgumentResolver들이 등록이 되어있다. 그래서 나는 HanlderMethodArgumentResolver를 통해 Custom으로 로그인 유저정보를 Method Argument로 처리할 수 있도록 만들어 보려고한다.

HandlerMethodArgumentResolver

일단 Controller에 존재하는 Method Argument를 처리해주기 위해서는 HandlerMethodArgumentResolver에 대해 알아야 할 필요가 있다. 그럼 해당 Resolver는 뭐냐 싶을건데 일단 이것은 구현체가 아닌 인터페이스다. 해당 인터페이스 이름을 보면 어느정도 유추가 가능하다 Handler 즉 컨트롤러 Method Argument Resolver 컨트롤러에 있는 메소드 파라미터들을 처리해 주는 Resolver다 그래서 이걸 Custom으로 구현하고 등록하면 Http 요청마다 처리 해주어야하는 여러 보일러 플레이트 코드가 생기는 문제를 해결할 수 있다.

HandlerMethodArgumentResovler가 구현해야하는 메소드에 대해 한번 알아보자 구현해야하는 메소드는 2가지가 있는데 supportsParameter, resolveArgument이다.

supportsParameter는 해당 파라미터를 처리할 수 있는 Resolver인지 체크하기 위한 메소드이다. 그래서 해당 함수의 Argument로는 MethodParameter가 넘어온다. 그래서 우리는 MethodParameter를 통해 해당 파라미터가 어떤 타입인지 어떤 어노테이션을 달고 있는지 등등 파라미터에 대한 정보를 보고 처리할 수 있는 파라미터인지 체크하는 로직을 작성하면 되는 것이다.

resolveArgument는 supportsParameter를 통과하고 이제 해당 파라미터를 우리가 원하는 대로 Binding 시켜 return 시키는 메소드이다. 파라미터를 처리할 수 있게 여러가지 Argument를 전달해주는데 일단 아까 위에서 말한 MethodParameter, 현재 요청에 대한 Model과 View에 대한 정보를 가지고 있는 ModelAndViewContainer, Web 요청 정보에 대한 WebRequest 인터페이스를 상속하고 있는 NativeWebRequest 인터페이스, WebDataBinder를 생성하는 WebDataBinderFactory 인터페이스를 전달해 준다.

코드로 보면 아래와 같다.

public interface HandlerMethodArgumentResolver {

	/**
	 * Whether the given {@linkplain MethodParameter method parameter} is
	 * supported by this resolver.
	 * @param parameter the method parameter to check
	 * @return {@code true} if this resolver supports the supplied parameter;
	 * {@code false} otherwise
	 */
	boolean supportsParameter(MethodParameter parameter);

	/**
	 * Resolves a method parameter into an argument value from a given request.
	 * A {@link ModelAndViewContainer} provides access to the model for the
	 * request. A {@link WebDataBinderFactory} provides a way to create
	 * a {@link WebDataBinder} instance when needed for data binding and
	 * type conversion purposes.
	 * @param parameter the method parameter to resolve. This parameter must
	 * have previously been passed to {@link #supportsParameter} which must
	 * have returned {@code true}.
	 * @param mavContainer the ModelAndViewContainer for the current request
	 * @param webRequest the current request
	 * @param binderFactory a factory for creating {@link WebDataBinder} instances
	 * @return the resolved argument value, or {@code null} if not resolvable
	 * @throws Exception in case of errors with the preparation of argument values
	 */
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

HandlerMethodArgumentResolver 직접 구현해보기

HandlerMethodArgumentResolver를 직접 구현해 볼것이다. 아까 말했듯이 어노테이션을 통해 Login한 유저정보를 Arugment에 전달할 수 있는 기능을 구현해보도록 하겠다. 일단 필요한 부분은 로그인한 유저 정보를 가지기 위한 세션ID, UserService가 필요할 것이고 User정보를 담는 User객체가 필요하다 또한 Login한 유저 정보를 처리하기 위한건지 판단할 수 있는 어노테이션이 필요하다. 일단 어노테이션 부터 만들도록 하겠다.

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

위와 같이 LoginUser라는 어노테이션을 만들어 주었다. 일단 Target은 파라미터에만 적용할 것이기 때문에 ElementType.PARAMETER로 설정해 주었고 Runtime시에 정보를 읽어야 하기 때문에 Retention을 Runtime으로 설정해 주었다.

이제 HandlerMethodArgumentResolver을 구현한 LoginUserMethodArgumentResolver를 구현한 코드 부터 보도록 하자

@RequiredArgsConstructor
public class LoginUserMethodArgumentResolver implements HandlerMethodArgumentResolver {
    private final UserService userService;
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean hasAnnotation = parameter.hasParameterAnnotation(LoginUser.class);
        boolean hasUserType = User.class.isAssignableFrom(parameter.getParameterType());
        return hasAnnotation && hasUserType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        User user = userService.getLoginUserById(webRequest.getSessionId()).orElse(null);
        webRequest.setAttribute("user",user,0);
        return user;
    }
}

supportsParameter를 먼저 보자면 두개의 상태를 나타내는 변수가 있는데 하나는 hasAnnotaion이라는 변수로 LoginUser라는 어노테이션이 파라미터에 붙어있냐 라는걸 체크하기 위한 변수이다. 또 다른 하나는 우리가 처리하려는 파라미터의 타입이 User 타입인지 체크하기 위한 변수이다. 그래서 이 두 변수가 모두 true라면 해당 파라미터는 해당 Resolver에서 처리가 가능하다.

resolveArgument도 보면 엄청 간단한데 userService를 통해 sessionId로 User 정보를 가져오는 것을 볼 수 있다. 그리고 webRequest를 통해 user에 대한 정보를 등록해주어서 컨트롤러 마다 처리해야하는 번거로운 작업을 HandlerMethodArgumentResolver를 통해 처리해 줄 수 있다.

HandlerMethodArgumentResolver **등록하기**

HandlerMethodArgumentResolver를 등록하기 위해서는 WebMvcConfigurer를 Configuration으로 등록해줘야한다. WebMvcConfigurer에 있는 addArgumentResovlers를 통해 내가 만든 ArgumentResovler를 아래와 같이 등록해줄 수 있다.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final UserService userService;
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new LoginUserMethodArgumentResolver(userService));
    }
}

등록까지 완료 했다면 아래와 같이 설정해 두면 로그인한 유저에 한해서 유저 정보를 받아 올 수 있다.

@Controller
@RequiredArgsConstructor
public class CommonController {

    @GetMapping("/")
    public String mainPage(@LoginUser User user){
        return "index";
    }
}
profile
Programmer

0개의 댓글