Backend/Spring

AOP, Filter, Interceptor의 정의와 차이점 (2) - Interceptor 편

Juwon2106 2022. 1. 11. 11:26
728x90

Spring MVC Lifecycle

앞선 포스트에서는 Filter에 대해 알아보았고 이번 포스팅에서는 Interceptor를 알아보겠습니다.


Interceptor ( 인터셉터 : 낚아채다 )

Spring에서의 Interceptor 역할은 Client로부터 들어오는 요청( HttpRequest )을 Controller의 Handler로 도달하기 전에 가로채거나 , Controller로 부터 보내는 응답( HttpResponse )을 가로채는 역할을 합니다.

 

HandlerInterceptor에서 가로채어 원하는 추가적인 로직을 수행한 후에 Controller의 Handler로 보낼 수 있도록 하는 Module입니다.

( Handler : Client가 요청한 url에 따라 실행되어야 할 Method ) 

 

 

아래의 그림은 Dispather Servlet의 내부 처리 과정입니다.

Dispather Servlet의 내부 처리 과정 흐름도

최초에 Dispatcher Servlet에 요청이 들어오면 바로 Handler를 찾고 실행하는 것이 아닌 몇 가지 선행 처리 작업을 실행합니다.

 

이 과정을 요청 선처리 작업이라고 합니다.

 

 

요청 선처리 작업

요청 선처리 작업

  1. Request에 해당하는 Locale을 결정하고 노출한다.
    • org.springframework.web.servlet.LocalResolver를 이용해 java.util.Locale을 결정합니다.
  2. 결정된 Locale을 RequestContextHolder에 저장합니다.
    • org.springframework.web.context.request.RequestContextHoler 객체를 이용합니다.
    • 프레임워크의 코드가 단계마다 요청 정보를 넘겨받지 않고 손쉽게 요청 정보를 얻기 위해서입니다.
  3. FlahMapManager를 통해 FlashMap을 생성합니다.
    • org.springframework.web.servlet.FlashMapManager 객체를 이용해 FlashMap을 생성합니다.
  4. Multipart HTTP( headers 정보 : Axios 포스팅 참고 ) 요청 정보 확인 후에  MultipartResolver로 결정합니다.
    • Multipart가 맞다면 org.springframework.web.multipart.MultipartHttpServletRequest로 처리합니다.
  5. HandlerExecutionChain 결정 단계로 넘어갑니다.

 

이제 DispatcherServlet은 HandlerMapping에게 Client의 HttpRequest 객체를 수행할 Handler를 찾도록 합니다.

 

이 과정에서 HandlerExecutionChain ( 핸들러 실행 체인 )이 작동합니다.

 

이때 행해지는 과정을 HandlerExecutionChain 결정이라고 합니다.

 

HandlerExecutionChain은 하나 이상의 HandlerInterceptor를 거쳐 Controller가 실행되도록 구성되어 있습니다.

 

HandlerInterceptor에서 Request에 대해 원하는 작업을 수행한 후 Request 객체를 Controller로 전달합니다.

 

대표적인 HandlerInterceptor 작업

  • Login Session 검증
  • Header 검증
  • (Jwt) Token 검증
  • URL Handling 
  • ...

Interceptor 활용을 통해서 얻을 수 있는 크게 3가지가 있습니다.

  • 공통 코드 사용으로 코드 재사용성 증가
  • 메모리 낭비, 서버의 부하 감소
  • 코드 누락에 대한 위험성 감소

 

Interceptor의 예시 코드입니다.

/admin에 대한 모든 Handler가 요청 헤더 ( Request Header )의 JWT 검증을 수행합니다.

@RequiredArgsConstructor //생성자 주입으로 느슨한 결합 유도합니다.
@RequestMapping("/admin")
@RestController // 요청에 대한 응답을 JSON 형태로 객체를 반환합니다.
public Class AdminController{
	
    private AdminService adminService;
    
    @PostMapping("/login")
    public String adminLogin(@RequestHeader(value = "Authorization") String jwt, UserDTO userDto) throws JwtException {
    	boolean flag = adminService.login(jwt, userDto);
        if(flag){
        	return "ok";
        }
        return "no";
    }
    
    @GetMapping("/{id}")
    public String adminDetail(@PathVariable(name = "id") String id){
    	boolean flag = adminService.detail(id);
        if(flag){
        	return "ok";
        }
        return "no";
    }
    
    @PostMapping
    public String admin... throws JwtException{
    	...
    }
    
    ...
    
}

대다수의 Handler에서 JWT 토큰 검증을 수행하여 JwtException을 throw 하는 로직이 계속해서 발생하게 된다.

( 메모리, 서버의 부하 증가 )

 

또안 adminDetail Handler와 같은 JWT 검증 로직 누락이 존재하여 위험성이 증가합니다.

 

이러한 상황에 Interceptor를 활용해 JWT 검증 로직은 Interceptor에서 한 번만 검증하여 

 

@RequestMapping("/admin")에 대한 요청만 Interceptor를 적용하여 메모리, 서버의 부하를 감소시킬 수 있습니다.

 

코드 누락의 위험성 또한 감소합니다.

 

Interceptor 구현하기

Spring에서 Interceptor 구현 방법은 두 가지가 있습니다.

  1. HandlerInterceptor 인터페이스를 구현하는 방법
  2. HandlerInterceptorAdapter 클래스를 상속 받는 방법

먼저 인터페이스를 통해 구현하는 예시 코드입니다.

// Handler로 보내기 전에 처리하는 인터셉터
@RequiredArgsConstructor
public class adminInterceptorImpl implements HandlerInterceptor{

	private AuthService authService;
    
    // 매개변수 Object는 핸들러의 정보를 의미합니다. ( RequestMapping, DefaultServletHandler )
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException. JwtException {
		String token = request.getHeader("Authorization");
        
        if(token == null){
        	throw new AuthenticationException("not exist Token");
        }
        
        try{
        	authService.vertifyJwt(token);
        } catch (JwtException e){
        	throw new AuthenticationException("token is not valid");
        }
        
        // 반환값이 false라면 Handler로 보내지 않는다.
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{
    	// 요청에 대한 응답을 View에 렌더링 하기 전에 호출되는 메소드
        
        // ModelAndView 정보를 알 수 있다.
        
        // 특정 View ( 페이지 )에서 ModelAndView 값을 수정해야할 필요가 있다면 
        
        // postHandle 메소드에서 작업을 추가하면 됩니다.
        
        System.out.println("이제 view에 렌더링 할 예정입니다.")
      
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception e) throws Exception {
    	// View에 렌더링과 모든 동작이 끝난 후에 작동하는 메소드
    }
    
}

 

다음은 HandlerInterceptorAdapter 클래스를 상속받는 예제입니다.

@RequiredArgsConstructor
public class adminInterceptor extends HandlerInterceptorAdaper{

	private AuthService authService;
	
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException, JwtException{
    	String token = request.getHeader("Authorization");
        
        if(token == null){
        	throw new AuthenticationException("not exist Token");
        }
        
        try{
        	authService.vertifyJwt(token);
        } catch (JwtException e){
        	throw new AuthenticationException("token is not valid");
        }
        
        // 반환값이 false라면 Handler로 보내지 않는다.
        return true;
    }
    
}

 

이러한 Interceptor 로직으로 Controller의 Method에 접근하기 전 후 필요한 작업을 수행할 수 있습니다.

 

환경 구성으로 pom.xml에 Dependency를 추가합니다.

Maven

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-web</artifactId>
	<version>${org.springframework-version}</version>
</dependency>


Gradle 

dependencies {
	compile('org.springframework.boot:spring-boot-starter-web')
    	testCompile('org.springframework.boot:spring-boot-starter-test') 
}

 

DispatcherServlet의 Context 파일 ( servlet-context.xml )에 Interceptor Class와 적용될 URL을 기재합니다.

<mvc:interceptors>
		<mvc:interceptor>
			<mvc:mapping path="/admin/**"/>
			// Interceptor에서 제외할 경로가 있을 경우 
			<mvc:exclude-mapping path="/demoPage"/>
			<bean id="jwtInterceptor" class="project.config.interceptor.JwtInterceptor"/>
		</mvc:interceptor>
</mvc:interceptors>

 

이러한 단순 과정으로도 Spring에서 Interceptor를 활용할 수 있습니다.

 

다음 포스팅에서는 AOP에 대해 알아보겠습니다.

728x90