Hanbit the Developer

[Spring Boot] ArgumentResolver로 Controller 간소화하기 본문

Back-end

[Spring Boot] ArgumentResolver로 Controller 간소화하기

hanbikan 2024. 8. 3. 18:30

기존 코드

@PostMapping
public ApiResponse<Res> createChat(@RequestBody Req req, @RequestHeader("Authorization") String authorization) {
    // 토큰 유효성 검증
    if (!jwtUtil.validateAuthorizationHeader(authorization)) {
        throw new InvalidAuthorizationHeaderException();
    }

    // 토큰에서 사용자 추출하기
    String token = jwtUtil.extractTokenFromAuthorizationHeader(authorization);
    String username = jwtUtil.extractUsername(token);

    User user = userService.getUserByName(username);

    log.info("user id = {}", user.getId());
    // (나머지 코드)
}

위 코드에선 Authorization 헤더에서 JWT를 통해 username을 추출하고 UserService에서 사용자를 가져오고 있습니다. 10줄이나 되는 이 로직은 다른 함수에서도 자주 쓰이게 될 일반적인 코드이므로 간소화하고자 합니다.

결과 코드

탑다운으로 생각해봅시다. 제가 원하는 이상적인 코드는 아래와 같습니다.

@PostMapping
public ApiResponse<Res> createChat(@RequestBody Req req, @LoggedInUser User user) {
    log.info("user id = {}", user.getId());
    // (나머지 코드)
}

즉 @LoggedInUser라는 커스텀 어노테이션을 넣으면 기존 코드를 모두 대체하는 것입니다.

구현 방법

요청 메시지를 분석하고 처리하여 컨트롤러 메서드에 파라미터로 User를 넣어줘야 하므로, Argument Resolver를 구현하여 스프링 MVC에 추가하고자 합니다.

Argument Resolver란?

Argument Resolver(HandlerMethodArgumentResolver)는 Spring MVC에서 컨트롤러 메서드의 파라미터를 자동으로 변환하고 주입해주는 편의 기능을 제공합니다.

작동 과정은 다음과 같습니다:

  1. HandlerAdapter가 요청을 처리할 때 컨트롤러 메서드(우리가 평소에 작성하는 그 함수)의 파라미터를 확인합니다.
  2. 각 파라미터에 대해 HandlerMethodArgumentResolver의 supportsParameter 메서드를 호출하여 지원 여부를 확인합니다.
  3. 지원하는 Resolver가 있으면 resolveArgument 메서드를 호출하여 파라미터 값을 생성합니다.
  4. 변환된 파라미터와 함께 컨트롤러 메서드를 호출합니다.

@LoggedInUser 어노테이션 생성

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

함수 파라미터 및 런타임에 적용시킬 어노테이션이므로 위와 같이 작성하였습니다.

HandlerMethodArgumentResolver

구현하게 될 HandlerMethodArgumentResolver 인터페이스입니다.

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);

    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

구현해야 할 함수가 두 개 있습니다:

  • supportsParameter: 컨트롤러 메서드의 각 파라미터에 이 함수가 호출되며, 지원하는 파라미터면 true를 반환합니다. @LoggedInUser 어노테이션이 붙어있는지, 파라미터 타입이 User인지를 확인할 예정입니다.
  • resolveArgument: 주어진 인자를 기반으로 User를 반환하면 됩니다.

LoggedInUserArgumentResolver.java

@Component
@RequiredArgsConstructor
public class LoggedInUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final JwtUtil jwtUtil;
    private final UserService userService;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(LoggedInUser.class) != null
                && parameter.getParameterType().equals(User.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        String authorization = webRequest.getHeader("Authorization");

        // 토큰 유효성 검증
        if (!jwtUtil.validateAuthorizationHeader(authorization)) {
            throw new InvalidAuthorizationHeaderException();
        }

        // 토큰에서 사용자 추출하기
        String token = jwtUtil.extractTokenFromAuthorizationHeader(authorization);
        String username = jwtUtil.extractUsername(token);

        return userService.getUserByName(username);
    }
}

WebMvcConfigurer

작성한 Argument Resolver를 스프링 MVC에 추가합니다. 이를 위해 WebMvcConfigurer 인터페이스를 구현합니다.

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final LoggedInUserArgumentResolver loggedInUserArgumentResolver;

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

Controller에 적용하기

이제 컨트롤러에 적용하기만 하면 됩니다. @LoggedInUser를 적용해서 User를 다이렉트로 받을 수 있습니다.

@PostMapping
public ApiResponse<Res> createChat(@RequestBody Req req, @LoggedInUser User user) {
    log.info("user id = {}", user.getId());
    // (나머지 코드)
}

 

실제로 돌려봐도 잘 작동하는 것을 확인할 수 있었습니다.

마치며

Spring MVC의 Argument Resolver 덕분에 코드를 대폭 감소시킬 수 있었습니다. 또한 코드의 재사용성을 높이고 유지보수를 더욱 용이하게 만들었습니다.