필터(3) : 필터의 응용

de_sj_awa·2021년 5월 23일
0
post-custom-banner

필터를 사용하는 방법에는 제한이 없으며, 필터의 특징을 잘 활용하느냐에 따라서 필터의 응용 범위가 달라질 수 있다. 보통 다음과 같은 기능에 필터를 적용한다.

  • 사용자 인증
  • 캐싱 필터
  • 자원 접근에 대한 로깅
  • 응답 데이터 변환(HTML 변환, 응답 헤더 변환, 데이터 암호화 등)
  • 공통 기능 실행

이외에도 많은 활용 방법이 존재할 수 있지만, 역서 제시한 다섯 가지 정도가 필터의 주요 응용 방식이다. 여기서는 여러 응용 방법 중에서 사용자 인증 필터와 XSL/T 필터에 대해서 살펴볼 것이다. 이 두 가지 필터는 필터를 통한 흐름 제어 방법과 응답 데이터를 조작하는 방법을 보여주고 있기 때문에, 두 가지 필터가 어떤 식으로 동작하는지 이해한다면 그 외의 다른 종류의 필터도 어렵지 않게 구현할 수 있을 것이다.

1. 로그인 검사 필터

많은 사이트가 회원제로 운영하고 있고 로그인을 한 이후에 컨텐츠에 접속할 수 있도록 제한하는 곳도 존재한다. 특히 컨텐츠의 유료화 추세에 맞춰 사용자 인증을 거친 후 컨텐츠를 구매하도록 유도하는 사이트가 증가하고있다.

사용자 인증은 웹 사이트의 필수 기능이므로 각각의 JSP/서블릿 등의 코드가 사용자가 로그인했는지 여부를 판단하기 위한 코드를 구현할 수 있다. 하지만, JSP/서블릿마다 사용자 인증 코드를 넣으면, 회원 인증 정책이 변경될 때마다 관련된 모든 코드를 변경해야 한다는 문제가 발생한다.

이런 문제는 인증 여부를 검사하는 필터를 사용해서 깔끔하게 해결할 수 있다. 앞서 설명했듯이 웹 브라우저의 요청은 서블릿/JSP에 전달되기 전에 먼저 필터를 통과한다. 이는 필터에서 조건에 따라 알맞게 흐름을 제어할 수 있다는 것을 의미한다. 즉, 필터에서 로그인했는지 여부를 판단하고, 로그인하지 않았다면 로그인 폼을 보여주는 페이지로 흐름을 이동시킬 수 있는 것이다.

여기서는 간단하게 session에 "MEMBER" 속성이 존재하면 로그인한 것으로 판단하는 필터인 LoginCheckFilter 클래스를 작성해보자. LoginCheckFilter는 아래 예제 코드와 같다.

package filter;

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

import java.io.IOException;

public class LoginCheckFilter implements Filter {
    @Override
    public void init(FilterConfig config) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpSession session = httpRequest.getSession(false);

        boolean login = false;
        if (session != null) {
            if (session.getAttribute("MEMBER") != null) {
                login = true;
            }
        }
        if (login) {
            chain.doFilter(request, response);
        } else {
            RequestDispatcher dispatcher = request
                    .getRequestDispatcher("/loginForm.jsp");
            dispatcher.forward(request, response);
        }
    }

    @Override
    public void destroy() {
    }
}

로그인 여부를 검사하는 필터는 매우 간단하게 동작한다. 로그인 검사 필터는 로그인한 상태라면 필터 체인의 다음 필터로 이동하고, 로그인하지 않은 상태로 판단되면 로그인 페이지로 이동한다.

LoginCheckFilter를 테스트하기 위해서 web.xml 파일에 다음과 같ㅇ느 필터 설정을 추가해보자.

    <filter>
        <filter-name>LoginCheck</filter-name>
        <filter-class>filter.LoginCheckFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>LoginCheck</filter-name>
        <url-pattern>/board/*</url-pattern>
    </filter-mapping>

web.xml 파일의 설정에 따라 /board/*에 해당하는 요청을 보내면 LoginCheckFilter가 동작한다. LoginCheckFilter는 session에 "MEMBER" 속성이 존재하지 않으면 /loginForm.jsp로 포워딩한다. 실제로 session에 MEMBER 속성이 존재하지 않는 상태에서 /board/*에 해당하는 http://localhost:8080/chap19/board/boardList.jsp를 요청하면 아래 그림과 같이 필터는 /loginForm.jsp로 흐름을 이동시킨다.

loginForm.jsp는 아래 코드와 같이 간단하다. loginForm.jsp 결과 화면에서 로그인할 때 이를 처리하는 login.jsp 코드는 다음과 같다.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>로그인</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/login.jsp">
    아이디<input type="text" name="memberId">
    비밀번호<input type="password" name="password" %>
    <input type="submit" value="로그인">
</form>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String memberId = request.getParameter("memberId");
    session.setAttribute("MEMBER", memberId);
%>
<html>
<head>
    <title>로그인</title>
</head>
<body>
로그인 처리
</body>
</html>

로그인 폼에서 알맞게 아이디를 입력하고 [로그인] 버튼을 누르면 login.jsp를 실행하는데, login.jsp는 memberId 파라미터로 전달한 값을 그대로 session의 MEMBER 속성에 저장한다. 따라서, 로그인 후 다시 /board/boardList.jsp를 실행하면 LoginCheckFilter는 MEMBER 속성이 존재하므로 필터 체인을 통해 boardList.jsp를 실행한다. 결과적으로 아래 그림과 같이 boardList.jsp의 결과 화면을 응답으로 보게 된다.

인증 필터를 사용해서 얻은 장점은 서블릿/JSP와 같은 각종 최종 자원에서 일일이 로그인 인증여부를 판단하지 않아도 된다는 점이다. 서블릿과 JSP는 클라이언트의 요청을 처리하는 데 필요한 작업만 처리하면 되고, 사용자 인증을 검사하는 작업은 하지 않아도 된다. 로그인 검사 방식이 변경되더라도 필터만 변경하면 되고 나머지 서블릿이나 JSP는 수정할 필요가 없어진다.

2. XSL/T 필터

응답 데이터를 변경해주는 필터를 만들어보자. 여기서 만들 필터는 JSP나 서블릿이 생성한 XML 응답 데이터를 XSL/T를 이용해서 HTML로 변환해주는 기능을 제공한다. 응답 데이터를 반환하려면, 서블릿/JSP가 생성한 XML 데이터를 웹 브라우저에 곧바로 전송하면 안 된다. 대신, 서블릿/JSP가 생성한 데이터를 임시 버퍼에 저장하고, 그 버퍼에 저장된 XML 데이터를 XSL/T를 사용해서 변환해야 한다. 이를 구현하려면 다음 역할을 할 클래스를 구현해야 한다.

  • 응답 데이터를 임시로 보관하는 버퍼로 사용할 출력 스트림
  • 버퍼를 사용하는 응답 래퍼 클래스
  • 응답 래퍼 클래스를 이용해서 응답 데이터에 XSL/T 변환을 수행하는 필터

버퍼의 역할을 할 출력 스트림은 서블릿/JSP에서 사용하는 PrintWriter 타입이어야 한다. 이 예제에서 버퍼의 역할을 할 출력 스트림인 ResponseBufferWriter 클래스는 다음과 같다.

package filter;

import java.io.PrintWriter;
import java.io.StringWriter;

public class ResponseBufferWriter extends PrintWriter {
    
    public ResponseBufferWriter(){
        super(new StringWriter(4096));
    }
    
    public String toSting(){
        return ((StringWriter) super.out).toString();
    }
}

ResponseBufferWriter는 print(), println(), write() 등의 메서드를 통해서 전달된 데이터를 StringWriter에 저장한다. toString() 메서드는 StringWriter에 저장된 데이터를 String 타입으로 변환해준다.

출력 버퍼를 만들었으니 그다음으로 해야 할 일은 서블릿과 JSP가 ResponseBufferWriter를 출력 스트림으로 사용하도록 응답 래퍼 클래스를 작성하는 것이다. 이 예제에서 사용할 응답 래퍼 클래스는 다음과 같다.

package filter;

import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;

import java.io.PrintWriter;

public class XSLTResponseWrapper extends HttpServletResponseWrapper {
    
    private ResponseBufferWriter buffer = null;
    
    public XSLTResponseWrapper(HttpServletResponse response){
        super(response);
        buffer = new ResponseBufferWriter();
    }
    
    @Override
    public PrintWriter getWriter() throws java.io.IOException{
        return buffer;
    }
    
    @Override
    public void setContentType(String contentType){
        // do nothing
    }
    
    public String getBufferedString(){
        return buffer.toSting();
    }
}

JSP나 서블릿이 생성한 XML 데이터를 임시로 저장할 버퍼(ResponseBufferWriter)와 응답 래퍼(XSLTResponseWrapper)를 구현했으므로, 다음 작업은 필터를 구현하는 것이다. 필터는 다음과 같은 4단계로 작업을 처리한다.

  1. 응답 래퍼(XSLTResponseWrapper)를 생성한다.
  2. 생성한 응답 래퍼를 체인의 다음 필터에 전달한다.
  3. 래퍼로부터 서블릿/JSP가 출력한 데이터를 읽어와 XSL/T를 사용하여 HTML로 변환한다.
  4. 변환된 결과인 HTML을 실제 응답 스트림에 출력한다.

이 과정을 구현한 필터는 다음에 표시한 XSLTFilter 클래스이다.

package filter;

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletResponse;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.*;

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

    private String xslPath = null;

    @Override
    public void init(FilterConfig config) throws ServletException {
        // XSL/T 변환할 때 사용할 XSL 파일의 경로를 구한다.
        xslPath = config.getServletContext().getRealPath("/WEB-INF/xsl/book.xsl");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 필터가 생성할 출력의 콘텐츠 타입을 "text/html; charset=utf-8"로 지정한다.
        // 따라서 웹 브라우저에 출력되는 문서는 HTML 문서가 된다.
        response.setContentType("text/html; charset=utf-8");
        PrintWriter writer = response.getWriter();
        // 필터 체인을 통해서 전달할 응답 래퍼 객체를 생성한다.
        XSLTResponseWrapper responseWrapper = new XSLTResponseWrapper((HttpServletResponse) response);
        // 체인을 실행한다. 체인을 통해서 응답 래퍼 객체가 전달되므로
        // JSP나 서블릿이 출력하는 내용은 응답 객체의 버퍼
        // (, XSLTResponseWrapper 클래스의 buffer 필드)에 저장된다.
        chain.doFilter(request, responseWrapper);

        // XSL/T 변환
        try{
            TransformerFactory factory = TransformerFactory.newInstance();
            Reader xslReader = new BufferedReader(new FileReader(xslPath));

            StreamSource xslSource = new StreamSource(xslReader);

            Transformer transformer = factory.newTransformer(xslSource);

            // 응답 래퍼로부터 JSP/서블릿이 생성한 내용을 읽어온다. XML 문서 원본으로 사용한다.
            String xmlDocument = responseWrapper.getBufferedString();
            Reader xmlReader = new StringReader(xmlDocument);
            StreamSource xmlSource = new StreamSource(xmlReader);

            StringWriter buffer = new StringWriter(4096);

            // XSL/T 변환을 실행한다.
            transformer.transform(xmlSource, new StreamResult(buffer));

            // 변환 결과를 출력한다.
            writer.println(buffer.toString());
        }catch (Exception ex){
            throw new ServletException(ex);
        }

        writer.flush();
        writer.close();
    }

    @Override
    public void destroy() {
    }
}

필터까지 모든 구현이 끝났다. @WebFilter 애노테이션을 사용했으므로 따로 web.xml 파일에 설정을 추가하지 않아도 필터가 등록된다.

위 코드를 보면 /xml/*로 들어오는 모든 요청에 대해서 XSLT 필터가 적용되도록 설정했다. 이 요청에 해당하는 JSP나 서블릿은 XSL 파일로 변환할 수 있는 XML 문서를 생성하면 된다. 예제에서 사용할 XSL 파일은 아래와 같다.

<?xml version="1.0" encoding="utf-8" ?>
  
<xsl:stylesheet 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method = "html" indent="yes" encoding="utf-8" />
    <xsl:template match="list">
<html>
<head><title>책 목록</title></head>
<body>
현재 등록되어 있는 책의 목록은 다음과 같습니다.
<ul>
    <xsl:for-each select="book">
    <li><b><xsl:value-of select="title" /></b>
    (<xsl:value-of select="price" /> 원)
    <br />
    <i><xsl:value-of select="author" /></i>
    </li>
    </xsl:for-each>
</ul>
</body>
</html>
    </xsl:template>
</xsl:stylesheet>

book.xsl 파일에 의해서 알맞게 변환될 수 있는 XML 문서를 생성하는 JSP 페이지는 아래 코드와 같이 작성할 수 있다. 실제 웹 어플리케이션에는 DB에서 데이터를 읽어와 XML 문서로 출력해주는 형태가 되겠지만, 이 예제는 XSLTFilter의 처리 결과를 보여주는 것이 목적이므로 XML 문서를 그대로 사용했다.

<?xml version="1.0" encoding="utf-8" ?>
<%@ page contentType="text/xml; charset=utf-8" %>
<%@ page trimDirectiveWhitespaces="true" %>
<list>
  <book>
    <title>스프링 4 프로그래밍 입문</title>
    <author>최범균</author>
    <price>25,000</price>
  </book>

  <book>
    <title>객체 지향과 디자인 패턴</title>
    <author>최범균</author>
    <price>20,000</price>
  </book>
</list>

bookList.jsp가 생성한 XML 문서는 앞서 작성한 XSLTFilter를 통해 HTML 문서로 변환 처리되어 출력된다. 실제로 bookList.jsp를 웹 브라우저를 통해 실행해보면 아래 그림과 같이 XML이 아닌 XSL/T를 통해 HTML로 변환된 결과가 출력되는 것을 확인할 수 있다.

실제로 테스트를 해보니 xml 파일이 한글 인코딩이 깨지는 문제가 발생해서 utf-8에서 euc-kr로 인코딩을 모두 바꿔줬더니 한글 인코딩이 깨지지 않았다.

XSLT 필터가 적용되는 경우와 적용되지 않는 경우의 차이를 보여주기 위해 bookList.jsp와 똑같은 코드를 갖는 JSP 파일을 chap19/xml2 폴더에 복사한 뒤 실행해보자.

XSLTFilter는 /xml/*로 들어오는 경로에 대해서만 XSLTFilter를 적용하므로 /xml2/bookList.jsp에는 이 필터를 적용하지 않는다. 실제로 /xml2/bookList2.jsp의 실행 결과인 아래 그림을 보면 JSP가 생성한 XML 문서가 그대로 출력되었는데, 이 결과를 통해서 필터가 적용되는 경우와 그렇지 않은 경우의 차이점을 확인할 수 있을 것이다.

3. 캐릭터 인코딩 필터

지금까지 작성한 JSP 코드는 요청 파라미터의 글자를 올바르게 처리하기 위해 다음과 같이 캐릭터 인코딩을 설정했다.

<% request.setCharacterEncoding("utf-8"); %>

요청 파라미터를 사용하는 모든 JSP 코드마다 인코딩을 설정하기 위해 이 코드를 추가하는 것이 잘못된 것은 아니지만 동일한 코드가 여러 곳에 중복되어 출현하는 것은 좋은 방법이 아니다.

요청 파라미터의 캐릭터 인코딩을 한 코드에서 설정하는 방법이 있는데 그것은 바로 필터를 사용하는 것이다. 캐릭터 인코딩을 설정하는 필터를 사용하면 JSP에 요청 캐릭터 인코딩을 설정하는 코드를 추가할 필요 없이 필터 한 곳에만 코드를 추가하면 된다. 캐릭터 인코딩을 설정하기 위한 필터는 아래와 같이 작성할 수 있다.

package util;

import jakarta.servlet.*;

import java.io.IOException;

public class CharacterEncoding implements Filter {
    
    private String encoding;

    @Override
    public void init(FilterConfig config) throws ServletException {
        encoding = config.getInitParameter("encoding");
        if(encoding == null){
            encoding = "UTF-8";
        }
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        req.setCharacterEncoding(encoding);
        chain.doFilter(req, res);
    }

    @Override
    public void destroy() {
    }
}

setCharacterEncoding() 메서드를 이용해서 요청 캐릭터 인코딩을 설정한다. 사용할 인코딩은 "encoding" 초기화 파라미터를 이용해서 설정한다.

CharacterEncodingFilter를 사용하려면 web.xml에 다음과 같은 설정을 추가하면 된다.

<filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>util.CharacterEncoding</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>

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

encoding 초기화 파라미터를 이용해서 사용할 인코딩을 지정하고, 필터 매핑을 통해서 어떤 URL 패턴에 필터를 적용할지 지정한다. 그러면, 필터를 통해서 요청 캐릭터 인코딩을 설정하기 때문에 JSP마다 요청 캐릭터 인코딩을 설정하지 않아도 된다.

참고

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

0개의 댓글