- Filter란 Web Application에서 관리되는 영역으로써 Spring Boot Framework에서 Client로부터 오는 요청/응답에 대해서 최초/최종 단계의 위치에 존재하며, 이를 통해서 요청/응답의 정보를 변경하거나, Spring에 의해서 데이터가 변환되기 전의 순수한 Client의 요청/응답 값을 확인할 수 있다.
- 유일하게 ServletRequest,ServletResponse의 객체를 반환할 수 있다.
- 주로 Spring Framework에서는 request/response의 Logging 용도(백오피스, admin에서 모든 요청의 기록에 대해 AOP등에서는 객체로 매핑한 상태이기 때문에 순수한 상태가 아니다) 로 활용하거나 인증(interceptor를 타기전에 session의 유무를 보고 reject등을 발생시킬 수 있다) 과 관련된 Logic들을 해당 Filter에서 처리한다.
- 이를 선/후 처리함으로써 Service Business Logic과 분리시킨다.
-
Filter는 제일 앞단에 존재
-
AOP는 HandlerInterceptor 뒤에 Handler로 보내지는 타이밍에 동작하게 된다.
-
Fiter -> Interceptor -> AOP 순으로 진행
- 이번 실습에서는 lombok을 사용한다.
- lombok을 사용하면 getter,setter,toString,생성자 등 다양한 메소드에 대한 메소드 오버라이드를 지원하여 편리하게 활용 가능하다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- compileOnly : 컴파일 시에 같이 실행되는 형식으로 lombok을 사용한다. 프로그램이 직접 실행될 때 사용할 필요 없다.
- annotationProcessor : comile시 같이 동작하여 lombok을 사용할 수 있게 해준다.
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
}- @Getter : 멤버 변수에 대해 getter를 만들어준다
- @Setter : 멤버 변수에 대해 setter를 만들어준다
- @Data :getter,setter를 포함해서 equals,hashcode,toString 등 모두 관리해준다.
- @NoArgsConstructor : 기본 생성자 생성
- @AllArgsConstructor : 전체 Argument를 받는 생성자
- 지금까지 Systm.out.println 으로 로그를 남겼지만 Sprnig에서 logging을 할때 lombok을 사용하면 @Slf4j 를 추가할 수 있고 이를 통해 log를 사용할 수 있다.
@Slf4j
@RestController
@RequestMapping("/api/user")
public class ApiController {
@PostMapping("")
public User user(@RequestBody User user){
log.info("User : {}",user);
return user;
}
}- log.info("User : {}, {}",user,user); 처럼 사용할 때 {}의 내용은 뒤에 오는 user와 매칭된다.
- lombok을 사용한 User.java 와 ApiController를 함께 사용한다.
@Slf4j
@Component //Spring에 의해 Bean으로 관리
public class GlobalFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//전처리
HttpServletRequest httpServletRequest=(HttpServletRequest)request;
HttpServletResponse httpServletResponse=(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(httpServletRequest,httpServletResponse) 를 기준으로 전의 로직은 전처리, 후의 로직은 후처리를 담당한다.
{
"name":"steve",
"age":10
}2021-09-28 17:41:09.558 INFO 24872 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : url : /api/user , line : {
2021-09-28 17:41:09.560 INFO 24872 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : url : /api/user , line : "name":"steve",
2021-09-28 17:41:09.560 INFO 24872 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : url : /api/user , line : "age":10
2021-09-28 17:41:09.560 INFO 24872 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : url : /api/user , line : }
- url과 line이 정상 출력되는 것을 확인할 수 있다.
- 하지만 java.lang.IllegalStateException 예외가 발생한다.
- BufferedReader를 사용하면서 lambda 함수에서 Reqeueest로 온 내용을 cursor 단위로 모두 읽은 상태가 되었다.
- 전처리 후 메인 logic , 즉 Controller의 user() 메소드가 실행될 때 매개변수로 받는 RequestBody에서 모두 읽은 상태로 들어오기 때문에 더이상 읽을 수 없는 상태가 되었고 이로 인해 예외가 발생한 것이다.
- read로 한번 읽으면 더이상 읽을 수 없는 문제이기 때문에 이를 해결하기 위해 Readable, Cashable 를 상속받기도 하지만 Spring에서는 캐싱을 할 수 있는 ContentCachingRequestWrapper,ContentCachingResponseWrapper를 제공한다.
@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);
//기준
chain.doFilter(httpServletRequest,httpServletResponse);
//후처리
String url=httpServletRequest.getRequestURI();
String reqContent = new String(httpServletRequest.getContentAsByteArray());
log.info("reqeust");
String resContent=new String(httpServletResponse.getContentAsByteArray());
int httpStatus=httpServletResponse.getStatus();
log.info("response status : {} , responseBody : {}",httpStatus,reqContent);
}
}- ContentCachingRequestWrapper, ContentCachingResponseWrapper를 사용하여 길이는 캐시했지만 안의 내용은 복사해놓지 않은 상황이다. -> 후처리에서 처리해야 한다.
- ContentCachingRequestWrapper에 구현된 writeToCahce() 가 호출되어야 cashedContent에 담긴다.
- doFilter 이후에 호출되어야 내용을 읽을 수 있으므로 후처리에서 내용을 읽도록 로직을 수정한다.
2021-09-28 19:48:06.729 INFO 31708 --- [nio-8080-exec-1] c.e.filter.cotroller.ApiController : User : User(name=steve, age=10)
2021-09-28 19:48:06.756 INFO 31708 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : reqeust
2021-09-28 19:48:06.756 INFO 31708 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : response status : 200 , responseBody : {
"name":"steve",
"age":10
- 에러 없이 정상적으로 출력된다.
- 이 또한 ResponseBody를 이미 읽었으므로 Client에서 보여지지 않는다.
- ResponseBody를 복제하여 보여지도록 수정한다.
httpServletResponse.copyBodyToResponse(); - Filter에서 Request와 Response를 읽고자 한다면 ContentCachingRequestWrapper, ContentCachingResponseWrapper class를 사용하고 doFilter 후에 Response 를 copyBodyToResponse()메소드로 복사해야 Client에서 정상적으로 응답을 받을 수 있다.
- 특정 Class, 특정 Conotroller에 적용시키고 싶을때 urlPattern을 지정하여 사용한다.
@Slf4j
@RestController
@RequestMapping("/api/temp")
public class ApiUserController {
@PostMapping("")
public User user(@RequestBody User user){
log.info("Temp : {}",user);
return user;
}
}@Slf4j
@WebFilter(urlPatterns = "/api/user/*")
public class GlobalFilter implements Filter {
//생략
}- @Component Annotation에서 WebFilter Annotation으로 변경
- @WebFilter의 urlPatterns에 /api/user/* 를 설정하여 api/user 하위 모든 요청에 Filter 적용
@SpringBootApplication
@ServletComponentScan
public class FilterApplication {
}- @ServletComponentScan Annotation 추가
021-09-28 22:46:03.817 INFO 14728 --- [nio-8080-exec-1] c.e.filter.cotroller.ApiController : User : User(name=steve, age=10)
2021-09-28 22:46:03.848 INFO 14728 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : reqeust
2021-09-28 22:46:03.850 INFO 14728 --- [nio-8080-exec-1] com.example.filter.filter.GlobalFilter : response status : 200 , responseBody : {
"name":"steve",
"age":10
}
2021-09-28 22:46:11.167 INFO 14728 --- [nio-8080-exec-2] c.e.filter.cotroller.ApiUserController : Temp : User(name=steve, age=10)