티스토리 뷰

나중에 사용할 HttpServletRequestWrapper 클래스

 

문제의 발생

 

새로운 프로젝트를 시작하면서, 로그를 다시 붙여야할 일이 생겼다.

 

기존 프로젝트에 로그를 개발하면서 했던 파이프라인을 그대로 가져와서 붙여넣었는데,

 

POST 요청에서 이상하게 동작하지 않았다.

 

원인을 분석하면서 생긴 일을 정리해보려고 한다.

 

우선 현재 시스템에서 로그를 어떻게 찍는지 간단히 정리해보고 가자.

 

1. Controller에서 API 요청을 받아서 서비스로직까지 처리한 후 return
2. 이 return 할 때 AOP AfterReturning을 통해 캐치
3. 이때 발생하는 return object와 요청 정보를 담고 있는 HttpServletRequest을 통해서 로그를 생성

 

이 과정 중 3번에서 문제가 발생했다.

 

사실 스프링부트에서 POST 요청의 RequestBody를 가져오는 과정에 대해서 잘 알고 있었다면,

 

원인을 빠르게 알아차릴 수 있었던 문제였지만, 이때만해도 무슨 문제인지 알 수가 없었다.

 

원인 분석 - 1. 어디서 문제가 발생했을까?

 

에러를 발생시켰던 위치부터 차근차근 알아보자.

 

기존에 사용하던 로그 매니저 코드를 그대로가져왔는데

 

RequestBody를 가져오는 매서드에서 getReader를 찾을 수 없다는 에러를 발생했다.

public String getRequestBody() {
    StringBuilder stringBuilder = new StringBuilder();
    BufferedReader reader;
    try {
        reader = httpServletRequest.getReader();
        String line;
        while ((line = reader.readLine()) != null) {
            stringBuilder.append(line);
        }
    } catch (IOException e) {
        return StringUtils.EMPTY;
    }

    return stringBuilder.toString();
}

가장 먼저 든 생각은 HttpServletRequest가 javax에서 jakarta로 버전업되면서 getReader 매서드가 변경되었나? 라는 생각을 했다.(기존은 자바 1.8을 쓰지만 여기선 17로 버전업 했음)

 

그래서 다른 방식으로 RequestBody를 가져와봤다.

private final HttpServletRequest httpServletRequest;

String jsonParameter = getRequestBodyFromInputStream(httpServletRequest.getInputStream());

public String getRequestBodyFromInputStream(InputStream inputStream) throws IOException {
	StringBuilder stringBuilder = new StringBuilder();
	BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
	String line;
	while ((line = reader.readLine()) != null) {
		stringBuilder.append(line);
	}
	return stringBuilder.toString();
}

가장 일반적인 방식인 HttpServletRequest이 들고있는 InputStream에서 가져오기.

 

그런데 HttpServletRequest에서 스트림을 가져와 readLine으로 읽어왔을 때 값이 항상 null이었다.

 

여기서부터는 원인을 알아내는데 시간이 좀 걸렸는데, 이유는 단순했다.

 

로그 시스템이 "@RequestBody로 요청 전문을 한번 가져와 서비스로직을 처리한 후" 동작해서 그런거였다. 

 

원인 분석 2. @RequestBody

@RequestBody 어노테이션이 동작하는 원리에 대해 알아야한다.

@PostMapping("/requestBody")
public String requestBodyToJson(HttpServletRequest request, HttpServletResponse response){
    try {
        ServletInputStream inputStream = request.getInputStream();
        return StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
    } catch (IOException e) {
        return null;
    }
}

@RequestBody 어노테이션을 안썼을 경우, 위의 코드처럼

 

HttpServletRequest의 ServletInputStream을 통해 RequestBody를 String형태로 읽어와야한다.

 

여기서 문제가 발생하는데

 

한번이라도 HttpServletRequest에서 Stream형태로 RequestBody에 접근해 데이터를 가져갈경우

 

외부에서는 HttpServletRequest 저장되어 있었던 RequestBody에 접근할 수 없게된다.

 

그렇다면, AOP나 interceptor, filter와 같은 공통처리부에서는 RequestBody에 접근할 수 없을까?

 

컨트롤러 외부에서도 다시 요청전문에 접근할 수 있도록 ReqeustWrapper를 재정의하는 방법을 통해 이 문제를 해결했다.

 

해결법 - RequestWrapper 추가하기

원인을 알았으니 해결법은 간단하다.

 

RequestBody를 한번 읽어가기 전에 Wrapper에서 들고 있게 처리하면되기 때문이다.

 

스프링에서는 HttpServletRequest 내부의 매서드를 개발자의 입맛에 맞게 재정의(오버라이드)해서 사용할 수 있도록HttpServletRequestWrapper라는 클래스를 제공한다.

 

HttpServletRequestWrapper를 이용해 공통처리부의 가장 앞단인

 

Filter에서 RequestWrapper를 정의해서 HttpServletRequest가 body를 항상 들고 있도록 구현했다.

 

Filter 클래스

@Component
public class RequestFilter implements Filter {
    private FilterConfig filterConfig = null;

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        request = new RequestWrapper(httpServletRequest);
        chain.doFilter(request, response);
    }

    public void init(FilterConfig filterConfiguration) {
        this.filterConfig = filterConfiguration;
    }

    public void destroy() {
        this.filterConfig = null;
    }

}

HttpServletRequestWrapper 클래스

@Getter
public class RequestWrapper extends HttpServletRequestWrapper {
    //Use this method to read the request body N times
    private final String body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);

        StringBuilder stringBuilder = new StringBuilder();
        try (BufferedReader bufferedReader = request.getReader()) {
            char[] charBuffer = new char[128];
            int bytesRead;
            while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                stringBuilder.append(charBuffer, 0, bytesRead);
            }
        } catch (IOException e) {
            throw new BadRequestException();
        }

        body = stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                throw new UnsupportedOperationException();
            }

            public int read() {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

}

위와 같이 구현하면, 첫 코드의 HttpServletRequest에서 RequestBody를 가져오는 getRequestBody 매서드가 정상동작하게 된다.

 

마치며

그런데 조금 더 생각해보면, 기존 시스템에서는 로그가 왜 정상적으로 동작했을까?

 

기존 코드에서는 RequestWrapper가 이미 구현되어 있었기 때문이었다...

 

사실 이 코드를 보긴했다.

 

그런데 이게 무슨 의미인지 전혀 알지 못했다. 실제로 코드 내에서도 사용하는 부분이 없었기 때문이다.

 

내가 로그를 구축하면서 이 코드를 처음으로 사용한 거였다.(그럼 왜 만들어놓으셨나요?)

 

Spring은 잊을만하면 알아야할게 생기는 것 같다...

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
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
글 보관함