Study/SpringBoot

[SpringBoot] HttpSession 대신 HandlerMethodArgumentResolver 사용하기

HandlerMethodArgumentResolver란?

HandlerMethodArgumentResolver는 사용자 요청이 Controller에 도달하기 전에 그 요청에 포함된 파라미터들을 수정할 수 있게 해주는 역할을 해요.


Controller에서 @RequestBody 어노테이션을 사용해 Request의 Body 값을 받아올 때,

@PathVariable 어노테이션을 사용해 Request의 Path Parameter 값을 받아올 때 이 HandlerMethodArgumentResolver를 사용해서 값을 받아온다고 해요.


1. HttpSession을 @LoginUser로 변경하기

@PostMapping
public ResponseEntity<Object> saveChess(@RequestBody final ChessSaveRequestDto requestDto, HttpSession session) {
    String playerName = (String) session.getAttribute(USER);
    chessService.saveChess(requestDto, playerName);
    return new ResponseEntity<>(HttpStatus.CREATED);
}

원래는 위와 같이 HttpSession을 컨트롤러 메소드의 파라미터로 받아 getAttribute로 Session에 저장된 사용자 이름을 가져오는 로직이었어요.

이방법도 괜찮아 보이긴 하지만, Session이 필요한 모든 곳에 아래와 같은 로직이 필요해져요.(끔찍)

String playerName = (String) session.getAttribute(USER);

애노테이션 작성

import java.lang.annotation.*;

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

    boolean required() default true;
}

각 애노테이션에 관련된 설명은 링크로 남길게요 [SpringBoot] 커스텀 애노테이션으로 Password규칙 적용하기

간략하게 @Target은 해당 애노테이션이 어디에 사용될 수 있는지를 정의하고(현재는 파라미터에만),

@Retention은 언제까지 유지할 지에 대한 속성(현재는 란타임 기간동안)이에요.

컨트롤러 수정

@PostMapping
public ResponseEntity<Object> saveChess(@RequestBody final ChessSaveRequestDto requestDto, @LoginUser User user) {
    chessService.saveChess(requestDto, user);
    return new ResponseEntity<>(HttpStatus.CREATED);
}

이렇게 작성한 애노테이션을 붙여주기만 해도 사용할 수 있으면 참 좋을텐데..!

애노테이션이 어떻게 동작하는지에 대한 방법이 없기에 지금 이 메서드는 동작하지않아요.

이때 필요한 것이 HandlerMethodArgumentResolver예요.

위에서 한번 설명했듯이 HandlerMethodArgumentResolver는 사용자 요청이 Controller에 도달하기 전에 그 요청에 포함된 파라미터들을 수정할 수 있게 해주는 역할을 해요.


@ResponseBody는 스프링 프레임워크가 이미 동작하게 작성해뒀을 거에요.

저흰 @LoginUser를 동작하게 해보죠.


2. HandlerMethodArgumentResolver 구현

import chess.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private UserDao userDao;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(LoginUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String playerName = (String) webRequest.getAttribute("user", WebRequest.SCOPE_SESSION);
        return userDao.findByName(playerName).get();

    }
}

HandlerMethodArgumentResolver를 구현하는 LoginUserArgumentResolver클래스를 생성했고, 구현해야할 2개의 메소드를 재정의 했어요.

supportsParameter

현재 파라미터를 Resolver가 적용 가능한지 검사하는 역할을 헤요.

여기서는 해당 컨트롤러의 파라미터에 LoginUser 어노테이션이 붙어 있는지 검사해서 boolean 값을 리턴해요.

true면 resolveArgument가 실행돼요.

resolveArgument

파라미터와 기타 정보를 받아서 실제 객체를 반환해요.

여기서는 webRequest에 담긴 세션에 담긴 유저 이름을 가져오고, 이를 DB에 쿼리를 날려 실제 User를 가져오게 했어요.

WebRequest.SCOPE_SESSION을 통해서 Request의 세션을 가져왔어요.

3. Mvc Config에 등록

클래스만 만들어 놓으면 스프링 컨테이너가 어떻게 사용할지 모르겠죠? 그렇기 때문에 Configuation에 등록해줍니다.

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Bean
    public LoginUserArgumentResolver argumentResolver() {
        return new LoginUserArgumentResolver();
    }

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

위와 같이 등록하면, 이제 전체 애플리케이션에서 @LoginUser를 사용할 수 있어요.


번외

스프링 공식 홈페이지에서는 Mvc Config를 등록할 때 아래와 같이 지정해요.

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
}

하지만 저는 @EnableWebMvc를 사용하지 않았어요

이유가 뭘까요?

스프링 부트는 @EnableAutoConfiguration 애노테이션을 통해 클래스 경로 설정, 기타 Bean 및 다양한 속성 설정을 기반으로 Bean을 자동적으로 추가해줘요.
(/org.springframework.boot/spring-b/spring-boot-autoconfigure-2.4.3.jar!/META-INF/spring.factories)

이때 spring-webmvc 경로가 자동적으로 들어가기 때문에. @EnalbeWebMvc가 없어도 잘 동작을 하는거에요(이미 등록되어있기 때문에)

제일 중요한 요점은, 스프링 부트를 사용할 때 @Configuration와 @EnableWebMvc를 같이 쓰면 안된다는 점이에요

이 경우 Spring MVC는 자체 직렬화 / 역 직렬화 구성을 로드하고 사용하여 Spring Boot 구성을 무시한다고 해요.


Refer

https://docs.spring.io/spring-framework/docs/5.0.2.RELEASE/kdoc-api/spring-framework/org.springframework.web.method.support/-handler-method-argument-resolver/index.html

https://velog.io/@kingcjy/Spring-HandlerMethodArgumentResolver의-사용법과-동작원리

https://www.baeldung.com/spring-mvc-custom-data-binder#1-custom-argument-resolver

https://stackoverflow.com/questions/51008382/why-spring-boot-application-doesnt-require-enablewebmvc

`