Java/Spring

Filter 와 interceptor, AOP 차이와 용도

amungstudy 2024. 2. 22. 11:16

이번 면접에서 아직 java에 대해서 정확하게 모르는 구나...! 라고 느꼈고

java, spring 에 대해서 다시 공부해야겠다고, 생각했습니다 😭

그래서 정리해보는 Filter와 interceptor 그리고 AOP

출처 : https://jake-seo-dev.tistory.com/83

종류 실행시점
Filter Dispatcher Servlet에 요청이 전달되기 전/후 실행
Interceptor Servlet이 Controller mapping method를 호출하기 전/후 실행
AOP target joinPoint(method)가 실행되기 전,후,자유롭게 호출

Filter

웹 어플리케이션에 전반적으로 사용되는 공통적인 기능을 구현할 때 적용할 수 있습니다.

특히 공통적인 기능을 서블릿이 호출되지 전에 수행(전처리)하거나 서블릿이 호출되고 난 후에 수행(후처리)시 사용.

 

필터를 사용하면 클라이언트의 요청을 가로채서 서버 컴포넌트의 공통적인 기능을 수행시킬 수 있다.

사용자 인증, 로깅, 암호화 등....에 사용한다.

 

출처 : https://docs.spring.io/spring-security/reference/_images/servlet/architecture/filterchain.png

클라이언트가 요청을 보내면, 애플리케이션 내부에서는 FilterChain이 생성됩니다.

요청 URI에 따라 적절한 Filter와 Servlet이 포함되어있습니다.

FilterChain의 모든 doFilter method가 호출되면 Servlet으로 요청을 전달합니다.

각 필터는 다음 필터 또는 서블릿의 호출을 막을지 여부를 결정할 수 있습니다.

각 필터는 HttpServletRequest 또는 HttpServletResponse를 수정할 수 있습니다.

예시) (FilterChain : 필터 체인을 제어할 때 사용되는 객체)

전처리

chain.doFilter : FilterChain을 통해 다음 필터 또는 서블릿으로 요청을 전달합니다.

후처리

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // do something before the rest of the application
    chain.doFilter(request, response); // invoke the rest of the application
    // do something after the rest of the application
}

❣️implementing Filter를 하는 대신 OncePerRequestFilter를 상속할 수도 있습니다.

OncePerRequestFilter를 사용하면 doFilterInternal 메서드를 구현할 수 있습니다.

이것은 모든 서블릿 컨테이너에서 요청 Dispatch당 단일 실행을 보장합니다. (한 번의 요청에 대해 단 한번만 호출)

2번 filter가 실행되는 경우... ex) Forwarding, Spring MVC error처리

인증,인가를 구현하는 경우 주로 사용합니다.

예시)

public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private final TokenProvider tokenProvider;
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer ";


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 요청header의 Authorization 키 값 조회
        String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
        String token = getAccessToken(authorizationHeader); // 실 토큰값으로 만듦
        // 가져온 토큰이 유효한 경우 인증 정보를 설정함
        if(tokenProvider.validToken(token)){
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request,response);
    }

Interceptor

클라이언트의 요청과 관련된 작업에 대해 추가적인 요구사항을 만족해야할 때 적용할 수 있습니다.

스프링 컨텍스트 내에서 실행됩니다. 즉 Filter와 달리 Spring에서 제공하는 기술입니다.

  • 특정 그룹의 사용자가 어떤 기능을 사용하지 못하는 경우, 인터셉터에서 처리할 수 있다.
  • 컨트롤러로 넘겨주기 위한 정보를 가공하기 용이하다.
  • API 호출에 대한 로깅 또는 검사 처리를 할 때 용이하다.
실행 메서드  
preHandle Servlet이 Controller mapping method를 호출하기 전 실행
mapping method 호출 여부를 return (true: 호출 / false : 미호출)
postHandle Controller에 mapping method가 호출 완료된 후 Servlet으로 반환 값이 전달되기 전에 호출.
model, view 제어 가능. 최근에는 @RestController의 활용으로 인해 자주 사용되지 않는다.
afterCompletion response 응답 정보가 filter로 전달되기 전, servlet의 응답(출력)이 완료된 후 호출
( = view페이지가 렌더링 된 후)
package com.bitc.mvc.interceptor;

        import java.lang.reflect.Method;
        import java.util.Map;

        import javax.servlet.http.HttpServletRequest;
        import javax.servlet.http.HttpServletResponse;

        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.web.method.HandlerMethod;
        import org.springframework.web.servlet.HandlerInterceptor;
        import org.springframework.web.servlet.ModelAndView;

        import com.bitc.mvc.vo.TestVO;

public class TestInterceptor implements HandlerInterceptor{

    @Autowired(required = false)
    TestVO vo;

    /**
     * Servlet이 Controller mapping method를 호출하기 전 실행
     * @return mapping method 호출 여부 - true 호출, false 미호출
     */
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler)
            throws Exception {
        System.out.println("TestInterceptor preHandler START ====== ");
        HandlerMethod method = (HandlerMethod)handler;
        Method methodObj = method.getMethod();
        System.out.println("controller : " + method.getBean());
        System.out.println("methodObj :" + methodObj);
        System.out.println("interceptor prehandle vo : " + vo);

        String command = request.getRequestURI()
                .substring(request.getContextPath().length()+1);
        System.out.println(command);
        if(command.equals("test1")) {
            response.sendRedirect("test2");
            System.out.println("TestInterceptor preHandler return false ");
            return false; //test1 매핑 메소드 호출안하고 test2에 redirect.
        }

        System.out.println("TestInterceptor preHandler END ====== ");
        return true; // 컨트롤러 메소드 호출함.    
    }

    /**
     * Controller에 mapping method가 호출 완료 되고 난 후
     * servlet으로 반환 값이 전달되기 전에 호출
     * 여기서 model, view 제어 가능.
     */
    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response, Object handler,
                           ModelAndView modelAndView) throws Exception {
        System.out.println("TestInterceptor postHandle START ====== ");
        System.out.println(modelAndView);
        Map<String, Object> map = modelAndView.getModel();
        for(String key : map.keySet()) {
            System.out.println("key : " + key);
            System.out.println("value : " + map.get(key));
        }
        String viewName = modelAndView.getViewName();
        System.out.println("viewName : "+ viewName);
        if(viewName.equals("result")) {
            modelAndView.setViewName("home");
        }

        Object result = map.get("result");

        if(result == null) {
            modelAndView.addObject("result","postHandle job");
        }
        String result1 = (String) request.getAttribute("result1");
        System.out.println("Post Handle result1 : "+ result1);
        System.out.println("TestInterceptor postHandle END ====== ");
    }
    /**
     * response 응답 정보가 filter로 전달되기 전 호출
     * servlet의 응답(출력)이 완료되고 난 후 호출
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("TestInterceptor afterCompletion START ====== ");
        System.out.println(response.getContentType());
        System.out.println(request.getAttribute("result"));
        System.out.println(request.getAttribute("result1"));
        System.out.println("TestInterceptor afterCompletion END ====== ");
    }
}

AOP

AOP는 URL, 파라미터, 어노테이션 등 PointCut이 지원하는 다양한 방법으로 대상을 지정할 수 있습니다.

로깅, 트랜잭션, 에러처리 등 비즈니스 단의 메서드에서 좀 더 세밀하게 조정하고 싶을 때 사용할 수 있습니다.

AOP의 PointCut  
@Before target joinPoint(method)가 수행되기 전 호출
@After target joinPoint(method)가 수행된 후 호출
@AfterReturning 정상적으로 수행되어 값이 반환된 후 호출, 리턴값 활용가능
@AfterThrowing 예외 발생 이후 호출
@Around target joinPoint(method)의 수행 전 / 후 호출

 

package com.bitc.aop;

import java.util.Arrays;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Aspect
@Component
@Slf4j
public class AOPAdvice {

    public AOPAdvice() {
        System.out.println("AOP Advice 생성");
    }
    // target joinPoint(method)가 실행되기 전 호출
    // * : 모든 반환타입 적용     // void : 반환타입이 없는것만 적용
    // com.bitc.service.*.*(..) : service pkg에 클래스이름 상관없고, 메소드이름 상관없고, 매개변수 상관없고, 갯수 상관없고
    @Before("execution(* com.bitc.service.*.*(..))")
    public void startLog(JoinPoint jp) {
        log.info("-------------------------------------");
        log.info("-------------------------------------");
        log.info("----------- START LOG ---------------");

        log.info("target : "+ jp.getTarget()); // target : AOP를 부착할 메소드가 포함된 클래스
        log.info("type : "+ jp.getKind()); // 메소드 종류
        log.info("parameters : " + Arrays.toString(jp.getArgs())); // jp.getArgs() = object타입의 배열
        log.info("name: " + jp.getSignature().getName()); // 메소드 이름

        log.info("----------- START LOG END -----------");
        log.info("-------------------------------------");
        log.info("-------------------------------------");
    }

    /*
    // 수신자가 존재하는 사용자 인지 확인    
    @Autowired
    UserDAO dao;

        @Around("execution(* com.bitc.service.MessageServiceImpl.addMessage(com.bitc.vo.MessageVO)) && args(message)")
        public Object checkTargetId(ProceedingJoinPoint pjp, MessageVO message) throws Throwable {
            log.info("param message : " + message);
            // 수신자가 존재하는 사용자 인지 확인
            String targetid = message.getTargetid();

            UserVO vo = dao.checkUser(targetid);
            if(vo == null) {
                throw new NullPointerException("존재하지 않는 수신자입니다.");
                // addmessage하지 않고 예외처리.(Before로 처리해도 됨.)
            }
            Object o = pjp.proceed();        
            log.info("------------ AROUND END -------------");
            return o;
        }
    */


    @After("execution(* com.bitc.service.MessageService*.*(..))")
    public void endLog(JoinPoint jp) {
        log.info("----------- END AFTER LOG -----------");
        log.info("-------------------------------------");

    }

    // AfterReturning: 정상적으로 수행돼서 값이 반환되고 나서 수행됨 returning에 지정된 이름으로 리턴된 값 전달받음
    @AfterReturning(
            pointcut="execution(!void com.bitc.service.MessageServiceImpl.*(..))",
            returning = "returnValue")
    public void successLog(JoinPoint jp,Object returnValue) {
        log.info("-------------------------------------------------------");
        log.info("------------ START Afterreturning LOG -----------------");
        log.info("target : "+ jp.getTarget());
        log.info("name : "+ jp.getSignature().getName());
        log.info("returns : "+ returnValue);
        log.info("-------------- END Afterreturning LOG -----------------");
        log.info("-------------------------------------------------------");
    }
    // AfterThrowing : 예외발생 이후 실행
    @AfterThrowing(
        value = "execution(* com.bitc.service.*.*(..))",
        throwing = "exception"
    )
    public void endThrowing(JoinPoint jp, Exception exception) {
        log.info("-------------------------------------------------------");
        log.info("------------ START AfterThrowing LOG -----------------");
        log.info("target : "+ jp.getTarget());
        log.info("name : "+ jp.getSignature().getName());
        log.info("error : "+ exception.getMessage());
        log.info("-------------- END AfterThrowing LOG -----------------");
        log.info("-------------------------------------------------------");
    }
    // args(uid,mno) : returning처럼 변수에 직접 넣어줌. 
    @Around(value="execution(* com.bitc.service.MessageServiceImpl.readMessage(String,int)) && args(uid,mno)")
    public Object readMessageLog(ProceedingJoinPoint pjp, String uid, int mno) 
            throws Throwable{ // ProceedingJoinPoint : 실행가능한joinpoint
        // List 또는 String 처럼 어떤타입으로 데이터가 반환될지 몰라서 Object로 지정

        // 전처리
        log.info("-------------------------------------");
        log.info("----------- AROUND START ------------");
        log.info("param uid : " +uid);
        log.info("param mno: "+mno);
        log.info("target : "+ pjp.getTarget());
        log.info("type : " + pjp.getKind());
        log.info("parameters : " + Arrays.toString(pjp.getArgs())); 
        log.info("name: " + pjp.getSignature().getName()); 
        // pointcut method 실행
        Object o = pjp.proceed(); //readMessage 실행해줌
        //후처리
        log.info("around : " + o.toString());
        log.info("------------ AROUND END -------------");
        log.info("-------------------------------------");
        return o;
    };

}

참고 : https://docs.spring.io/spring-security/reference/servlet/architecture.html

[Architecture :: Spring Security

The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like authentication, authorization, exploit protection, and more. The filters are executed in a spec

docs.spring.io](https://docs.spring.io/spring-security/reference/servlet/architecture.html)

https://ik0501.tistory.com/entry/Spring-Filter-Interceptor-AOP-%EC%B0%A8%EC%9D%B4

[Spring) Filter, Interceptor, AOP 차이

Filter // 요청과 응답을 거른 뒤 정제하는 역할 일반적으로 Spring과 무관하게 전역적으로 처리해야되는 작업을 처리할 때 사용 Filter는 Spring이 실행 되기전 실행되며 톰캣과 같은 웹 컨테이너에서

ik0501.tistory.com](https://ik0501.tistory.com/entry/Spring-Filter-Interceptor-AOP-%EC%B0%A8%EC%9D%B4)

https://dev-coco.tistory.com/173#head7

[[Spring] 필터(Filter)와 인터셉터(Interceptor)의 개념 및 차이

개발을 하다 보면 공통적으로 처리해야 할 업무들이 많다. 공통 업무에 관련된 코드를 페이지마다 작성한다면 중복 코드가 많아지게 되고, 프로젝트 단위가 커질수록 서버에 부하를 줄 수도 있

dev-coco.tistory.com](https://dev-coco.tistory.com/173#head7)