AOP를 공부하면서 AOP와 비슷한 기능을 하는 것처럼 보이는 다른 용어들과 많이 헷갈렸던 것 같아요. 오늘은 AOP와 비교하여 Filter, Interceptor, HandlerMethodArgumentResolver에 대해서도 간략하게 알아보도록 하겠습니다.
Interceptor
먼저 Interceptor부터 살펴보도록 하겠습니다. Interceptor를 이해하기 위해선 우선 HandlerMapping
에 대하여 알아야 합니다. HandlerMapping
은 DispatcherServlet
으로 하여금 요청이 왔을 때 해당 URL에 따른 메서드를 매핑하게 해줍니다. 그리고 실제 해당 메서드를 실행할 때는 HandlerAdapter
를 사용하죠.
위에서 언급한 HandlerMapping
와 같이 작동하는 Interceptor는 HandlerInterceptor
인터페이스를 구현해야 사용할 수 있는데요. 여기엔 세가지 메서드가 있습니다.
prehandle()
: 실제 handler가 실행되기 전에 호출됩니다.postHandle()
: handler가 실행된 후 호출됩니다. 예외발생시 실행되지 않습니다.afterCompletion()
: 요청 완료되고 뷰까지 만들어진 후 호출됩니다. 예외발생되어도 실행이 됩니다.
handler가 뭘까요?
A handler is a routine/function/method which is specialized in a certain type of data or focused on certain special tasks (출처 : https://stackoverflow.com/questions/195357/what-is-a-handler)
즉 특정 작업 또는 데이터를 처리하는데 중점을 둔, 특화된 메서드라고 생각할 수 있겠네요..
view 처리가 뭘까요?
Spring provides view resolvers, which enable you to render models in a browser without tying you to a specific view technology. (출처 : https://docs.spring.io/spring/docs/3.0.0.M3/reference/html/ch16s05.html)
저희가 model담은 값들을 브라우저에 표현을 하여 보내는 것을 말합니다.
return 값에 따라 뭐가 달라지나요?
return type 이 boolean인데요. true
일 때 handler 메서드가 제대로 작동이 되고 만약 false
가 반환되면 handler메서드가 실행이 안되고 바로 Httpstatus.OK
상태로 응답 처리됩니다.
코드스쿼드 java-qna-atdd 미션에서도 Interceptor가 쓰이고 있었는데요. 여기에서는 postHandle()
을 사용하여 테스트 할 때 default로 설정해놓은 User에 대해 로그인하고 세션값 부여하는 역할로 사용되고 있습니다. 그래서 다른 설정하지 않아도 세션값이 있는 로그인 유저로 처리되었던 것이죠.
Interceptor를 사용하기 위해선 @Configure
이 있고 WebMvcConfigurer
을 상속한 클래스에서 addInterceptors()
를 Override하여 사용할 수 있습니다. 그리고 모든 요청이 아닌 특정 URL에 제한하고 싶다면 설정할 때 같이 해주면 됩니다.
@Bean
public BasicAuthInterceptor basicAuthInterceptor() {
return new BasicAuthInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(basicAuthInterceptor());
.addPathPatterns("/**")
.excludePathPatterns("/users/**");
}
위에서 9번 10째 줄에서 그 제한이 있는데요. 10줄 같이 "/users/"로 시작하는 url은 Interceptor가 실행안되도록 하는 것입니다.
그럼 AOP란 다른점은 무엇일까요?
분명 전처리, 후처리를 할 수 있다는 점에서 비슷합니다. 다만 앞서 보았듯이 AOP는 좀 더 세밀하게 표현이 가능합니다. 포인트컷 표현식에서 execution을 사용하면 접근제한자, 인자타입, 클래스/인터페이스, 메서드명, 파라미터타입, 예외타입 등까지 조합이 가능하지만 Interceptor는 위와 같이 PathPattern에만 조건을 둘 수밖에 없는 것이죠.
Filter
Filter도 Interceptor와 같이 전후 처리를 합니다. 다만 분명 차이가 있는데요. 우선 그림부터 보겠습니다.
출처 : https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle/
우선 그림을 보면 알겠지만 실행시점이 다릅니다. 또 Filter는 Web Application에 등록하고, Interceptor는 Spring Context에 등록한다는 점에서 주된 차이가 있습니다. Filter는 doFilter()
라는 메서드를 통해서 처리를 하는데요. 자세한 차이점은 Stackoverflow의 글을 참고하시기 바랍니다.
HandlerMethodArgumentResolver
Resolver는 AOP, Interceptor랑은 전혀 관계가 없지만 혹시 저처럼 관련이 있지 않을까하여 헷갈리는 분들에 한해 간략하게 정리해보려 합니다. HandlerMethodArgumentResolver도 Interceptor와 같이 @Configure
클래스에서 아래와 같이 bean으로 등록하여 사용할 수 있습니다.
@Bean
public LoginUserHandlerMethodArgumentResolver loginUserArgumentResolver() {
return new LoginUserHandlerMethodArgumentResolver();
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserArgumentResolver());
}
HandlerMethodArgumentResolver는 메서드 인자에 따라 처리할 수 있게 해주는 전략적인 인터페이스입니다. 두 개의 메서드로 구성이 되는데요.
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
supportsParameter()
에서 인자인 parameter를 통해 메서드 인자를 확인하고 true
가 반환이 되면 resolveArgument()
가 실행이 되는 구조입니다.
qna-atdd에서 Controller의 매개변수로 HttpSession
을 안 받고 User
를 바로 받을 수 있었던 이유는 이곳에서 Session값을 받아 User
객체로 반환하기 때문입니다. 그렇기 때문에 이런 부가적인 작업들은 미리 처리하고 Controller에서는 그곳에서 해야할 일에 집중할 수 있었던 것이죠.
재미있는 점은 supportsParameter()
는 인자값을 하나하나 확인하기 때문에 만약 메서드의 인자가 5개가 있다면 해당 메서드가 처음 호출시 5번 호출되게 됩니다. 언듯보면 비효율적일 수 있지만 supportsParameter()
는 한번 호출된 메서드에 대해선 인자값을 캐쉬로 기억하고 있기 때문에 다음에 호출될 때는 여기서 처리가 요구되는 인자인지, 처리가 요구되지 않는 인자인지 기억하고 있다는 점입니다.
java로 AOP구현?
Java Reflection 내 proxy를 통해 AOP를 구현해볼 수도 있습니다. 계산기를 만들고 AOP로 계산 시간을 구해봅시다.
public interface Calculator {
public int add(int x, int y);
public int subtract(int x, int y);
public int multiply(int x, int y);
public int divide(int x, int y);
}
프록시가 사용되기 위해선 인터페이스가 필요합니다.
public class myCalculator implements Calculator {
private static final Logger log = getLogger(myCalculator.class);
@Override
public int add(int x, int y) {
log.info("계산 중");
return x + y;
}
@Override
public int subtract(int x, int y) {
return x - y;
}
@Override
public int multiply(int x, int y) {
return x * y;
}
@Override
public int divide(int x, int y) {
return x / y;
}
}
상속받아 구현하구요.
public class LogPrintHandler implements InvocationHandler {
private static final Logger log = getLogger(LogPrintHandler.class);
private Object target;
public LogPrintHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
StopWatch sw = new StopWatch();
sw.start();
log.info("Timer Begin");
int result = (int) method.invoke(target, args); // (3) 주업무를 invoke 함수를 통해 호출
sw.stop();
log.info("Timer Stop – Elapsed Time : "+ sw.getTime());
return result;
}
}
AOP에서 aspect에 해당하는 부분을 구현해줍니다. InvocationHandler
를 상속받아 invoke()
메서드를 구현해줍니다. 마지막으로 Driver에서 다음과 같이 호출하여 사용할 수 있습니다.
public class Driver {
public static void main(String[] args) {
Calculator cal = new myCalculator(); // 다형성
Calculator proxy_cal = (Calculator) Proxy.newProxyInstance( // (1)
cal.getClass().getClassLoader(), // Loader
cal.getClass().getInterfaces(), // Interface
new LogPrintHandler(cal)); // Handler (보조 업무를 구현하고 있는 실제 클래스)
System.out.println(proxy_cal.add(3, 4)); // add 메서드를 호출하여 3 + 4 결과를 출력
}
}
그럼 결과값으로 다음을 얻을 수 있습니다.
14:02:55.222 [main] INFO aop.proxy.LogPrintHandler - Timer Begin
14:02:55.225 [main] INFO aop.proxy.myCalculator - 계산 중
14:02:55.225 [main] INFO aop.proxy.LogPrintHandler - Timer Stop – Elapsed Time : 5
7
AOP와 같이 target의 전후로 처리된 것을 확인할 수 있습니다!