본문 바로가기
Programming/Java, Spring

Spring에서 argumentResolver를 사용하여 인증 책임 분리하기

by Renechoi 2023. 7. 23.

 

문제의식

 

 

다음과 같은 컨트롤러 메서드가 있다고 해보자. 

 

@PostMapping("/tasks")
public ResponseEntity<Void> createTask(@RequestBody TaskCreateReq taskCreateReq,
                                       HttpSession session) {
    final Long userId = (Long) session.getAttribute(LOGIN_SESSION_KEY);
    if (userId == null) {
        throw new RuntimeException("bad request. no session.");
    }
    taskService.create(taskCreateReq, AuthUser.of(userId));
    return ResponseEntity.ok().build();
}

 

 

 

위 코드를 보면 세션에서 LOGIN_SESSION_KEY로 저장된 사용자 ID를 가져온다. 만약 이 ID가 null이라면, 즉 세션에 로그인 정보가 없다면, 사용자는 로그인하지 않은 상태로 판단되며, 이에 대한 예외를 던진다.

 

즉 user에 대한 인증 기능을 수행하는 것이다. 

 

그런데 매번 이렇게 session에서 값을 꺼내서 인증 기능을 해주어야 할까? 번거롭고 단일책임원칙에도 위배된다. 다음과 같이 AuthUser를 단순히 받아서 사용할 수 있다면 어떨까? 

 

@PostMapping("/tasks")
public ResponseEntity<Void> createTask(@RequestBody TaskCreateReq taskCreateReq,
                                       HttpSession session,
    AuthUser authUser) {
    taskService.create(taskCreateReq, authUser);
    return ResponseEntity.ok().build();
}

 

즉, 어딘가에서 인증 기능을 미리 수행하고 AuthUser를 인자로 넘겨주는 것이다. 이것이 ArgumentResolver 개념과 연관된다. 

 

 

ArgumentResolver

 

Spring MVC에서는 ArgumentResolver를 사용하여 HTTP 요청의 파라미터를 Controller의 인자로 바로 매핑하는 기능을 제공한다. 이를 활용하면 세션으로부터 사용자 ID를 가져오고 이를 기반으로 AuthUser 객체를 생성하는 로직을 컨트롤러에서 분리할 수 있다.

 

public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {
    private final HttpSession session;

    public AuthUserArgumentResolver(HttpSession session) {
        this.session = session;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return AuthUser.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) {
        final Long userId = (Long) session.getAttribute(LOGIN_SESSION_KEY);
        if (userId == null) {
            throw new RuntimeException("bad request. no session.");
        }
        return AuthUser.of(userId);
    }
}

 

코드를 보면 AuthUserArgumentResolver는 HandlerMethodArgumentResolver 인터페이스를 구현한다. 

 

HandlerMethodArgumentResolver는 Spring에서 제공하는 인터페이스로서, HTTP 요청을 특정 컨트롤러 메서드의 매개변수로 매핑하는데 사용된다. 다시 말해, 이 인터페이스를 구현하면 컨트롤러 메서드의 특정 매개변수를 HTTP 요청에 따라 다르게 해석하여 커스터마이징된 매개변수로 재정의할 수 있다. 다음과 같이 이미 스프링에서는 다양한 구현체들을 제공하고 있다. 

 

 

커스텀 ArgumentResolver를 구현하기 위해서는 HandlerMethodArgumentResolver의 두 가지 메서드를 오버라이딩 하면 된다. 

 

public class AuthUserResolver implements HandlerMethodArgumentResolver {
   @Override
   public boolean supportsParameter(MethodParameter parameter) {
      return false;
   }

   @Override
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
      return null;
   }
}


1. supportsParameter: 이 메서드는 이 Argument Resolver가 주어진 매개변수를 해석할 수 있는지 여부를 결정한다. 즉, 어떤 종류의 매개변수에 대해서 이 Resolver를 사용할 것인지를 결정하는 메서드이다.

-> 즉 파라미터가 AuthUser 타입인지를 본다. 

2. resolveArgument: 이 메서드는 실제로 매개변수를 해석하는 작업을 수행한다. HTTP 요청을 받아 특정 타입의 객체로 변환하거나, 서비스 레이어로부터 데이터를 가져오는 등의 작업을 수행한다.

 

이렇게 구현된 Argument Resolver는 Spring MVC에 등록한다. 등록된 Resolver는 Spring MVC에 의해 관리되며, HTTP 요청이 들어올 때마다 알맞은 Resolver가 선택되어 실행되도록 한다.

 

현재 예제에서는 컨트롤러 메서드에서 AuthUser 타입의 파라미터를 사용할 때마다 이 Resolver가 실행되어, 인증된 사용자 정보를 자동으로 제공받을 수 있다.

webRequest를 통해 받도록 리팩토링하여 작성한 최종 코드는 다음과 같다. 

 

커스텀 argumentResolver 등록 코드 

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

 

커스텀 AuthUserResolver


public class AuthUserResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return AuthUser.class.isAssignableFrom(parameter.getParameterType());
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        final Long userId = (Long) webRequest.getAttribute(LOGIN_SESSION_KEY,
                WebRequest.SCOPE_SESSION);
        if (userId != null) {
            return AuthUser.of(userId);
        } else {
            throw new RuntimeException("bad request. no session");
        }
    }
}

 

 


 

참고 자료

- 패스트캠퍼스: 한 번에 끝내는 Spring 완.전.판 초격차 패키지 Online

 

반응형