[Spring] Filter, Interceptor, AOP

mallin·2022년 5월 10일
1

spring

목록 보기
3/4
post-thumbnail

개발을 하다보면 공통적으로 처리해야하는 것들이 많습니다
EX) 로그인 처리, 로그 등등 !

모든 로직에 공통적으로 처리해야하는 것들의 코드를 넣게 되면,
중복되는 코드가 많아지고 관리하는데 있어서 굉장히 어려워 집니다.

그렇기 때문에 공통적으로 사용하는 로직은 따로 빼서 관리해야 합니다

Spring 에선 이럴 때 3가지 방법을 사용할 수 있습니다.
① Filter
② Interceptor
③ AOP

그러면 Filter, Interceptor, AOP 에 대해서 알아보도록 하겠습니다.

Filter, Interceptor, AOP 의 흐름

Filter, Interceptor, AOP 는 위와 같은 흐름으로 실행됩니다.

  • 요청이 들어오게 되면 request > Filter > Servlet > Interceptor > AOP > Controller순으로 실행됩니다.
  • DisptacherServlet, Interceptor, AOP, Controller 는 Spring Context 에 속하고, Filter 의 경우에는 Web Context 에 속합니다.

흐름을 알았으니 이제 각각에 대해서 조금 더 자세하게 알아봅시다.

필터(Filter)

필터(Filter) 란 ?

  1. 스프링의 독자적인 기능이 아닌 자바 서블릿에서 제공하는 기능
  2. 스프링 컨텍스트가 아니라 웹 컨텍스트에 속하며, 스프링 컨테이너가 아니라 웹컨테이너(톰캣) 에 의해 관리됩니다.
  3. Filter 는 FilterChain 을 통해 여러 필터가 연쇄적으로 동작하게 할 수 있습니다.
  4. 주로 요청에 대한 권한, 인증을 처리하는데 사용합니다. EX) Spring Security, JWT 등등

스프링에서 필터 사용하기

필터 클래스는 servlet 의 Filter 인터페이스(javax.servlet.Filter) 를 통해 구현할 수 있습니다.

public interface Filter {

	    public default void init(FilterConfig filterConfig) throws ServletException {}
        
    	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
        
        public default void destroy() {}

}

Filter 인터페이스는 위와 같이 총 3개의 메소드를 가지고 있습니다. (init, doFilter, destory)

① init (필터가 생성될 때 수행되는 메소드)
: 웹 컨테이너가 사용 중인 필터를 나타내기 위해 호출됩니다. 서블릿 컨테이너틑 필터를 인스턴스화한 후 init 메서드를 정확히 한 번 호출합니다. 필터가 필터링 작업을 수행하도록 요청하기 전에 init 메소드는 성공적으로 완료되어야 합니다.

② doFilter (request, response 가 필터를 지나갈 때 수행되는 메소드)
: 클라이언트의 요청이나 응답으로 인해 체인을 통과할 때마다 컨테이너에 의해 호출되는 메소드입니다. 이 메소드에 파라미터로 전달된 필터 체인은 필터가 요청과 응답을 다음 엔티티로 전달할 수 있도로 합니다.

③ destroy (필터가 종료될 때 수행되는 메소드)
: 필터의 doFilter 메서드 내의 모든 스레드가 종료되거나 시간 초과 시간이 지난 후에 웹 컨테이너에서 호출하며, 필터에 서비스가 중단되었음을 나타냅니다. 웹 컨테이너가 이 메서드를 호출한 다음에는 doFilter 메소드를 다시 호출하지 않습니다.

그러면 실제로 사용해보도록 하겠습니다.

첫번째, Filter 를 implements 받아서 Filter 인터페이스 내 메소드를 구현해줍니다.
CustomFilter.java

@Slf4j
public class CustomFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("1. CustomFilter 실행");
        Filter.super.init(filterConfig);
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info(">>>>>>>>> 2. CustomFilter 동작 >>>>>>>>>");
        log.info(">>>>>>>>> 시작 >>>>>>>>>");
        chain.doFilter(request, response);
        log.info(">>>>>>>>> 종료 >>>>>>>>>");
    }

    @Override
    public void destroy() {
        log.info("3. CustomFilter 삭제");
        // Filter.super.destroy();
    }
}

두번째, 위에서 만든 customFilter 를 Spring Bean 으로 등록합니다.
FilterRegistrationBean 을 사용해서 Filter 를 Bean 으로 등록할 수 있습니다.

FilterConfig.java

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean customFilterBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(new CustomFilter());
        return registrationBean;
    }

}

세번째, 테스트 컨트롤러를 만들어서 실제 요청을 해보도록 하겠습니다.
FilterTestController.java

@RestController
@RequestMapping("/filter/test")
public class FilterTestController {

    @GetMapping
    public String test() {
        return "test";
    }
    
}

그런 다음 애플리케이션을 한번 실행해보도록 하겠습니다.

애플리케이션 실행했을 때 init 메소드가 실행되고,

API(/filter/test) 호출했을 때 doFilter 메소드가 실행되고,

애플리케이션 종료했을 때 destory 메소드가 실행되는 걸 확인할 수 있습니다.

그러면 커스텀 필터를 두 개 설정했을 때에는 어떻게 될까요 ??
기존의 커스텀 필터 이름을 FirstCustomFilter 로 바꾸고, SecondCustomFilter 를 만든 후 FilterConfig 에 secondCustomFilter 관련해 추가해주고 API 를 실행해보도록 하겠습니다.

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean firstCustomFilterBean() {
        FilterRegistrationBean firstRegistrationBean = new FilterRegistrationBean(new FirstCustomFilter());
        firstRegistrationBean.setOrder(1);
        return firstRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean secondCustomFilterBean() {
        FilterRegistrationBean secondRegistrationBean = new FilterRegistrationBean(new SecondCustomFilter());
        secondRegistrationBean.setOrder(2);
        return secondRegistrationBean;
    }
}

그러면 FirstFilter 가 호출되고, SecondFilter 가 호출되는 걸 알 수 있는데요.
위 config 에서 설정한 setOrder 에 따라 Filter 가 호출되는 걸 확인할 수 있습니다.

3번의 설명 처럼 연쇄적으로 작용하는 것 또한 확인할 수 있습니다.
FirstFilter > SecondFilter > controller > SecondFilter > FirstFilter

해당 소스코드는 깃허브에서 확인하실 수 있습니다. 👉 링크

Dispatcher Servlet

Disptacher Servlet 은 프론트 컨트롤러입니다.

프론트 컨트롤러(front controller) 란 ?
서블릿 컨테이너 제일 앞단에서 서버로 오는 모든 요청을 받아 처리하는 컨트롤러

클라이언트로부터 어떠한 요청이 오면 프론트 컨트롤러인 디스패처 서블릿이 가장 먼저 받게됩니다.
그러면 디스패치 서블릿은 공통적인 작업을 먼저 처리하고, 해당 요청을 처리해야 하는 컨트롤러를 찾아서 작업을 위임합니다.

장점

디스패치 서블릿이 없을 때에는 web.xml 에 URL 매핑을 위해 등록해줘야 했지만, 디스패치 서블릿이 해당 어플리케이션으로 들어오는 모든 요청을 핸들링 해주고, 공통 작업을 처리해주면서 상당히 편리하게 사용할 수 있게 되었습니다.

디스패치 서브릿의 동작 과정

① 클라이언트의 요청을 받음

② 요청 정보를 통해 요청을 위임할 컨트롤러를 찾음 (Handler Mapping)
: 컨트롤러에 요청을 위임해야 하는데 그러기 위해서는 요청을 위임할 컨트롤러를 찾아야 합니다.
: HandlerMapping 의 구현체 중 하나인 RequestMappingHandlerMapping 은 모든 컨트롤러 빈을 파싱하여 HashMap 으로 관리합니다.
: 그래서 요청이 오게 되면 HandlerMapping 은 Http Method, URI 등을 사용해 요청 정보를 객체로 만들고, key 로 사용해 요청을 처리할 HandlerMethod 를 찾고, HandlerMethodExecutionChain 으로 감싸서 반환합니다.

③ 요청을 컨트롤러로 위임할 핸들로 어댑터를 찾아서 전달 (Handler Adapter)
: 디스패처 서블릿은 컨트롤러로 요청을 직접 위임하는 것이 아니라 HandlerAdapter 를 통해 요청을 위임합니다.
: 공통적인 전/후처리 과정이 필요하기 때문에 Adapter 를 통해 컨트롤러를 호출합니다.

④ 핸들러 어댑터가 컨트롤러로 요청을 위임 (RestController)
: HandlerMethod 에 컨트롤러 빈 이름과 메소드, 빈 팩토리가 있어서 빈 팩토리에서 컨트롤러 빈을 찾는 등의 작업이 일어납니다.

⑤ 비즈니스 로직을 처리 (Service, Repository)
: 컨트롤러는 서비스를 호출하고 비즈니스 로직들을 진행

⑥ 컨트롤러가 반환값을 반환
: 주로 ResponseEntity 를 반환

⑦ HandlerAdapter 가 반환값을 처리
: 컨트롤러부터 받은 응답을 ReturnValueHandler를 통해 후처리한 후 디스패처 서블릿으로 리턴합니다.

⑧ 서버의 응답을 클라이언트로 반환
: 다시 필터를 거쳐서 클라이언트로 반환합니다.

디스패처 서블릿의 계층 구조는 다음과 같습니다.

Interceptor

Spring 이 제공하는 기술로써, 디스패치 서블릿이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공합니다.

디스패치 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾도록 요청하는데, 그 결과로 실행 체인(HandlerExecutionChain) 을 돌려줍니다.

1개 이상의 인터셉터가 등록되어 있는 경우 👉 순차적으로 인터셉터를 거쳐 컨트롤러 실행
인터셉터가 등록되어 있지 않은 경우 👉 바로 컨트롤러 실행
합니다.

스프링에서 Interceptor 알아보기

org.springframework.web.servlet.HandlerInterceptor 의 인터페이스를 통해 구현할 수 있습니다.

package org.springframework.web.servlet;

public interface HandlerInterceptor {

	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		return true;
	}
    
	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
	}
    
	default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,@Nullable Exception ex) throws Exception {
	}
    
}

HandlerInterceptor 인터페이스는 위와 같이 총 3개의 메소드를 가지고 있습니다. (preHandle, postHandle, afterCompletion)

① preHandle (Handler 가 실행되기 전 메소드)
: Handler 가 실행하기 전의 Interception 포인트입니다. 컨트롤러가 호출되기 전에 실행됩니다.
핸들러 매핑에서 적절한 핸들러 객체를 결정한 후 핸들러 어댑터가 핸들러를 호출하기 전에 호출됩니다.
디스패처 서블릿은 수많은 인터셉터로 구성된 실행 체인의 핸들러를 처리하며, 핸들러 자체는 마지막에 처리합니다.
이 방법을 사용하면 각 인터셉터가 실행 체인을 중단하도록 결정할 수 있으며, 일반적으로 HTTP 오류를 전송하거나 사용자 지정 응답을 작성할 수 있습니다.

② postHandle (Handler 를 성공적으로 실행한 후 메소드)
: Handler 를 성공적으로 실행한 후 Interception 포인트입니다. 컨트롤러를 호출한 후에 실행됩니다.
Handler Adapter 가 실제로 Handler 를 호출한 후 디스패처 서블릿이 view 를 렌더링 하기 전에 호출됩니다.
지정된 ModelAndView 를 통해 추가 모델 객체를 뷰에 노출할 수 있습니다.
디스패처 서블릿은 수많은 인터셉터로 구성된 실행 체인의 핸들러를 처리하며, 핸들러 자체는 마지막에 처리합니다.
이 방법을 사용하면, 각 인터셉터는 실행 체인의 역순으로 적용되며 실행을 후 처리할 수 있습니다.

③ afterCompletion (요청 처리가 완료된 후 메소드)
: 요청 처리가 완료된 후, 즉 view 를 렌더링 한 후 콜백합니다. 즉, 모든 작업이 완료된 후 실행됩니다.
Handler 실행 결과에 따라 호출되므로 적절한 리소스 정리를 수행할 수 있습니다.
인터셉터의 preHandle 메소드가 성공적으로 완료되고 true 가 반환된 경우에만 호출됩니다.
postHandle 메소드와 마찬가지로 이 메소드는 체인의 각 인터셉터에서 역순으로 호출되므로 첫번째 인터셉터가 마지막으로 호출됩니다.

첫번째, HandlerInterceptor 를 implements 받은 CustomInterceptor 를 만들어줍니다.

@Slf4j
public class CustomInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authorizationHeader = request.getHeader("Authorization");
        log.info("[preHandle] authorizationHeader>>>>>>>>>>>>>>>>>>"+authorizationHeader);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        log.info("[postHandler] 컨트롤러 응답 >>>>>>>>>>>>>>>>>>>>>>>>>>>..");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("[afterCompletion] 컨트롤러 응답 완료 >>>>>>>>>>>>>>.");
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

}

두번째, CustomInterceptor 를 적용할 URL 을 설정해줍니다.

@Configuration
public class CustomWebMvcConfiguration extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CustomInterceptor())
                .addPathPatterns("/interceptor/**");
    }
    
}

세번째, 테스트
/interceptor URL 을 호출해줍니다

그냥 호출 했을 때에 preHandler, postHandler, afterCompletion 이 모두 호출 되는 걸 확인 할 수 있고,

Header 에 Authroization 을 232323 으로 넣어서 호출하면,

preHandle 에서 232323 이 출력되는 걸 확인할 수 있습니다.

전체 소스는 해당 👉 링크 에서 확인 하실 수 있습니다.

용도 및 사용 예시

  • 세부적인 보안 및 인증/인가 공통 작업
  • API 호출에 대한 로깅 또는 감사
  • Controller 로 넘겨주는 정보의 가공

클라이언트의 요청과 관련되어 전역적으로 처리해야 하는 작업들을 처리할 수 있습니다.
필터와 다르게 HttpServletRequest, HttpServletResponse 등과 같은 객체를 제공받기 때문에, 컨트롤러로 넘겨주기 위한 정보를 가공하기에 용이합니다.
EX) 클라이언트의 IP 나 요청 정보를 로깅

AOP (Aspect Oriented Progamming)

관점 지향 프로그래밍

  • 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화(어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것) 하는 것입니다.
  • 스프링 빈에만 AOP 적용 가능합니다.
  • 프록시 패턴 기반의 AOP 구현체입니다.

프록시 패턴이란?
기존 클래스의 빈이 만들어질 때 프록시(기능이 추가된 클래스)를 자동으로 만들고 원본 클래스 대신 프록시를 빈으로 등록합니다.

주요 개념

명명설명
Aspect흩어진 관심사를 모듈화 한 것
TargetAspect 를 적용하는 곳
Advice실직적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
JointPointAdvice 가 적용될 위치, 메서드 진입 지점, 생성자 호출 시점 등 다양한 시점에 적용 가능
PointCutJointPoint의 상세한 스펙을 정의한 것

스프링에서 AOP 사용해보기

AOP 를 사용해서 메소드 실행시간을 재는 예제를 만들어보도록 하겠습니다.

① 어노테이션 사용하기 ② 적용 범위 지정해주기
이렇게 총 두가지 방법으로 AOP 를 적용할 수 있는데, 아래 예제에서 두 가지 방법 모두를 사용해보도록 하겠습니다.

첫번째, 커스텀 annotation 만들어주기
어노테이션으로 AOP 를 사용하기 위해서 커스텀 어노테이션인 LogExecutionTime 을 만들어주도록 하겠습니다. 적용 범위 지정해서 AOP를 사용하실 분들은 스킵하셔도 됩니다.

LogExcutionTime.java

@Retention(RetentionPolicy.RUNTIME)     // 어노테이션 정보 유지 위치
@Target(ElementType.METHOD)             // 해당 어노테이션을 어디에서 사용할 수 있을지 결정
public @interface LogExecutionTime {
}

@Retention : 어노테이션 정보를 어디까지 유지할건지 설정

이름설명
RetentionPolicy.SOURCE컴파일 이후 없어짐
RetentionPolicy.CLASS컴파일러가 클래스 참조할 떄까지 유효
RetentionPolicy.RUNTIME컴파일 이후에도 JVM에 의해 참조 가능

@Target : 해당 커스텀 어노테이션을 어디에서 사용할 수 있는지

이름설명
ElementType.PACKAGE패키지 선언시
ElementType.TYPE타입 선언시
ElementType.CONSTRUCTOR생성자 선언시
ElementType.FIELD멤버 변수 선언시
ElementType.METHOD메소드 선언시
ElementType.ANNOTATION_TYPE어노테이션 타입 선언시
ElementType.LOCAL_VARIABLE지역 변수 선언시
ElementType.TYPE_PARAMETER매개 변수 타입 선언시

두번째, AOP 적용 코드 구현

LogAspect.java

@Aspect    // Aspect 명시
@Component // 스프링 빈 등록
public class LogAspect {

    //로거 생성
    Logger logger = LoggerFactory.getLogger(LogAspect.class);

    // @Around("@annotation(LogExecutionTime)")
    @Around("execution(* velog.soyeon.spring.aop.*.*(..))") // 메소드 실행전후에 공통 로직을 적용할 때 사용
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable{
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();                      // 스탑워치 시작
        Object proceed = joinPoint.proceed();   // 메소드 실행
        stopWatch.stop();                       // 스탑워치 끝
        logger.info(stopWatch.prettyPrint());   // 로깅
        return proceed;
    }
}

@Around 는 메소드 실행 전/후에 공통 로직을 적용하고 싶을 떄 사용하고,
@Before 은 메소드 실행 전에 공통 로직을 적용하고 싶을 떄,
@After 은 메소드 실행 후에 공통 로직을 적용하고 싶을 떄 사용합니다.

그리고 해당 어노테이션에 pointcut 표현식을 사용해 어느 실행 시점에 AOP 를 적용할지 설정할 수도 있고, 주석된 것 처럼 어노테이션을 명시해서 해당 어노테이션이 붙은 경우에만 실행시켜 줄 수 있습니다.

Pointcut 표현식설명
execution(public * *(..))public 메소드 실행
execution(* set*(..))이름이 set으로 시작하는 모든 메소드명 실행
execution(* com.xyz.service.TestService.*(..))TestService 인터페이스의 모든 메소드 실행
execution(* com.xyz.service.*.*(..))com.xyz.service 패키지의 모든 메소드 실행
bean(*Reepository)이름이 "Repository" 로 끝나는 모든 빈

세번째, 테스트 Controller 생성

@RestController
@RequestMapping("/aop")
public class AopController {

//    @LogExecutionTime
    @GetMapping("/test")
    public boolean test() throws InterruptedException {
        Thread.sleep(40);
        return true;
    }

}

어노테이션 기반 AOP 테스트를 하고 싶은 경우 주석에 있는 걸 풀고 테스트해보면 됩니다.
API 를 호출한 경우

실제로 위와 같이 실행 시간이 로깅되게 됩니다. 🙂
요걸 다양하게 사용해서 Request, Response 를 로깅하거나 등등으로 사용하면 됩니다.

전체 코드는 요기 👉 링크 를 참고해주세요.

이번엔 Filter, Interceptor, AOP 에 대해서 알아봤는데 스프링에 대한 기초가 정말 부족하다는 걸 많이 느꼈던 것 같습니다.
지금부터라도 스프링을 조금 더 딥하게 공부해야겠다는 생각.... 이 들었습니다.

필터 vs 인터셉터 vs AOP

인터셉터 = 세션 및 쿠키 체크하는 http 프로토콜 단위로 처리해야 하는 업무가 있을 때
AOP = 비즈니스 단에서 세밀하게 조정하고 싶을때

🙇🏼‍♀️ 레퍼런스

[Spring] Filter, Interceptor, AOP 차이 및 정리
[Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)
spring - 스프링에서의 필터 개념 및 예제
[Spring] Dispatcher-Servlet(디스패처 서블릿)이란? 디스패처 서블릿의 개념과 동작 과정
[Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)
[Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP
Spring AOP 스프링이 해줄건데 너가 왜 어려워 해? Spring boot에서 aop logging 사용법 제일 쉽게 알려드립니다!
Spring AOP - 어노테이션 만들기

0개의 댓글