Spring Filter에서 Response 수정하기

Jeongkuk Seo
sjk5766
Published in
18 min readMar 2, 2023

이전 글에서 Interceptor에서 어떻게 Response 데이터를 수정하는지 다뤘는데 결론은 리턴되는 데이터가 있다면 Interceptor에서 수정할 수 없다는 내용이었다. 따라서 내 관심사는 Filter로 옮겨졌고 여기서 어떻게 데이터를 수정하는지에 대해 정리하도록 하겠다.

우선 FilterRegistrationBean에 내가 작성한 Filter를 등록해보자.

@Configuration
public class FilterRegistry {
@Bean
public FilterRegistrationBean<Filter> registrationBean() {
FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CustomFilter());
return registrationBean;
}
}

아래는 내가 작성한 CustomFilter로 chain.doFilter 메소드를 기준으로 다음 Filter나 dispatcher servlet으로 넘기기 전/후 ServletResponse.isCommitted 메소드를 호출하는 걸 알 수 있다.

@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
System.out.println("=====request do Filter=====");
System.out.println("response.isCommitted():" + response.isCommitted());

chain.doFilter(request, response);

System.out.println("=====response do Filter=====");
System.out.println("response.isCommitted():" + response.isCommitted());
}
}

isCommitted 실행결과가 true 라면 그 시점에 response는 이미 output stream에 데이터가 쓰여지고 전송되었기 때문에 이때는 수정할 수 없다.

간단한 UserController를 만들고 값을 리턴하는 API를 만들자.

@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;

public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping()
public List<UserResponse> findAll() {
return userService.findAll();
}
}

Filter가 적용된 상태로 /users로 GET 요청을 보내보면 아래와 같이 출력이 남는다. request 영역에서는 아직 응답이 commit 되지 않았기 때문에 false 지만 response 영역에서는 true인 상태로 응답이 온다.

=====request do Filter=====
response.isCommitted():false

=====response do Filter=====
response.isCommitted():true

이전 포스팅이었던 Interceptor와 마찬가지로 리턴되는 데이터가 있으면 response 데이터를 변경할 수 없다. 그렇다면 어떻게 변경할 수 있을까?

스프링에는 ContentCachingResponseWrapper 라는 클래스가 있는데 이를 통해 Output Stream에 쓰여진 데이터를 캐시하고 Byte Array를 통해 이를 읽을 수 있는 방법을 제공한다.

따라서 내가 작성한 Filter를 아래와 같이 수정하자. 핵심은 Request 영역의 response를 ContentCachingResponseWrapper 클래스로 변경 후 이를 다음 chain.doFilter로 전달하는 것이다.

@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
System.out.println("=====request do Filter=====");
System.out.println("response.isCommitted():" + response.isCommitted());

ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper((HttpServletResponse) response);

chain.doFilter(request, responseWrapper); // responseWrapper를 넣는다.

System.out.println("=====response do Filter=====");
System.out.println("response.isCommitted():" + response.isCommitted());
System.out.println("responseWrapper.isCommitted():" + responseWrapper.isCommitted());
}
}

ContentCachingResponseWrapper 클래스는 내부적으로 상속 관계를 따라가다 보면 결국 ServletResponse 인터페이스를 구현하기 때문에 FilterChain에 넘겨줘도 정상적으로 동작한다.

이 때 /users API를 호출하면 출력되는 로그를 보면 아래와 같다.

=====request do Filter=====
response.isCommitted():false

=====response do Filter=====
response.isCommitted():false
responseWrapper.isCommitted():false

isCommitted 가 false 라는 의미는 아직 데이터가 Output Stream에 쓰여지지 않았기 때문에 이제 response 데이터를 수정할 수 있다.

3가지 정도의 Response 데이터를 조작하는 법을 정리하겠다.

  • Response 데이터 수정 없이 로깅만 하고 원본 데이터를 응답하는 경우
  • 기존 데이터를 무시하고 새로운 데이터를 응답하는 경우
  • 기존 데이터를 읽어 수정한 뒤 응답하는 경우

Response 데이터 수정 없이 로깅만 하고 원본 데이터를 응답하는 경우

ContentCachingResponseWrapper 가 제공하는 getContentAsByteArray 메소드로 byte 배열을 읽고 이를 String으로 변환 후에 로깅한다.

이 때 맨 마지막 line인 copyBodyToResponse 메소드는 캐시해 둔 원본 데이터를 다시 response에 저장하는 방법이다. ContentCachingResponseWrapper 가 response ouput stream에서 데이터를 읽는 시점에 stream은 빈 상태가 된다. 따라서 기존 응답을 그대로 전달하고 싶다면 copyBodyToResponse 메소드를 호출해야 한다.

@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper((HttpServletResponse) response);

chain.doFilter(request, responseWrapper);

byte[] responseArray = responseWrapper.getContentAsByteArray();
String responseStr = new String(responseArray, responseWrapper.getCharacterEncoding());
System.out.println(responseStr); // 여기서 로깅한다.
responseWrapper.copyBodyToResponse();
}

기존 데이터를 무시하고 새로운 데이터를 응답하는 경우

아래 Filter 에서는 ObjectMapper로 Json 객체를 생성하고 값을 넣어 응답하고 있다. 좋은 예시가 생각나지 않아 임의로 저렇게 코드를 작성했지만 핵심은 원하는 객체를 생성해서 아래와 같이 응답할 수 있다는 점이다.

@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper((HttpServletResponse) response);

chain.doFilter(request, responseWrapper);

ObjectNode json = new ObjectMapper().createObjectNode();
json.put("message", "this response is modified");

String newResponse = new ObjectMapper().writeValueAsString(json);
response.setContentType("application/json");
response.setContentLength(newResponse.length());
response.getOutputStream().write(newResponse.getBytes());
}
}

/users API를 조회하면 아래와 같은 응답을 받게 된다.

기존 데이터를 읽어 수정한 뒤 응답하는 경우

어떻게 response를 수정할 수 있을까? 라는 의문에서 출발했기 때문에 response를 수정해야 하는 좋은 예시는 찾지 못하겠다. 세계 평화를 위해
그냥 무작정 수정해야 한다고 가정한다면 Type이 있는 형태(response dto 같은)와 Type이 고정적이지 않은 형태(actuator의 /health 응답 같은)가 있겠다.

아래의 Filter는 응답 타입이 List<UserResponse> 일 때 어거지로 값을 수정하는 예제다.

@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper((HttpServletResponse) response);

chain.doFilter(request, responseWrapper);

if (httpRequest.getMethod().equals(HttpMethod.GET.name())) {
byte[] responseArray = responseWrapper.getContentAsByteArray();
String responseStr = new String(responseArray, responseWrapper.getCharacterEncoding());
List<UserResponse> userResponses
= Arrays.asList(new ObjectMapper().readValue(responseStr, UserResponse[].class));

// UserResponse에 setter가 있다고 가정한다면
userResponses.get(0).setId(9999L);

String newResponse = new ObjectMapper().writeValueAsString(userResponses);
response.setContentType("application/json");
response.setContentLength(newResponse.length());
response.getOutputStream().write(newResponse.getBytes());
}
}
}

이렇게 Filter를 작성하고 /users API를 호출하면 id가 수정된 것을 알 수 있다.

만약 응답이 List가 아닌 하나의 객체라면 아래와 같이 Filter를 작성할 수 있다.

@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper((HttpServletResponse) response);

chain.doFilter(request, responseWrapper);

if (httpRequest.getMethod().equals(HttpMethod.GET.name())) {
byte[] responseArray = responseWrapper.getContentAsByteArray();
String responseStr = new String(responseArray, responseWrapper.getCharacterEncoding());
UserResponse userResponse = new ObjectMapper().readValue(responseStr, UserResponse.class);

// UserResponse에 setter가 있다고 가정한다면
userResponse.setId(9999L);

String newResponse = new ObjectMapper().writeValueAsString(userResponse);
response.setContentType("application/json");
response.setContentLength(newResponse.length());
response.getOutputStream().write(newResponse.getBytes());
}
}
}

만약 정해진 타입이 없다면 어떻게 할 수 있을까? actuactor 의존성을 추가하면 자동으로 /actuator/health API가 동작하게 되는데 이때 응답은 아래와 같다.

이 데이터를 변경하고 싶다면 아래와 같은 Filter를 작성해 볼 수 있다.

@Component
public class CustomFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
ContentCachingResponseWrapper responseWrapper =
new ContentCachingResponseWrapper((HttpServletResponse) response);

chain.doFilter(request, responseWrapper);

byte[] responseArray = responseWrapper.getContentAsByteArray();
String responseStr = new String(responseArray, responseWrapper.getCharacterEncoding());

JsonNode node = new ObjectMapper().readTree(responseStr);
((ObjectNode)node).put("status", "DOWN");

String newResponse = new ObjectMapper().writeValueAsString(node);
response.setContentType("application/json");
response.setContentLength(newResponse.length());
response.getOutputStream().write(newResponse.getBytes());
}
}

필터 적용 후 health API를 다시 호출해 보면 값이 수정되었다.

정리

어떻게 response 데이터에 접근하거나 수정할 수 있을까? 라는 의문에서 여기까지 해 본 것 같다. 실무에서 이렇게 Response를 수정할 일은 없겠지만
혹시 나와 같은 궁금함을 가진 분에게 도움이 될까 해서 정리해봤다.

레퍼런스

--

--