Burninghering's Blog
article thumbnail
Published 2022. 6. 18. 05:31
Spring boot - Filter Spring

 

처음으로 Lombok 사용해보기!

@Data를 쓰면,

Getter/Setter뿐만 아니라 equals/hashCode/toString까지 만들어준다

 

Lombok이 프로그램이 실행될때는 필요없다

컴파일됐을 때 이미 클래스파일에다 생성자/Getter/Setter 등이 만들어져있기 때문에


여태까지는 sout를 사용하여 시스템 로그를 남겨왔는데,

@Slf4j 어노테이션을 사용하면 로그를 쉽게 남길 수 있다.

package com.example.filter.controller;

import com.example.filter.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api/user")
public class ApiController {

    @PostMapping("")
    public User user(@RequestBody User user){
        log.info("User : {}",user);
        return user;
    }
}

package com.example.filter.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

@Slf4j //로그 남기기
@Component //스프링에 등록시킴
public class GlobalFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /////////////////들어가기 전, 전처리 구간

        HttpServletRequest httpServletRequest = (HttpServletRequest)request; // (2)String url = request. 할 때 쓸 수 있는 메소드가 없자 request를 형변화했다
            // (3)HttpServletRequest는 ServletRequest를 상속받은 클래스이기 때문에, 메소드가 다 구현되어있으니 우리는 인터페이스만 바꾸자
        HttpServletResponse httpServletResponse = (HttpServletResponse)response; //(4)

        String url = httpServletRequest.getRequestURI(); //(5)

        BufferedReader br = httpServletRequest.getReader(); //(6)

        br.lines().forEach(line->{ //(7)
            log.info("url : {} , line : {}",url,line); //(9) url도 찍는다
        });

        //String url = request. (1)
        //chain.doFilter(request,response); (1-1)

        chain.doFilter(httpServletRequest,httpServletResponse); // (8)doFilter에 변환한 request와 response를 넣어주자

        /////////////////chain.doFilter 동작 후 response 생성됨, 후처리 구간

    }
}

코드 작성 후 프로그램 돌리기!

 

근데 에러가 뜬다...

왜일까?

 

자바는 커서단위로 읽어버리기 때문에,

요청으로 Body를 읽더라도 한번에 한 줄 끝까지 다 읽어버린다.

 

Read를 한 번 해버리면,

클라이언트에서 오는 요청을 더 이상 읽을 수 없다.

 

2022-06-17 02:53:31.756  INFO 16528 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter   : url : /api/user , line : {
2022-06-17 02:53:31.758  INFO 16528 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter   : url : /api/user , line :   "name":"steve",
2022-06-17 02:53:31.758  INFO 16528 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter   : url : /api/user , line :   "age":10
2022-06-17 02:53:31.758  INFO 16528 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter   : url : /api/user , line : }

위 코드와 같이 GlobalFilter에서 한 번 읽어버리면,

더 이상 스프링이 컨트롤러에 json body를 전달하려고 했더니 내용이 없는 상태이다

 

이것을 해결해줄 수 있는 방법은?

ContentCachingRequestWrapper httpServletRequest = (ContentCachingRequestWrapper)request;
ContentCachingResponseWrapper httpServletResponse = (ContentCachingResponseWrapper)response;

ContentCaching Wrapper!

 

br.lines().forEach(line->{
    log.info("url : {} , line : {}",url,line);
});

위 코드에서 한 번 읽어도, 계속해서 내용을 읽을 수 있다!

이미 캐싱이 되어있기 때문에...

 

 

자 코드를 파고 파고 들어가보잣

public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {

   private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";


   private final ByteArrayOutputStream cachedContent;

   @Nullable
   private final Integer contentCacheLimit;

   @Nullable
   private ServletInputStream inputStream;

   @Nullable
   private BufferedReader reader;
ByteArrayOutputStream

에다가 미리 내용을 담아두고, 

 

스프링이 원한다던지, 

그 뒤에 누가 읽으려고 하면 cachedContent에 담겨진 내용을 리턴해준다.

 

몇번이고 다시 계속 읽을 수 있게 만들어주는 것이 위 클래스의 핵심이다. 

이미 만들어진 클래스를 잘 활용해서 쓰는 것 또한 좋은 방법이다.

그러므로 ContentCaching Wrapper을 사용하자.

 

package com.example.filter.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

@Slf4j
@Component
public class GlobalFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /////////////////들어가기 전, 전처리 구간

        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

        String url = httpServletRequest.getRequestURI();

        BufferedReader br = httpServletRequest.getReader();

        br.lines().forEach(line->{
            log.info("url : {} , line : {}",url,line);
        });

        chain.doFilter(httpServletRequest,httpServletResponse);

        /////////////////chain.doFilter 동작 후 response 생성됨, 후처리 구간

    }
}
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

ContentCaching Wrapper를 쓰기 위해 

캐스팅이 아닌 생성을 한다.

(HttpServletRequest)request 를 매개변수로 주고 생성한다.

 

ContentCachingRequestWrapper

클래스에 들어가보면, 매개변수가 위와 같이 들어가있다.

ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

이렇게 형변환을 한 번 시켜서 넘겨주면 된다.

 

이렇게 해서 실행을 하면 또 다시 에러가 난다

 

그 이유는

public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {

   private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";


   private final ByteArrayOutputStream cachedContent;

   @Nullable
   private final Integer contentCacheLimit;

   @Nullable
   private ServletInputStream inputStream;

   @Nullable
   private BufferedReader reader;


   /**
    * Create a new ContentCachingRequestWrapper for the given servlet request.
    * @param request the original servlet request
    */
   public ContentCachingRequestWrapper(HttpServletRequest request) {
      super(request);
      int contentLength = request.getContentLength();
      this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
      this.contentCacheLimit = null;
   }
   .
   .
   .

생성자에서

컨텐츠의 길이에 대해서만 지정을 해놓고 있고

실제로 내용은 나중에 사용한다.

 

ByteArrayOutputStream

에서 길이는 정해놨지만 안의 내용은 복사해놓지 않은 상태라서..

 

 

그래서 이걸 읽는 방법은, 

doFilter의 모든 후처리로 간다.

스프링이 모두 매핑한 뒤에 읽어야 하므로

br.lines().forEach(line->{
    log.info("url : {} , line : {}",url,line);
});

위 코드 삭제

 

아래와 같이 코드 작성

package com.example.filter.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

@Slf4j
@Component
public class GlobalFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /////////////////들어가기 전, 전처리 구간

        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
        //(ContentCaching를 생성했을 때 read하지 않고 일단 길이만 초기화시킴)

        chain.doFilter(httpServletRequest,httpServletResponse);
        //doFilter를 통해 실제 내부 스프링 안에 들어가야 그 메소드가 실행되어 컨텐츠가 아래 ByteArray에 들어갈것임.
        //그래서 핵심은? doFilter이후에 찍어야한다!

        String url = httpServletRequest.getRequestURI(); //그러니 이 코드도 doFilter 아래로 내리자

       /////////////////chain.doFilter 동작 후 response 생성됨, 후처리 구간
        //doFilter가 일어난 후 request에 대한 정보를 찍어보자
        String reqContent = new String(httpServletRequest.getContentAsByteArray()); //컨텐츠의 내용을 ByteArray로 받을것임
        log.info("requset url : {}, request body : {}",url,reqContent);

        String resContent = new String(httpServletResponse.getContentAsByteArray()); //컨트롤러를 타고 response에 담겨서 옴
        //응답 찍기
        int httpStatus=httpServletResponse.getStatus();

        log.info("response status : {}, responseBody : {}",httpStatus,resContent);
    }
}

잘 넘어갔지만

문제는 Client측 Body가 비어있다

 

package com.example.filter.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;

@Slf4j
@Component
public class GlobalFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        /////////////////들어가기 전, 전처리 구간

        ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request);
        ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);
        //(ContentCaching를 생성했을 때 read하지 않고 일단 길이만 초기화시킴)

        chain.doFilter(httpServletRequest,httpServletResponse);
        //doFilter를 통해 실제 내부 스프링 안에 들어가야 그 메소드가 실행되어 컨텐츠가 아래 ByteArray에 들어갈것임.
        //그래서 핵심은? doFilter이후에 찍어야한다!

        String url = httpServletRequest.getRequestURI(); //그러니 이 코드도 doFilter 아래로 내리자

       /////////////////chain.doFilter 동작 후 response 생성됨, 후처리 구간
        //doFilter가 일어난 후 request에 대한 정보를 찍어보자
        String reqContent = new String(httpServletRequest.getContentAsByteArray()); //컨텐츠의 내용을 ByteArray로 받을것임
        log.info("requset url : {}, request body : {}",url,reqContent);

        String resContent = new String(httpServletResponse.getContentAsByteArray()); //여기서 한번 읽으며, Body의 커서 끝까지 내려가서 내용이 없는 상태.
        int httpStatus=httpServletResponse.getStatus();
        //그래서 읽은 만큼 복사를 해주자
        httpServletResponse.copyBodyToResponse(); //복사를 함으로써 Body를 채워준다

        log.info("response status : {}, responseBody : {}",httpStatus,resContent);
    }
}

 

내가 만약에 Filter에서

request, response를 찍어야한다면

 

1.

ContentCachingRequestWrapper

클래스를 사용한다

 

2. 

chain.doFilter(httpServletRequest,httpServletResponse);

doFilter한다

 

3.

String reqContent = new String(httpServletRequest.getContentAsByteArray()); //컨텐츠의 내용을 ByteArray로 받을것임
log.info("requset url : {}, request body : {}",url,reqContent);

카피해서

로그로 찍는다

 

4.

httpServletResponse.copyBodyToResponse(); //복사를 함으로써 Body를 채워준다

복사를 해줘야 Client가 제대로 된 응답을 받을 수 있다.

 

 

Filter에서는

최전방에서 들어온 정보들을 볼 수 있으며

세션에 있는 내용을 읽을 수도 있다(세션 내용 읽고 로그아웃/404에러 내리기 등등..)

 

이러한 일들을 Filter에서 사용한다.

 

주로 로그를 남기는데 사용한다.


하나의 컨트롤러에만 Filter를 적용시키는 법

 

ApiUserController만들기

package com.example.filter.controller;

import com.example.filter.dto.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api/temp")
public class ApiUserController {

    @PostMapping("")
    public User user(@RequestBody User user){
        log.info("Temp : {}",user);
        return user;
    }
}

 

Filter 변경

@Slf4j
//@Component 삭제하고
@WebFilter(urlPatterns = "/api/user/*") //api/user/하위 모든 주소에 Filter를 매칭시키겠다는 뜻
public class GlobalFilter implements Filter {

 

 

http://localhost:8080/api/temp로 Post 요청을 보내면,

response는 잘 내려오나

requestBody에 대한 내용은 찍지 않는다 

'Spring' 카테고리의 다른 글

Server to server - GET  (0) 2022.06.26
Spring Boot - Interceptor  (0) 2022.06.20
Spring - DTO를 사용하는 이유  (0) 2022.06.16
Validation 모범 사례  (0) 2022.06.13
Exception 처리  (0) 2022.06.06
profile

Burninghering's Blog

@개발자 김혜린

안녕하세요! 반갑습니다.