본문 바로가기
프로그래밍/스프링프레임워크

실행 흐름에 끼어들기(Filter,Interceptor,AOP) 1 - Servlet Filter

by pentode 2018. 4. 2.

Java 웹 프로그래밍에서 프로그램 흐름의 앞이나 뒤에 공통적인 처리를 추가할 수 있는 방법이 있습니다. 서블릿에서 지원하는 서블릿 필터와 스프링 프레임웍을 사용하면 쓸 수 있는 인터셉터, AOP 가 있습니다.

 

먼저 Servlet Filter에 대해 알아 봅니다.

 

Servlet Filter 는 Servlet Specification 2.3 부터 지원합니다. 필터가 동작하도록 지정된 자원의 앞단에서 요청 내용을 변경하거나 여러가지 체크를 수행할 수 있습니다. 또한 자원의 처리가 끝난 후 응답 내용에 대해서도 변경하는 처리를 할 수 있습니다.

 

 

1. web.xml 에서 필터를 등록합니다. /WEB-INF/web.xml 파일 입니다.

 

<filter>
    <filter-name>firstFilter</filter-name>
    <filter-class>com.tistory.pentode.filter.FirstFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>firstFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

 

등록한 firstFilter 는 이름은 encoding 이고, 값은 UTF-8 인 파라미터를 정의하고 있습니다. 필터를 /* 에 맵핑 했습니다. 이것은 필터가 servlet, jsp 뿐만 아니라 이미지와 같은 모든 자원의 요청에도 호출 된다는 것을 의미합니다. 실제 사용시에는 필요한 자원에만 맵핑 되도록 합니다.

 

 

2. 필터를 정의합니다. src/main/java/com/tistory/pentode/filter/FirstFilter.java 파일 입니다. 이 필터에서는 하는 작업은 다음과 같습니다.


 - 필터 등록시 설정한 초기 파라미터로 POST 요청 데이터의 인코딩을 지정합니다.
 - 요청 정보에 대한 추가 처리를 위해 HttpServletRequestWrapper 를 사용하여 HttpServletRequest에서  필요한 메소드를 오버라이딩 합니다.
 - 출력되는 응답 정보를 변형하기 위해서 HttpServletResponseWrapper 를 이용하여 출력을 모으는  작업을 합니다.
 - 모은 응답 정보는 모두 대문자로 변환하여 출력하도록 합니다.

 

package com.tistory.pentode.filter;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.slf4j.Logger; import org.slf4j.LoggerFactory;

public class FirstFilter implements Filter {
	private static final Logger logger = LoggerFactory.getLogger(FirstFilter.class);
	private String encoding;

	/**
	 * init 함수는 컨텍스트 로드시 호출됩니다.
	 */
	@Override
	public void init(FilterConfig config) throws ServletException {
		logger.info("init call");

		// web.xml 에서 필터 등록시 정의했던 파라미터를 가져옵니다.
		this.encoding = config.getInitParameter("encoding");
	}

	/**
	 * 필터 실행 부분 입니다.
	 */
	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		// Controller 처리 전에 처리를 수행 하는곳 입니다.
		// 파라미터 값으로 POST 데이터의 인코딩을 지정합니다.
		request.setCharacterEncoding(this.encoding);

		// 요청과 응답에 필요한 처리를 수행할 Wrapper를 생성합니다.
		ServletRequest requestWrapper = new TestRequestWrapper((HttpServletRequest) request);
		ServletResponse responseWrapper = new TestResponseWrapper((HttpServletResponse) response);

		logger.info("before doFilter");

		// 다음 필터의 호출, 실제 자원의 처리를 합니다.
		chain.doFilter(requestWrapper, responseWrapper);

		// 응답에 대한 처리를 하는곳 입니다.
		logger.info("after doFilter");

		// 응답 래퍼를 이용하여 출력을 모두 대문자로 변형합니다.
		if(response.isCommitted() == false) {
			response.getWriter().write(responseWrapper.toString().toUpperCase());
		}
	}

	/**
	 * destroy 는 컨텍스트가 종료될 때 호출됩니다.
	 */
	@Override
	public void destroy() {
		logger.info("destroy call");
	}

	/**
	 * 요청 래퍼 입니다.
	 */
	class TestRequestWrapper extends HttpServletRequestWrapper {
		public TestRequestWrapper(HttpServletRequest request) {
			super(request);
		}

		/**
		 * 입력 파라미터에서 <, > 를 제거 합니다.
		 */
		@Override
		public String getParameter(String name) {
			String value = super.getParameter(name);
			if(value == null) value = "";
			return value.replaceAll("[<>]", "");
		}
	}

	/**
	 * 응답 래퍼 입니다.
	 */
	class TestResponseWrapper extends HttpServletResponseWrapper {

		protected CharArrayWriter charWriter;

		public TestResponseWrapper(HttpServletResponse response) {
			super(response);
			charWriter = new CharArrayWriter();
		}

		/**
		 * 출력을 나중에 수정할 수 있도록 모아둡니다.
		 */
		@Override
		public PrintWriter getWriter() throws IOException {
			return new PrintWriter(charWriter);
		}

		@Override
		public String toString() {
			return charWriter.toString();
		}
	}
}

 

 

3. 테스트용 컨트롤러와 jsp 페이지 입니다. src/main/java/com/tistory/pentode/TestController.java 파일 입니다.

 

package com.tistory.pentode;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class TestController {
    private static final Logger logger = LoggerFactory.getLogger(TestController.class);

    @RequestMapping(value = "/filter.do", method = RequestMethod.GET)
    public String filter(Model model) {
        logger.info("call filter.do");
        return "filter";
    }
}

 

/WEB-INF/views/filter.jsp 파일 입니다.

 

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Servlet Filter</title>
</head>
<body>
    <h1>Servlet Filter!</h1>
    <h1><%= request.getParameter("param") %></h1>
</body>
</html>

 

 

4. 실행 결과 입니다. 

 

http://localhost:8080/pentode/filter.do?param=<test> 로 요청합니다. 실행결과 모든 소스가 대문자로 변경되었고, <test> 는 test 로 변경 되었습니다.

 

실행결과

 

필터는 여러개를 등록할 수 있습니다. 등록된 필터의 처리 순서는 web.xml 에서 <filter-mapping> 이 나오는 순서대로 입니다.

 

같은 자원에 대해 처리를 수행하는 F1, F2, F3 세 개의 필터가 있다고 가정을 해 봅시다.

 

처리 순서는 F1 요청처리 -> F2 요청처리 -> F3 요청처리 -> 자원의 처리 -> F3 응답처리 -> F2 응답처리 -> F1 응답 처리 순 입니다.

 

일반적으로 필터 처리 도중에 예외가 발생하면 거기에서 처리를 멈추고 나머지 처리는 실행이 되지 않습니다.

 

스프링 프레임웍에서 Exception Resolver로 에러 페이지가 지정이 되어 있다면 Controller 실행 도중에 에러가 발생할 경우 지정된 에러 페이지가 보여지지만 필터들은 요청, 응답 모두 실행이 됩니다.


이상으로 서블릿 필터의 사용법을 알아 봤습니다. 요청에 대한 처리로 많이 사용되는 것으로는 인코딩 변환, 로그인 여부확인, 권한 체크, XSS(Cross site script) 방어 등이 있습니다. 응답 처리에 많이 사용되는 것은 모든 페이지 내용에 공통적으로 특정 내용을 끼워넣는 작업 등에 많이 사용됩니다.

 

이 글에서 만들어본 예제는 필터의 용도 및 흐름을 보여주기 위한 것으로 실제 코드에 사용하기에는 부적절할 수 있습니다. 많은 예외처리, 상태체크 등이 포함 되어야 합니다.

 

 

※ 2017년 9월 14일 내용 추가

 

필터가 요청에 적용되는 방법을 제한할 수 있습니다. 위의 예에서는 기본값으로 필터가 클라이언트로 부터 직접 오는 REQUEST 에 대해서만 동작합니다. 다음과 같습니다.

 

<filter-mapping>
    <filter-name>firstFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

 

필터 맵핑의 <dispatcher> 요소를 사용해서 필터가 적용될 곳을 지정할 수 있습니다. 사용될 수 있는 옵션은 다음과 같습니다.

 

- REQUEST : 클라이언트로부터의 직접 요청시 적용됩니다. 기본값입니다.

- FORWARD : 포워드 되는 컴포넌트(페이지) 에 적용됩니다.

- INCLUDE : 인클루드 되는 컴포넌트(페이지) 에 적용됩니다.

- ERROR : 에러페이지에 적용됩니다.(isErrorPage="true")

 

<dispatcher> 요소를 여러개를 동시에 지정할 수 있습니다.

 

반응형