Post

경험으로 알아보는 Spring Filter를 사용하는 이유

🔅 문제상황

안드로이드 개발자와 얘기를 하다가 response데이터들을 하나의 response객체에 담아서 보내기로 하였다.

기존에는

1
2
3
4
5
6
7
{
    "questionBoardDto": {
        ...
        필드값들
        ...
    }
}

이런식으로 값을 줬다면

1
2
3
4
5
6
7
{
    "response" :{
        ...
        필드값들
        ...
    }
}

즉, 모든 dto를 response라는 객체로 바꾸고 그 안에 값을 담아서 보내야했다.

모든 DTO에 어노테이션을 달아서 해결할 수는 있지만, 이미 사이즈가 어느정도 커졌고 좀 더 편한 방법이 없을까 했다.

🤔 Interceptor vs Filter

인터셉터로 구현할까 해봤다.

하지만 필터와 달리 인터셉터는 requestresponse 조작이 불가능하다.

필터의 동작방식은 모두가 알테니 설명하지는 않겠다. 컨트롤러에 요청이 도달하기 전, 도달하고 클라이언트에게 응답을 보낼때의 request, response 조작이 가능하다.

나는 클라이언트에게 모든 요청에 대해서 response body에 담겨진 Json값을 response라는 객체에 담아서 보내야했기에 response 조작이 필요했다.

그래서 하나의 필터를 만들었다.

참고글은 이글이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Slf4j
public class JsonFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //전처리
        ContentCachingResponseWrapper responseWrapper =
                new ContentCachingResponseWrapper((HttpServletResponse) response);

        filterChain.doFilter(request, responseWrapper);

        //후처리

        responseWrapper.setCharacterEncoding("UTF-8");
        byte[] responseArray = responseWrapper.getContentAsByteArray();
        String responseStr = new String(responseArray, responseWrapper.getCharacterEncoding());

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(responseStr);
        ObjectNode dataNode = objectMapper.createObjectNode();
        dataNode.set("response", jsonNode);

        String modifiedJson = objectMapper.writeValueAsString(dataNode);
        //한글이 포함되어 있어서 사이즈를 재계산해줘야함.
        byte[] utf8Bytes = modifiedJson.getBytes(StandardCharsets.UTF_8);
        int length = utf8Bytes.length;

        response.setContentType(responseWrapper.getContentType());
        response.setContentLength(length);
        response.getOutputStream().write(modifiedJson.getBytes());
    }

}

지금은 결과물이지만 조금의 고생이 있었다.

  1. response에 한글이 내려올 경우 깨지는 경우가 있어서 UTF-8로 변환후 값을 재구성해줘야했다. 이는 그렇게 어렵지 않았다.
  2. swagger-ui와 같이 쓸 경우

🤔 Swagger-ui와 같이 쓸 경우

이게 왜 문제일까?

저 위의 코드는 무조건 JSON값이 들어온다고 가정하고 값을 파싱해서 response객체에 담아서 보내고 있다.

Swagger에 접근할 경우 응답으로 줘야할건 JSON이 아니라 Html이다.

그래서 JSON파싱을 못하기에 에러가 발생했다.

자, 그럼 WebConfig에서 swagger에 대한 url을 무시하면 되지 않느냐?가 생각났다.

😕 되지 않는 무시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/swagger-ui/**", "/v3/**").permitAll()
                        .requestMatchers("/api/users/signup", "/api/users/verification").permitAll()
                        .anyRequest().authenticated())
                .requestCache().disable()
                .securityContext().disable()
                .sessionManagement().disable()
                .headers().frameOptions().disable()
                .and()
                .formLogin()
                .disable()
                .exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                .exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint)
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(jsonFilter, FilterSecurityInterceptor.class);

이런식으로 구성했었다.

보면은 requestMatcher에서 스웨거에 대한 url은 모두 요청을 허용하도록 하였다. 즉, 내 의도는 jsonFilter를 거치지 않아서 response를 그대로 다음 Filter에 넘겨주길 바랬다.

하지만..계속 필터를 탔다.

왜 그럴까? 왜 그럴까? 계속 생각하다가 구글링 하면서 어느정도 해답을 알았다.

👍 해결방법

이 글을 보면서 어느정도 해소가 되었다.

즉, 위의 코드에서 JsonFilter는 컴포넌트 스캔으로 스프링 컨택스트에서 관리하는데, 스프링 시큐리티에서 아는 Filter와 다른 Filter로 스프링 시큐리티에서 해당 필터를 무시하라고 해도 무시가 되지 않았던 것이다.

그래서 addFilterBefore메서드가 호출될 때 그냥 호출되어버리는 것이다.

이를 new연산자를 통해서 객체로 직접 넘기면 해결이 된다.

1
2
3
...
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JsonFilter(), FilterSecurityInterceptor.class);

이런식으로 하면 Spring 컨텍스트와 관계가 없는 객체가 되기 때문에 Spring Security가 해당 객체를 필터로 간주하고 필터체인에 추가하게 되는 것이다.

그리고 혹시 몰라서 설정값을 바꿔줬다.

1
2
3
4
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return (web) -> web.ignoring().requestMatchers("/swagger-ui/**", "/v3/**");
}

이런식으로 아예 시큐리티 필터체인을 거치지 않도록 설정하였다.

✏️ 결론

스프링에는 여러가지 필터가 있고, 설정을 어떻게 하느냐에 따라 동작방식이 달라진다.

나의 문제는 어떻게보면 당연히 빈으로 관리하면 편할거라 생각하고 필터조차 @Component로 달아서 관리하도록 하였는데, 내 생각과는 다르게 동작이 되어서 문제가 발생했던 것 같다.

그리고 필터에 대한 이해도도 좀 더 높아야겠다고 생각하였고, 직접 HttpServletResponse객체를 조작하면서 필터를 왜 사용하는가에 대해 알 수 있었다.

This post is licensed under CC BY 4.0 by the author.