

처음으로 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 |