Java + Spring 에서 NestJS Style Auth Guard 만들기

Spring Security, JWT 만을 사용하기에는 너무 크고 복잡하다. 간단한 Annotation으로 컨트롤러의 인증 인가를 설정하는 코드를 작성해보자.

Java + Spring 에서 NestJS Style Auth Guard 만들기

PassportJava

  • 새로운 프로젝트에서 인증, 인가 과정에 Spring Security 를 사용하는 것이 적합하다는 의사결정을 내리기는 했으나, NestJS 를 통해 백엔드를 구현했던 입장에서는 Spring Security의 사용에 다소 불편함이 있었다. 때문에, Java + Spring 에서 NestJS Style (AuthGuard + Passport.js) 으로 인증 / 인가 프레임워크를 제작해보고 Spring Security 와의 장단점을 비교해보고자 했다.
  • 내가 구현하고자 하는 시스템(?)의 목표는 다음과 같다.
    • NestJS 와 같이 @AuthGuard(Guard) 와 같은 형태로 Controller 의 method 에 대해서 가볍게 Auth 과정을 표현할 수 있을 것.
    • AuthGuard 의 경우 Strategy 의 선택을 통해서 Guard 의 작동 방식을 선택할 수 있어 유연성을 가질 수 있을 것. (Strategy pattern 적용)
    • Guard 에서 처리한 정보를 이후 Method 에서 사용할 수 있도록 Parameter를 통해서 주입할 수 있도록 할 것.
    • 사용자 입장에서는 Guard, Strategy의 정의만으로 쉽게 Auth 과정을 정의할 수 있을 것.
  • Passport.js 의 구조와 NestJS 의 구조를 따라했다는 점에서 PassportJava 라는 이름을 붙여보았다.

구조

  • Controller의 method를 대상으로 Request 처리 이전에 intercept하여 실행할 로직을 결정하는 시스템이다. 해당 시스템은 @UseGuards, @Guard 를 통해서 Guard를 선택하여 method에 적용할 수 있으며, 로직의 정의는 Strategy 를 통해 수행한다.
PassportJava 구조

@Guard

  • Controller 에서 사용자가 request 이전에 인증 / 인가 과정에 사용할 Guard를 선택할 수 있도록 하는 annotation 이다.
  • 사용법
    • value = {...<T extends AuthGuard> T.class}
    • args = {...String}
      • 주입할 인자가 없을 경우 입력하지 않고 @Guard(RoleGuard.class) 와 같이 사용해도 된다.
@Guard(value = RoleGuard.class, args = {"ADMIN"})  
public ResponseEntity<UserResDto> getById(@PathVariable("id") Long id)  
	throws NotFoundException {  
	return ResponseEntity.ok(userService.getById(id));  
}

@UseGuards

  • Controller 에서 사용자가 request 이전에 인증 / 인가 과정에 사용할 Guard여러 개 선택할 수 있도록 하는 annotation 이다.
    • Guard를 여러개 사용하는 것과 다르지는 않다.
  • 사용법
    • value = {...Guard.class}
    • args = {...String.class}
@UseGuards({  
	@Guard(JwtGuard.class),  
	@Guard(value = RoleGuard.class, args = {"ADMIN"})  
})
public ResponseEntity<UserResDto> getById(@PathVariable("id") Long id)  
	throws NotFoundException {  
	return ResponseEntity.ok(userService.getById(id));  
}

AuthGuard

  • AuthGuardUseGuard 에서 사용할 Guard의 기틀이 되는 abstract class 이다.
  • 해당 클래스는 AuthStrategyprivate member 로 가지고 있으며, 이를 상속하는 모든 Guard 객체는 super(strategy) 를 통해 해당 Guard가 사용하는 Strategy를 결정한다.
    • AuthStrategy 를 통해서 직접 사용할 수 있으나 다음과 같이 구현한 이유는 아래와 같다.
      • Guard 의 경우 어떤 형태의 Auth 과정의 Guard를 적용 함을 의미한다.
        • Guard의 적용은 결국 Controller 의 method 실행 이전 어떤 동작이 실행됨을 적용하는 것이다.
      • 예를 들어, 유저의 로그인 가능 여부를 묻는 인증 과정이라고 하자. 이를 LoginedGuard 라고 하면, 해당 인증 과정에는 JWT 를 사용한 전략이 사용될 수도 있고, Session 을 통한 전략이 사용될 수도 있다. 혹은 또 다른 전략이 선택될 수도 있다.
      • 전략이 변경된 경우에도, 해당 Guard의 역할은 로그인 검증일 뿐이므로, 바뀌지 않는다. 따라서 단순히 해당 유저를 검증하는 전략만 변경 함으로써 코드의 단 한부분만 변경하여 해당 전략의 변경이 가능하다.
      • 만약 그렇지 않다면, @UseGuards({JwtStrategy.class}) 라고 작성된 모든 method를 변경해야할 것이다.
  • 해당 클래스를 구현하는 객체는 @Component 를 통해 Spring bean 으로 등록되어야한다. GuardResolver 에서 해당 객체들을 스캔하여 가지고 있어야한다.
  • 구현
public abstract class AuthGuard {  
	private AuthStrategy authStrategy;  
	/**  
	* 상속받은 클래스는 반드시 AuthStrategy를 지정해야합니다.  
	*  
	* @param authStrategy  
	*/  
	public AuthGuard(AuthStrategy authStrategy) {  
		this.authStrategy = authStrategy;  
	}  
	  
	public final void check(HttpServletRequest request, String... args) {  
		try {  
			authStrategy.check(request, args);  
		} catch (CustomException customException) {  
			throw customException;  
		} catch (RuntimeException e) {  
			throw new CustomException(ExceptionStatus.UNAUTHORIZED_USER);  
		}  
	}  
}
  • 사용법
@Component  
public class RoleGuard extends AuthGuard {  
	  
	public RoleGuard(@Autowired RoleStrategy strategy) {  
		super(strategy);  
	}  
}

AuthStrategy

  • AuthStrategy 는 사용자가 정의할 Strategy의 기틀이 되는 interface 이다.
    • 보다 유연하게 전략을 선택하여 Guard 를 정의하도록 하였다.
  • 해당 interfaceAuthGuard 에 주입될 객체를 정의하며, implements 하는 classpublic void check(HttpServletRequest request, String... args) 를 정의해야한다.
  • 이후 해당 전략을 사용하기 위해서는 AuthGuard 를 상속한 객체의 constructor에서 사용할 Spring bean strategy를 주입할 수 있도록 JwtGuard(@Autowired JwtStrategy strategy) 와 같이 정의해야한다.
  • 구현
public interface AuthStrategy {  
	/**  
	* Check method는 해당 Auth 작업에 대한 전략을 수행합니다. 만약, 검증 과정에서 실패할 경우 Error를 throw 합니다. 해당 Error는  
	* Exception Handler 에서 처리할 수 있습니다.  
	*  
	* @param request  
	*/  
	public void check(HttpServletRequest request, String... args);  
}
  • 사용법
@Component  
public class RoleStrategy implements AuthStrategy {  
	@Override  
	public void check(HttpServletRequest request, String[] args) {  
	// role check  
	}  
}

// JwtGuard 의 정의
public class RoleGuard extends AuthGuard {
  public RoleGuard(@Autowired RoleStrategy strategy) { super(strategy); }
}

GuardAspect

  • @UseGuards 를 핸들링하기 위한 ApectJ를 사용하는 class이다. handleUseGuards() 를 통해 해당 annotation 을 선언한 메소드가 실행되기 이전에 GuardStrategy 를 실행한다.
  • GuardAspect 의 경우 GuardResolver 를 통해 Guard 객체를 찾아낸다.
  • 구현
@Aspect  
@Component  
public class GuardAspect {  
	  
	@Autowired  
	private GuardResolver guardResolver;  
	  
	@Before("@annotation(useGuards)")  
	public void handleUseGuards(UseGuards useGuards) {  
		final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
		.currentRequestAttributes())
		.getRequest();  
		final List<Guard> guards = Arrays.stream(useGuards.value()).toList();  
		for (Guard guard : guards) {  
			AuthGuard authGuard = guardResolver.getGuard(guard.value().getSimpleName());  
			authGuard.check(request, guard.args());  
		}  
	}  
	  
	@Before("@annotation(guard)")  
	public void handleGuard(Guard guard) {  
		final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder  
		.currentRequestAttributes()).getRequest();  
		final AuthGuard authGuard =  
		guardResolver.getGuard(guard.value().getSimpleName());  
		authGuard.check(request, guard.args());  
	}  
}

GuardResolver

  • Spring Bean 으로 등록된 객체 중 AuthGuard 타입의 객체를 찾아 저장하고, 이를 이후에 GuardAspect 에 전달하는 역할을 수행한다.
  • 구현
  
@Component  
public class GuardResolver {  
	  
	private final Map<String, AuthGuard> guards;  
	  
	public GuardResolver(List<AuthGuard> guards) {  
		this.guards = guards.stream()  
			.collect(  
				Collectors.toMap(  
				g -> g.getClass().getSimpleName(),  
				Function.identity()  
			));  
	}  
	  
	public AuthGuard getGuard(@NonNull String guardName) {  
		return this.guards.get(guardName);  
	}  
}

UserData

  • Guard 를 통해서 Session 혹은 JWT 정보를 가져올 수 있다. 해당 경우를 위한 지원으로 HttpServletRequest 객체를 통해서 유저의 데이터를 parameter 를 통해 주입하는 역할을 수행하는 annotation 이 필요하다.

@UserData

  • Guard 에서 주입된 정보를 사용하며, 이 정보는 HttpServletRequestUSER_DATA attribute 를 기본으로 한다. 만약 다른 형태의 attribute 를 사용하고자 하는 경우, @UserData value 값을 지정해야한다.
    • 해당 값은 외부 Configure 을 통한 주입을 추천하며, HttpServletRequestattribute 와 겹치지 않도록해야한다. SNAKE_CASE(UPPER CASE) 를 추천한다.

예시

  • Guard
@Component  
public class JwtStrategy implements AuthStrategy {  
	@Value("jwt.data.name")  
	private String dataName = "JWT_DATA";  
	  
	@Override  
	public void check(HttpServletRequest request, String... args) {
		String token = getTokenFromRequest(request);
		request.setAttribute(dataName, token);  

	}  
	  
	private String getTokenFromRequest(@NonNull HttpServletRequest request) {  
		// Get Bearer token from authorization  
		return request.getHeader(HttpHeaders.AUTHORIZATION);  
	}  
}
  • UserData 적용
// controller code
@Value("jwt.data.name")
private String jwtDataName;

//...

@GetMapping("/{id}")
public boolean someMethod(
	@PathVariable("id") Long id,  
	@UserData(jwtDataName) String token, // token is injected
	@UserData JwtData data // value 없이도 가능. default "USER_DATA" 사용.
	) throws NotFoundException {
	return ResponseEntity.ok(userService.getById(id, token));  
}

UserDataAspect

  • @UserData 를 핸들링하기 위한 ApectJ를 사용하는 class이다. handleUserData() 를 통해 해당 annotation 을 선언한 parameter 값에 해당 Object 를 주입한다.
  • 해당 annotation을 사용할 때 반드시 Strategy에서 사용하는 타입과 일치해야한다.
  • 구현

@Aspect
@Component
public class UserDataAspect {

  /**
   * TokenData annotation 을 적용한 Parameter 에 대해서 Request의 TokenData Attribute 를 가져옵니다. Pointcut :
   * method(params.., @TokenData(Type data), params..)
   * <p>
   * TokenData 의 경우 v
   *
   * @param joinPoint
   * @return
   * @throws Throwable
   */
  @Around("execution(* *(.., @com.ticketwar.ticketwar.auth.pass.UserData (*), ..))")
  public Object handleUserData(ProceedingJoinPoint joinPoint)
      throws Throwable {
    final Signature signature = joinPoint.getSignature();
    final MethodSignature methodSignature = (MethodSignature) signature;
    final Method method = methodSignature.getMethod();
    final Parameter[] parameters = method.getParameters();
    final Object args[] = joinPoint.getArgs();
    final HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
        .currentRequestAttributes()).getRequest();

    for (int idx = 0; idx < parameters.length; ++idx) {
      UserData userData = parameters[idx].getAnnotation(UserData.class);
      if (userData != null) {
        args[idx] = request.getAttribute(userData.value());
        break;
      }
    }
    return joinPoint.proceed(args);
  }
}

JWT 적용하기

JWT 의 경우 auth0:java-jwt 라이브러리를 활용하였다.

(단순한 PassportJava 적용 예시)

JwtConfigure

  • Spring Bean 으로, 외부 변수를 주입하기 위함이다.
    • jwt.data.name
      • @UserData 에 저장할 requestattribute 이름을 지정함.
    • jwt.secret
      • JWT secret 지정.

@Component
public class JwtConfigure {
  @Value("${jwt.data.name:USER_DATA}")
  private String dataName;

  @Value("${jwt.secret}")
  private String secret;

  public String getDataName() {
    return dataName;
  }

  public String getSecret() {
    return secret;
  }
}

JwtUtil

  • SpringBean (Configure 사용 및 타 Bean 에서 autowire 사용을 위함.) 으로, JWT 토큰 획득 과정과 관련된 method를 담는다.
  • verifyTokenFromRequestAuthorizationHeader
    • HttpServletRequest 로 부터 Bearer Token 을 획득하고, 이를 JwtData 형태로 변환한다.
  • 이후 다른 형태로 JWT 가 필요할 경우, 해당 형태로 가져와서 사용할 수 있도록 method를 추가할 것이다.

@Component
public class JwtUtil {
  private final String BEARER = "BEARER ";
  @Autowired
  private JwtConfigure jwtConfigure;

  /**
   * Verify Authorization header's bearer token.
   * <p>
   * if fails, it throws RuntimeException.
   *
   * @param request
   * @return
   */
  public JwtData verifyTokenFromRequestAuthorizationHeader(HttpServletRequest request) {
    final String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION).substring(BEARER.length());
    final Algorithm algorithm = Algorithm.HMAC256(jwtConfigure.getSecret());
    final JWTVerifier jwtVerifier = JWT.require(algorithm).build();
    final DecodedJWT decodedJWT = jwtVerifier.verify(bearerToken);

    return getJwtDataFromDecodedJwt(decodedJWT);
  }

  private JwtData getJwtDataFromDecodedJwt(DecodedJWT decodedJWT) {
    final Claim id = decodedJWT.getClaim("id");
    final Claim email = decodedJWT.getClaim("email");
    final Claim nickname = decodedJWT.getClaim("nickname");
    final Claim role = decodedJWT.getClaim("role");

    return JwtData.builder()
        .id(id.asLong())
        .email(email.asString())
        .nickname(nickname.asString())
        .role(role.asString())
        .build();
  }
}

JwtStrategy

@Component
public class JwtStrategy implements AuthStrategy {
  @Autowired
  private JwtConfigure jwtConfigure;
  @Autowired
  private JwtUtil jwtUtil;

  @Override
  public void check(HttpServletRequest request, String... args) {
    final JwtData jwtData = jwtUtil.verifyTokenFromRequestAuthorizationHeader(request);
    request.setAttribute(jwtConfigure.getDataName(), jwtData);
  }

}

JwtGuard

@Component
public class JwtGuard extends AuthGuard {
  public JwtGuard(@Autowired JwtStrategy strategy) {
    super(strategy);
  }
}

개발 과정 중 문제

Spring bean 획득과 new ()를 통한 Object

  • AuthGuard 의 설계 초기에는 super(new JwtStrategy()) 와 같은 형태를 통해서 Strategy 를 주입하였다. Spring 과 상관 없이 Strategy 를 제작을 하고자 함이었다.
@Component  
public class JwtGuard extends AuthGuard {  
	  
	public JwtGuard() {  
		super(new JwtStrategy());  
	}  
}
  • 그러나 JwtStrategy 를 만들면서, 실패했다. JwtStrategy 의 경우 필연적으로 secret 을 외부로부터 주입받아야하는데, 보통 그 수단으로 Spring의 @Value 를 통해서 값을 주입받는다. 그러나, 이는 Spring bean 에 의해서만 가능하다.
  • 따라서 어쩔 수 없이, @Component 를 선언하고, Spring bean 에 등록을 하였으나, jwt.secret 의 값을 제대로 가져오지 못했다. 위 코드를 보면 알 수 있듯, JwtStrategynew 를 통해서 새로 인스턴스를 만들고 주입하기에, Bean 과 관련없는 객체가 주입이되었다.
  • AuthGuardconstructor 에서 직접 bean 을 찾아 넣는 방식도 시도하였으나, 적절하지도 않으며, 해당 시점에는 ApplicationContext가 완성되지 않아 활용할 수도 없다. (되도록이면 접근하지 않는 것이 맞다고 생각한다.) 이렇게 할 경우, Spring 자체에도 의존성이 생겨버린 상태기 때문에 의미가 없었다.
  • 애초에 해당 시스템 자체가 Spring에 의존을 하고 있기 때문에, Strategy를 Component로 등록하지 않을 이유는 없었다. 따라서, 항상 AuthStrategy 구현 객체는 Spring Bean 에 등록하고, @Autowired 를 통해서 이를 등록하도록 했다.
  • 결과적으로는 NestJS + Passport 의 방식에 비하면 조금 복잡하기는 하나, AuthGuard 의 유연성을 지키고, 몇 줄 안되는 코드로 AuthStrategy 를 결정할 수 있는 방식이 되었다. 나름 깔끔하게 사용자가 선언할 수 있어 마음에 든다.
@Component  
public class JwtGuard extends AuthGuard {  
	  
	public JwtGuard(@Autowired JwtStrategy strategy) {  
		super(strategy);  
	}  
}

@Guard, @UseGuards에서 arguments

@UseGuards({JwtGuard.class, RoleGuard.class})
  • 초기에는 @Guard 없이 @UseGuards 만으로 구성했다. 또한, 인자를 받지 않았다.
  • JwtGuard 의 경우, JWT verification 만 하고 끝내면 되니, 특별한 인자가 필요없지만, RoleGuard 의 경우 해당 method 의 접근 권한을 설정해야하기 때문에 문제가 발생한다.
  • 또한 다른 custom guard 를 만들 경우에서도 인자를 받을 수 없어 유연성이 부족하다. 권한마다 매번 Guard를 만들 수는 없다.
@Target({ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)  
public @interface UseGuards {  
  
	Class[] value();  
	String[] args();
}

@UseGuards(value = {JwtGuard.class, RoleGuard.class}, args = {null, "ADMIN"})
  • 그렇다고, 위와 같이 args를 순서대로 매핑하는 느낌으로 사용하는 것도 바람직하지 않다. 또한 여러 인자를 한 Guard에 전달할 수도 없다.
  • 따라서, 각 AuthGuard는 해당 클래스와 인자를 전달하는 @Guard 를 통해서 사용하도록 했다.
@Target({ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)  
public @interface Guard {  

	Class value();  
	  
	String[] args() default {};  
}
  • 이를 통해서 Guard 는 각각의 인자를 받아올 수 있게 되었다.
  • 또한, 여러개의 Guard를 순차적으로 실행하는 것도 필요하기에 @UseGuards 는 다음과 같이 고쳤다.
/**  
* UseGuards  
*/  
@Target({ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)  
public @interface UseGuards {  
  
	Guard[] value();  
}

@GetMapping("/{id}")  
@UseGuards({  
	@Guard(JwtGuard.class),  
	@Guard(value = RoleGuard.class, args = {"ADMIN"})  
})  
public ResponseEntity<UserResDto> getById(){...}
  • 다소 복잡하기는 하나, 필요한 요소들을 모두 표현할 수 있다. 다만, String을 통해서만 인자를 받아올 수 있기 때문에 아쉬운 면이 있다.
    • UserRole.ADMIN 과 같이 설정한다면 더욱 편리할 것이다. 하지만, annotation에서 Enum의 경우 특정해야만 사용 가능하여, 다양한 형태를 받아야하는 현재로서는 어쩔 수 없었다.

Spring Security 와의 비교

직접 구현 이전에는, Spring security 의 예시만 알고 있기 때문에 직접 개발한 형태의 방식은 어떤 장점과 단점을 가질 지에 대해서 명확하게 정의를 내리기 어려웠다. 직접 구현 이후 Spring security 의 적용에 비해서 가질 수 있는 장단점을 간략하게 생각해보았다.

장점

  • 상대적으로 쉽다.
    • 이는 자체적으로 구조가 단순하기 때문이다. Spring security 의 경우 다양한 보안 관련 기능을 지원해야하고, 이로 인해서 어쩔 수 없이 복잡한 구조를 가져야한다. (실제로 사용자가 사용하지 않는 기능들 까지 담아야하므로...) 반면, 해당 PassportJava 의 경우 Controller method의 실행 이전 intercept 하여 실행할 내용을 정의하는 것을 지원하는 것 뿐이다. 구조적으로 단순하니, 쉬울 수 밖에 없다.
    • 또한, 개발자가 직접 해당 기능에 대한 작동을 정의하기 때문에 상대적으로 그 코드를 작성한 개발자에게 있어서는 이해가 쉬울 것이다. 그러나, 팀 단위로 개발하는 환경을 고려하면 정형화되어 있는 형태로 작성해야하는 생태계 (Spring security)가 오히려 코드와 구조를 이해하는 측면에서는 나을 것 같다.
  • 직접 제어 가능하다.
    • Spring security 와 같은 형태의 이미 주어진 프레임워크에서는 어쩔 수 없이, 제한된 환경에서 해당 구조에 알맞게 코드를 작성해야한다. 물론 어느정도 유연하게 작성할 수는 있겠으나, 프레임워크가 제공하는 시점 아래에서 코드가 작성되어야하며 디테일한 조정은 불가하다.
    • 반면, 이렇게 직접 작성하는 경우 개발자가 원하는 방식으로 모두 제어할 수 있다. "불만이 있으면 직접 뛰던가" 의 예시...? 결국에는 Spring Security 가 등장하기 이전에는 어쩔 수 없이 이렇게 코드를 작성해야한 했을 것이다.

단점

  • 다양한 기능 지원의 부재
    • "쉽다/단순하다" 에서 비롯되는 어쩔 수 없는 단점이다. 필요한 기능이 있을 경우 개발자가 직접 지원하도록 만들어야한다. 시간적인 측면에서 문제가 있다.
  • 안정성
    • 거대하고 안정된 커뮤니티에서 만들어진 프레임워크를 사용하는 것에 비하면 안정성에서 확실하지 않다. 이는 이러한 형태의 시스템을 만드는 사람에 따라 다르겠으나, 아무래도 Spring Security 에 비하면 부족함은 사실이다.
  • (내 구조의 경우) 여전히 Spring에 의존한다.
    • Spring bean 이 되어 해당하는 AuthGuard를 찾아내고, 적합한 Strategy를 주입한다. 이로 인해 Spring 으로부터 독립된 패키지가 아니다. 다른 Java 의 Framework 에 적용할 수 없다.
  • 결국에는 Spring Security 형태를 따라갈 수 밖에 없다.
    • 이번에 PassportJava를 만들어내면서, Spring security 의 등장에 대해서 생각해보았다.
      • Spring Security 이전에는 Spring scope 내에서 처리하기 위해서는 PassportJava 와 같은 형태로 제작을 해야만 했을 것이다.
      • 혹은, Spring Security 와 마찬가지로 Servlet 에 직접 접근하여 Filter 를 사용하는 형식을 취할 것이다. 이런 형태의 최종 추상화가 바로 Spring Security 형태이다.
    • 개발자가 원하는 형태의 사용자 인증 Process 는 결국 비슷하다. 다양한 형태의 인증 프로세스를 범용적으로 지원하기 위한 고민을 Spring security 에서 나름의 정답을 내놓은 셈이다. 더 나은 프로세스가 존재할 수도 있지만, 결국에는 유사한 형태의 결과가 만들어지곤한다.
    • 특별히 더 나은 형태의 구조의 발상이 있지 않다면, 누구나 접근할 수 있으며, 사용법과 구조에 대해 이해가 있는 Spring Security 를 적용하는 것이 생산성 측면에서 더 유리하리라 생각한다.

테스트에서의 문제

  • 해당 라이브러리는 AspectJ 를 활용해서 Method가 실행되기 이전 intercept 하여 실행한다.
  • 이는 Controller 영역의 테스트에서 문제가 발생한다. Test 를 위한 지원이 포함되어있는 Spring Security 와 달리, 해당 라이브러리는 Test 를 할 때 Intercept 하고, argument 를 주입하는 영역에 대해 개별적으로 mocking을 하거나 하는 방식으로 처리를 해야한다.
@Test
void something() {
  // 이런 방식으로 Strategy 의 동작과 Resolver 의 동작을 구성해야함...
  BDDMockito.doNothing().when(jwtStrategy).check(any(HttpServletRequest.class));
      BDDMockito.given(userIdArgumentResolver.supportsParameter(any())).willReturn(true);
      BDDMockito.given(userIdArgumentResolver.resolveArgument(any(), any(), any(), any()))
}