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 어노테이션을 사용하면 로그를 쉽게 남길 수 있다.

<code />
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; } }

<code />
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를 한 번 해버리면,

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

 

<java />
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를 전달하려고 했더니 내용이 없는 상태이다

 

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

<code />
ContentCachingRequestWrapper httpServletRequest = (ContentCachingRequestWrapper)request; ContentCachingResponseWrapper httpServletResponse = (ContentCachingResponseWrapper)response;

ContentCaching Wrapper!

 

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

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

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

 

 

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

<code />
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;
<code />
ByteArrayOutputStream

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

 

스프링이 원한다던지, 

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

 

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

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

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

 

<java />
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 생성됨, 후처리 구간 } }
<java />
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request); ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

ContentCaching Wrapper를 쓰기 위해 

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

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

 

<code />
ContentCachingRequestWrapper

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

<java />
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest)request); ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse)response);

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

 

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

 

그 이유는

<java />
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; } . . .

생성자에서

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

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

 

<code />
ByteArrayOutputStream

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

 

 

그래서 이걸 읽는 방법은, 

doFilter의 모든 후처리로 간다.

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

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

위 코드 삭제

 

아래와 같이 코드 작성

<code />
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가 비어있다

 

<java />
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.

<code />
ContentCachingRequestWrapper

클래스를 사용한다

 

2. 

<code />
chain.doFilter(httpServletRequest,httpServletResponse);

doFilter한다

 

3.

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

카피해서

로그로 찍는다

 

4.

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

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

 

 

Filter에서는

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

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

 

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

 

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


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

 

ApiUserController만들기

<code />
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 변경

<code />
@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

@개발자 김혜린

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