필터(2) : 필터의 구현

de_sj_awa·2021년 5월 23일
0

필터를 구현하는 데 있어 핵심은 다음의 3개 타입이다.

  • javax.servlet.Filter 인터페이스 : 클라이언트와 최종 자원 사이에 위치하는 필터를 나타내는 객체가 구현해야 하는 인터페이스이다.
  • javax.servlet.ServletRequestWrapper 클래스 : 필터가 요청을 변경한 결과를 저장하는 래퍼이다.
  • javx.servlet.ServletResponseWrapper 클래스 : 필터가 응답을 변경하기 위해 사용하는 래퍼이다.

개발자는 이 세 타입을 이용해서 필터를 구현하고 요청과 응답 정보를 변경하는 기능을 구현할 수 있다.

1. Filter 인터페이스

Filter 인터페이스는 다음과 같은 메서드를 선언하고 있으며, 필터 기능을 제공할 클래스는 Filter 인터페이스를 알맞게 구현해주어야 한다.

  • public void init(FilterConfig filterConfig) throws ServletException : 필터를 초기화할 때 호출된다.
  • public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws java.io.IOException, ServletException : 필터 기능을 수행한다. chain을 이용해서 체인의 다음 필터로 쿼리를 전달할 수 있다.
  • public void destory() : 필터가 웹 컨테이너에서 삭제될 때 호출된다.

위의 메서드에서 필터의 역할을 하는 메서드가 바로 doFilter() 메서드이다. 서블릿 컨테이너는 사용자가 특정한 자원을 요청했을 때 그 자원 사이에 필터가 존재하는 경우 필터 객체의 doFilter() 메서드를 호출한다. 바로 이 시점부터 필터를 적용하기 시작한다. 다음은 전형적인 필터의 구현 방법을 보여주고 있다.

public class FirstFilter implements Filter {
    
    public void init(FilterConfig filterConfig) throws ServletException {
        // 필터 초기화 작업
    }
    
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException) throws IOException, ServletException {
        // 1. request 파라미터를 이용하여 요청의 필터 작업 수행
        ...
        // 2. 체인의 다음 필터 처리
        chain.doFilter(request, response);
        
        // 3. response를 이용하여 응답의 필터링 작업 수행
        ...
    }
    
    public void destroy(){
        // 주로 필터가 사용한 자원을 반납
    }
}

위 코드에서 Filter 인터페이스의 doFilter() 메서드는 요청이 있을 때마다 매번 실행된다. 예를 들어, 클라이언트가 요청한 자원이 필터를 거치는 경우 클라이언트의 요청이 있을 때마다 doFilter() 메서드가 호출된다. doFilter() 메서드는 JSP/서블릿과 마찬가지로 요청에 대해서 필요한 작업을 처리하게 된다.

위 코드를 보면 doFilter() 메서드는 세 번째 파라미터로 FilterChain 객체를 전달받는 것을 알 수 있다. 이는 클라이언트가 요청한 자원에 이르기까지 클라이언트의 요청이 거쳐 가게 되는 필터 체인을 의미한다. FilterChain을 사용해서 필터는 체인에 있는 다음 필터에 변경한 요청과 응답을 전달할 수 있다.

위 코드에서 우리가 또 알아야 한다는 것은 요청을 필터링 한 필터 객체가 또다시 응답을 필터링한다는 점이다. 위 코드의 doFilter() 메서드를 보면 1, 2, 3이라는 숫자를 사용하여 doFilter() 메서드 내에서 이루어지는 작업의 순서를 표시했는데, 그 순서를 다시 정리해보면 다음과 같다.

  1. request 파라미터를 이용하여 클라이언트의 요청을 필터링한다. 1단계에서는 RequestWrapper 클래스를 사용하여 클라이언트의 요청을 변경할 수 있다.
  2. chain.doFilter() 메서드를 호출한다. 2단계에서는 요청의 필터링 결과를 다음 필터에 전달한다.
  3. response 파라미터를 이용하여 클라이언트로 가는 응답을 필터링한다. 3단계에서는 체인을 통해서 전달된 응답 데이터를 변경하여 그 결과를 클라이언트에 전송한다.

1단계와 3단계 사이에서 다음 필터로 이동하기 때문에 요청의 필터 적용 순서와 응답의 필터 적용 순서는 위의 그림에서 보듯이 반대가 된다.

init() 메서드에 전달되는 FilterConfig는 필터의 초기화 파라미터를 읽어올 때 사용하며 제공하는 메서드는 아래와같다.

메서드 리턴 타입 설명
getFilterName() String 설정 파일에서 <filter-name>에 지정한 필터의 이름을 리턴한다.
getInitParameter(String name) String 설정 파일의 <init-param>에서 지정한 초기화 파라미터의 값을 읽어온다. 존재하지 않는 경우 null을 리턴한다.
getInitParameterNames() Enumeration<String> 초기화 파라미터의 이름 목록을 구해온다.
getServiceContext() ServletContext 서블릿 컨텍스트 객체를 구한다.

2. 필터 설정하기 : web.xml 이용

필터를 설정하는 방법은 두 가지가 있는데, 첫 번째 방법은 web.xml 파일에 관련 정보를 추가하는 것이다. 또 다른 방법은 필터 클래스에서 @WebFilter 애노테이션을 사용하는 방법이다.

web.xml 파일에 필터를 설정하려면 다음과 같이 <filter> 태그와 <filter-mapping> 태그를 사용하면 된다.

<?xml version="1.0" encoding="utf-8"?>

<web-app ...>
  
  <filter>
    <filter-name>FilterName</filter-name>
    <filter-class>javacan.filter.FileClass</filter-class>
    <init-param>
      <param-name>paramName</param-name>
      <param-value>value</param-value>
    </init-pram>
  </filter>
  
  <filter-mapping>
    <filter-name>FilterName</filter-name>
    <url-pattern>*.jsp</url.patter>
  </filter-mapping>
  
  ...
  
</web-app>

여기서 <filter> 태그는 웹 어플리케이션에서 사용할 필터를 지정한다. <filter-mapping> 태그는 특정 자원에 대해 어떤 필터를 사용할지를 지정한다. 위 예제는 클라이언트가 .jsp 확장자를 갖는 경로를 요청한 경우 FilterName 필터를 적용하도록 설정하고 있다.

<filter> 태그의 <init-param> 태그는 초기화할 때, 즉 필터의 init() 메서드를 호출할 때 전달할 파라미터를 설정한다. 주로 필터를 사용하기 전에 초기화 작업에 필요한 정보를 제공하기 위해 사용한다.

<url-pattern> 태그는 클라이언트가 요청한 특정 URI에 대해서 필터링할 때 사용된다. 서블릿에서 사용하는 <url-pattern>과 동일한 규칙을 갖는다.

<url-pattern> 태그를 사용하지 않고, 대신 <servlet-name> 태그를 사용하면 특정 서블릿에 대한 요청에 대해서 필터를 적용하게 된다. 예를 들면 다음과 같이 이름이 FileDownLoad인 서블릿에 대해서 AuthCheckFilter를 필터로 사용하도록 할 수 있다.

<filter-mapping>
  <filter-name>AuthCheckFilter</filter-name>
  <servlet-name>FileDownLoad</servlet-name>
</filter-mapping>

<servlet>
  <servlet-name>FileDownLoad</servlet-name>
  ...
</servlet>

<dispathcer> 태그를 사용하면 필터가 적용되는 시점을 설정할 수 있다. <dispatcher> 태그는 다음과 같이 <filter-mapping> 태그의 자식 태그로 사용된다.

<filter-mapping>
  <filter-name>AuthCheckFilter</filter-name>
  <servlet-name>FileDownLoad</servlet-name>
  <dispatcher>INCLUDE</dispatcher>
</filter-mapping>

<dispatcher> 태그는 실행되는 자원을 클라이언트가 요청한 것인지, 아니면 RequestDispatcher의 forward()를 통해서 이동한 것인지, 아니면 include()를 통해서 포함되는 것인지에 따라서 필터를 적용하도록 지정할 수 있다. <dispatcher> 태그가 가질 수 잇는 값은 다음과 같다.

  • REQUEST : 클라이언트의 요청인 경우 필터를 적용한다. (기본값)
  • FORWARD : forward()를 통해서 제어 흐름을 이동하는 경우에 필터를 적용한다.
  • INCLUDE : include()를 통해서 포함되는 경우에 필터를 적용한다.

예를 들어, 다음의 필터 매핑 정보를 살펴보자.

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

<filter-mapping>
  <filter-name>IPCheckFilter</filter-name>
  <url-pattern>/pds/data/*</url-pattern>
  <dispatcher>INCLUDE</dispathcer>
</filter-mapping>

웹 브라우저에서 /pds/data/download.jsp를 요청했다고 하자. 이 경우 두 개의 필터 매핑 모두 <url-pattern>에 따라서 이 URL을 처리할 수 있다. 그런데 두 번째 필터 매핑은 <dispatcher>의 값이 INCLDUE이기 때문에 웹 브라우저의 요청에 대해서는 필터가 적용되지 않는다.

반대로 이제 다음과 같은 <jsp:include> 코드를 사용했다고 가정한다.

<jsp:include page="/pds/data/util.jsp" flush="false" />

이 경우 /pds/data/util.jsp는 두 개의 필터 모두에 해당되지만, 첫 번째 필터는 웹 브라우저의 요청에 대해서만 적용되기 때문에 사용되지 않고, <dispatcher>의 값이 INCLDUE인 두 번째 필터만 사용된다.

웹 브라우저의 요청이 동시에 여러 개의 필터 매핑에 적용되는 경우 web.xml 파일에 표시한 순서대로 필터를 적용한다. 예를 들어, 다음과 같이 필터 매핑을 설정했다고 해보자.

<filter-mapping>
  <filter-name>AuthCheckFilter</filter-name>
  <url-pattern>/pds/*</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>IPCheckFilter</filter-name>
  <url-pattern>/pds/data/*</url-pattern>
</filter-mapping>

<filter-mapping>
  <filter-name>CompressFilter</filter-name>
  <url-patter>/pds/data/*</url-patter>
</filter-mapping>

웹 브라우저가 /pds/data/a.jsp를 요청하면 필터는 AuthCheckFilter -> IPCheckFilter -> CompressFilter 순서대로 실행된다.

하나의 <filter-mapping>에서 한 개 이상의 <url-pattern> 태그와 <servlet-name> 태그를 설정할 수 있다. 예를 들어, 다음과 같은 설정이 가능하다.

<filter-mapping>
  <filter-name>IPCheckFilter</filter-name>
  <url-pattern>/pds/data/*</url-pattern>
  <url-pattern>/webinterface/*</url-pattern>
  <servlet-name>DownloadServlet</servlet-name>
</filter-mapping>

3. 필터 설정하기 : @WebFilter 애노테이션 이용

필터를 설정하는 또 다른 방법은 @WebFilter 애노테이션을 사용하는 것이다. web.xml 파일에 지정하지 않더라도 Filter 클래스가 @WebFilter 애노테이션을 가지면 자동으로 필터로 등록된다.

@WebFilter 애노테이션을 사용하는 방법은 다음과 같다.

import javax.servlet.annotation.WebFilter;

@WebFilter(filterName = "xsltFilter", urlPatterns = "{/xml/*"})
public class XSLTFilter implements Filter{
    ... 필터 구현
}

위와 같이 설정한 경우 해당 필터는 /xml/*에 일치하는 URL에 적용된다. 두 개 이상의 URL 패턴을 지정하고 싶다면, 다음과 같이 콤마로 구분해서 여러 URL 패턴을 지정하면 된다.

@WebFilter(filterNamer = "xsltFiler", urlPatterns = {"/xml/*", "/xsl/*"})
public class XSLTFilter implements Filter{

@WebFilter 애노테이션의 주요 속성은 다음과 같다.

  • urlPatterns : 필터를 적용할 URL 패턴 목록을 지정한다.
  • servletNames : 필터를 적용할 서블릿 이름 목록을 지정한다.
  • filterName : 필터의 이름을 지정한다.
  • initParams : 초기화 파라미터 목록을 지정한다.
  • dispatcherType : 필터를 적용할 범위를 지정한다. 열거 타입인 Dispatcher에 정의된 값을 사용한다. 기본값은 DispatchType.REQUEST이다.

4. 요청 및 응답 래퍼 클래스

필터가 필터로서의 제 기능을 하려면 클라이언트의 요청을 변경하고 클라이언트로 가는 응답을 변경할 수 있어야 한다. 요청과 응답을 변경할 때 사용하는 것이 바로 ServletRequestWrapper와 ServletResponseWrapper이다. 래퍼 클래스를 이용하면 다음을 할 수 있다.

  • 요청 정보를 변경하여 최종 자원인 서블릿/JSP/HTML/기타 자원에 전달한다.
  • 최종 자원으로부터 응답을 변경하여 새로운 응답 정보를 클라이언트에 보낸다.

서블릿의 요청 래퍼와 응답 래퍼를 만들려면 javax.servlet 패키지에 정의되어 있는 ServletRequestWrapper 클래스와 ServletResponseWrapper 클래스를 상속받아서 구현해야 한다. 대부분 필터는 HTTP 프로토콜에 대한 요청과 응답을 필터링하기 때문에 이 두 클래스를 상속받아 알맞게 구현한 HttpServletRequestWrapper 클래스와 HttpServletResponseWrapper 클래스를 상속받아 구현하는 것이 좋다.

HttpServletRequestWrapper 클래스와 HttpServletResponseWrapper 클래스는 모두 javax.servlet.http 패키지에 정의되어 있다. 이 두 클래스는 각각 HttpServletRequest 인터페이스와 HttpServletResponse 인터페이스에 정의되어 있는 모든 메서드를 이미 구현해 놓고 있다.

필터를 통해서 변경하고 싶은 요청 정보가 있다면 HttpServletRequestWrapper 클래스를 상속받은 클래스를 만든 뒤 그 정보를 추출하는 메서드를 알맞게 재정의해서 변경된 정보를 제공하도록 구현한다. 그리고 구현한 래퍼 클래스의 객체를 FilterChain의 doFilter() 메서드에 넘겨주기만 하면 된다.

예를 들어, 지정한 파라미터가 존재하지 않을 경우, 파라미터의 값을 공백문자열("")로 제공하는 요청 래퍼 클래스는 아래 예제 코드와 같이 작성할 수 있다.

package filter;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

public class NullParameterRequestWrapper extends HttpServletRequestWrapper {
    
    private Map<String, String[]> parameterMap = null;
    
    public NullParameterRequestWrapper(HttpServletRequest request){
        super(request);
        parameterMap = new HashMap<String, String[]>(request.getParameterMap());
    }
    
    public void checkNull(String[] parameterNames){
        for(int i = 0; i < parameterNames.length; i++){
            if(!parameterMap.containsKey(parameterNames[i])){
                String[] values = new String[] {""};
                parameterMap.put(parameterNames[i], values);
            }
        }
    }
    
    @Override
    public String getParameter(String name){
        String[] values = getParameterValues(name);
        if(values != null && values.length > 0){
            return values[0];
        }
        return null;
    }
    
    @Override
    public Map<String, String[]> getParameterMap(){
        return parameterMap;
    }
    
    @Override
    public Enumeration<String> getParameterNames(){
        return Collections.enumeration(parameterMap.keySet());
    }
    
    @Override
    public String[] getParameterValues(String name){
        return (String[]) parameterMap.get(name);
    }
}

NullParameterRequestWrapper 클래스는 요청 래퍼 클래스다. checkNull() 메서드를 통해서 빈 문자열을 기본값으로 지정할 파라미터의 목록을 전달받아 처리한다.

NullParameterRequestWrapper를 사용해서 요청 파라미터를 처리하는 필터 클래스는 아래 예제 코드와 같이 작성할 수 있다.

package filter;


import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;
import java.util.StringTokenizer;

public class NullParameterFilter implements Filter {

    private String[] parameterNames = null;

    @Override
    public void init(FilterConfig config) throws ServletException{
        String names = config.getInitParameter("parameterNames");
        StringTokenizer st = new StringTokenizer(names, ",");
        parameterNames = new String[st.countTokens()];

        for(int i = 0; st.hasMoreTokens(); i++){
            parameterNames[i] = st.nextToken();
        }
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        NullParameterRequestWrapper requestWrapper = new NullParameterRequestWrapper((HttpServletRequest) request);
        requestWrapper.checkNull(parameterNames);

        chain.doFilter(requestWrapper, response);
    }

    @Override
    public void destroy(){
    }
}

NullParameterFilter를 작성했으므로 이 필터를 사용하도록 web.xml 파일을 아래와 같이 작성하자. web.xml 파일은 *.jsp로 들어오는 요청에 대해서 NullParameterFilter를 필터로 적용하도록 설정하고 있다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
         version="5.0">
    
    <filter>
        <filter-name>NullParameter</filter-name>
        <filter-class>filter.NullParameterFilter</filter-class>
        <init-param>
            <param-name>parameterNames</param-name>
            <param-value>id,name</param-value>
        </init-param>
    </filter>
    
    <filter-mapping>
        <filter-name>NullParameter</filter-name>
        <url-pattern>*.jsp</url-pattern>
    </filter-mapping>
</web-app>

parameterNames 초기화 파라미터의 값으로 "id,name"을 지정했다. 이는 id 파라미터와 name 파라미터가 존재하지 않을 경우 이 두 파라미터의 기본 값을 ""로 설정한다는 것을 의미한다. 실제로 파라미터의 기본값이 지정되는지 확인해보기 위해 아래와 같이 JSP 페이지를 작성해보았다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>NullParameterFilter 테스트</title>
</head>
<body>
id 파라미터 : <%= request.getParameter("id")%><br>
name 파라미터 : <%= request.getParameter("name")%><br>
member 파라미터 : <%= request.getParameter("member")%>
</body>
</html>

nullParam.jsp는 id, name, member 파라미터 값을 출력하는 단순한 JSP 페이지이다. 앞서 web.xml 파일에서 NullParameterFilter가 id 파라미터와 name 파라미터의 기본값을 지정하도록 필터를 설정했다. 필터가 동작하는지 확인해보기 위해 다음과 같은 URL을 사용해서 nullParam.jsp를 테스트 해보자.

http://localhost:8080/chap19/nullParam.jsp?id=madvirus

위 URL을 실행하면 id 파라미터만 전달하므로 필터가 동작하지 않은 경우 nullParam, jsp는 name 파라미터와 member 파라미터의 값으로 null로 출력해야 한다. 필터가 동작하는 경우 name 파라미터 값은 공백 문자열("")이어야 한다. 실행 결과인 아래 그림을 보면 필터가 동작해서 name 파라미터 값으로 null 대신 빈 문자열을 출력한 것을 확인할 수 있다.

참고

  • 최범균의 JSP2.3 웹 프로그래밍
profile
이것저것 관심많은 개발자.

0개의 댓글