ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Servlet]Filter를 통해서 특정 url 요청이 들어오면 응답 바디를 변환하여 클라이언트에게 전달하기
    1. JAVA/1.4 업무관련 알게 된 내용 2024. 9. 17. 16:14

    프론트앤드 프로젝트를 하며 백엔드 로직도 살짝 만지게 되었는데

    /rest라는 url 요청이 들어오면 /rest를 제외한 url로 변환하여 

    해당 백엔드 로직에 접근하고 , 응답 바디를 커스터마이징 하도록 구현하였다.

    ex ) /rest/test -> /test에 해당하는 컨트롤러에 접근

           -> 응답 데이터형식이 기존 list:[{...},{...}] 라면, /rest로 시작하는 요청건이라면 result로 한번 더 감싸서 result:{list:[{..},{..}]} 반환

     

    처음에는 interceptor에서 구현하려고 삽질을 하였지만, 인터셉터는 바디를 변환하는데 제한적이라는 것에 깨달음을 얻어 좀더 폭 넓은 Filter를 통하여 기능을 구현하였다.

     

    참고 ) 인터셉터와 필터와의 차이

    인터셉터 : 스프링MVC 기반 기술로, 요청이 컨트롤러에 도달하기 전과 후의 작업 수행.

    필터 : 자바 서블릿 기술. 서블릿의 요청, 응답의 전후에 대한 처리 담당. 클라이언트의 요청이 서블릿에 도달 전, 서블릿의 응답이 클라이언트에게 응답 전에 동작함.

     

    1. Filter 구현 및 Spring 등록

    2. doFilter 란에 원하는 로직 구현 -> requestURI 변환 :  /rest로 시작하는 url은 /rest를 제거 요청 URI를 변경하여 컨트롤러에 보넴

    3. 응답 body를 캡쳐 : HttpResponseWrapper 클래스 구현

    응답바디는 스트림을 통해서 읽어지게 되는데 (PrintWriter or OutputStream) 스트림 특성상 , 일방향으로만 데이터가 흐르기 때문에 다시 읽는게 불가능함.

    한번 읽어버리게 되면 해당 내용은 이미 응답 바디가 클라이언트에게 보내지기 전에 출력 스트림에 작성 되어 일반적으로 다시 읽거나 수정할 수 없는 상태가 된다고 한다. 그러므로 응답 바디를 캡쳐하고 수정하기 위해서는 응답 스트림을 통해 바디데이터를 가져와서 중간에서 변형해주는 방법을 사용해야 함. 

    4. RequestDispatcher(현재 서블릿의 요청을 다른 서블릿에 전달하기 위함) 객체 생성 후 include를 통하여 response 부분에 대한 내용을 3에서 만든 wrapper 클래스를 넣어주는 작업을 한다.

    5. 4번에서 생성한 RequestDispatcher 인스턴스에서 include 메서드를 통하여 현재 응답에 대한 다른 서블릿의 응답을 포함시킨다.

    (forward는 사용하면 응답이 새로생성 되고, include는 기존 응답에서 추가.)

    forward를 쓰면 응답이 새로 생성되어 또 다시 PrintWriter, OutputStream을 사용하게 됨. 하지만 3번에서 응답바디를 캡쳐할때 Stream이 이미 사용되었기 때문에 response.getWriter() has already been called for this response" or "Cannot call getWriter() after getOutputStream() has been called 예외가 터짐.

     

    코드구현

     

    1번

    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class TestConfiguration {
        @Bean
        public FilterRegistrationBean<TestFilter> testFiilter(){
            FilterRegistrationBean<TestFilter> registration = new FilterRegistrationBean<>();
            registration.setFilter(new TestFilter());
            registration.addUrlPatterns("/rest/*");
            return registration;
        }
    }

     

    2~5.

    import jakarta.servlet.*;
    import jakarta.servlet.annotation.WebFilter;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import jakarta.servlet.http.HttpServletResponseWrapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    
    import java.io.*;
    import java.nio.charset.StandardCharsets;
    
    @Slf4j
    @WebFilter("/rest/*")
    public class TestFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            Filter.super.init(filterConfig);
        }
    
        //2번
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
    
            String requestURI = httpRequest.getRequestURI();
    
                // '/rest'를 제거한 URI로 수정
            if(requestURI.contains("/rest")) {
               requestURI =  requestURI.substring(5);
    
               //응답 body를 캡쳐할 Wrapper 클래스 선언
               RestResponseWrapper restResponseWrapper = new RestResponseWrapper(httpResponse);
    
               //4번
                RequestDispatcher dispatcher = request.getRequestDispatcher(requestURI);
    
                //5번
                //forward와 include의 차이
                // forward: 다른 서블릿으로 응답이 전환되어 이를 클라이언트에게 전달함 >> 응답 새로생성
                // include: 현재 응답에 다른 서블릿의 응답을 포함시킴 >> 기존응답에 포함시
                dispatcher.include(request, restResponseWrapper);
    
                //응답바디를 바이트배열로 변환
                byte[] responseMessageBytes = getBytes((HttpServletResponse) response, restResponseWrapper);
    
                // 응답의 Content-Length를 설정
                int contentLength = responseMessageBytes.length;
                response.setContentLength(contentLength);
    
                // 응답 바이트 배열을 클라이언트에게 전송
                response.getOutputStream().write(responseMessageBytes);
    
                //응답 버퍼에 저장된 모든 데이터를 클라이언트에게 즉시 전하여 응답 완료
                response.flushBuffer();
    
            }else{
                chain.doFilter(request, response);
            }
    
        }
        /**
         * 응답 Body 변환
         * */
        private static byte[] getBytes(HttpServletResponse response, RestResponseWrapper restResponseWrapper) {
            StringBuilder sb = new StringBuilder();
            //response body 데이터를 result 로 한번 더 감싼다.
            sb.append("{\"result\": ");
            sb.append(restResponseWrapper.getDataStreamToString());
            //상태값이 200일때
            if(response.getStatus()==HttpServletResponse.SC_OK){
               sb.append(",status:true}");
            }
            else{
                sb.append(",status:false}");
            }
            return  sb.toString().getBytes(StandardCharsets.UTF_8);
        }
    
        @Override
        public void destroy() {
            Filter.super.destroy();
        }
    
    
    
        static class RestResponseBodyOutputStream extends ServletOutputStream{
            private final DataOutputStream outputStream;
    
            public RestResponseBodyOutputStream(OutputStream stream) {
                this.outputStream = new DataOutputStream(stream);
            }
    
            @Override
            public boolean isReady() {
                return true;
            }
    
            @Override
            public void setWriteListener(WriteListener listener) {
    
            }
    
            @Override
            public void write(int b) throws IOException {
                outputStream.write(b);
            }
        }
    
        //3번
        static class RestResponseWrapper extends HttpServletResponseWrapper{
            ByteArrayOutputStream byteArrayOutputStream;
            RestResponseBodyOutputStream restResponseBodyOutputStream;
    
            /**
             * Constructs a response adaptor wrapping the given response.
             *
             * @param response The response to be wrapped
             * @throws IllegalArgumentException if the response is null
             */
            public RestResponseWrapper(HttpServletResponse response) {
                super(response);
                byteArrayOutputStream = new ByteArrayOutputStream();
            }
    
    
            @Override
            public ServletOutputStream getOutputStream() throws IOException {
                if(restResponseBodyOutputStream == null){
                    restResponseBodyOutputStream = new RestResponseBodyOutputStream(byteArrayOutputStream);
                }
                return restResponseBodyOutputStream;
            }
    
            public String getDataStreamToString(){
                return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8);
            }
    
    
    
        }
    
    
    }

     

    결과

     

    기존

    변경 : /rest/test

Designed by Tistory.