ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ServletInputStream은 1번만 읽을 수 있다
    프레임워크/Spring Boot 2024. 5. 8. 23:36

     

    아래의 글은 다음과 같은 순서로 작성되었습니다.

    1. 예외의 발생

    2. 공식 문서에서 단서 찾기

    3. BufferedReader 동작 확인하기

    4. 그래도 2번 읽고 싶다면?

    5. 정리하기 

     

     

    1. 예외의 발생

    회원가입을 Spring Security의 필터 체인을 사용해서 구현 하고 있습니다.

     

    첫 번째 필터에서 input에 대한 validation 처리를 위해서 request에서 body 정보를 읽습니다.

     

    적절한 요청은 두 번째 필터로 전달을 하고, 회원가입 로직을 가지고 있는 서비스를 주입받아서 사용하려고 합니다.

    이 때, 필터 간의 데이터 전달을 위한 별도의 dto를 만들지 않기 위해서 기존의 리퀘스트에서 한번 더 바디를 읽으려고 합니다.

     

    하지만 기대와 다르게 예외가 발생합니다?

     

     

    2.  공식 문서에서 단서 찾기

    이 내용은 스프링 레벨에서 논하기 보다는 자바 레벨에 대한 이해가 우선되어야할 것 같습니다.

     

    우선 getInputStream의 문서를 살펴봅니다.

    body의 데이터를 ServletInputStream에서 가져온다고 합니다. getReader 라는 메소드로도 가능한 듯 보입니다.

     

     

    ServletInputStream문서로 이동해서 보니 readLine이라는 메소드가 보입니다.

    readLine은 BufferedReader에서 보던 메소드 입니다.

     

     

    3.  BufferedReader 동작 확인하기

    BufferedReader에 데이터를 담아두고 반복적으로 읽을 수 있는가를 확인해보겠습니다.

    샘플 텍스트를 준비합니다.

     

    그리고 다음과 같은 코드를 실행시켜 보겠습니다.

     

    로그를 확인해보면 첫 번째 loop에서는 문장을 로그로 잘 찍는데 두 번째 루프는 문장을 로그로 찍지 않습니다.

    첫 loop 이후에 Read Once: >> 부터는 ready()를 실행했을 때 false 반환합니다.

     

    위 예제에서는 파일을 가지고 테스트하였지만 //// 표시한 윗 부분을 네트워크 I/O 라고 생각한다면 동일한 동작임을 생각해볼 수 있습니다.

     

     

    4.  그래도 2번 읽고 싶다면?

    방법을 찾아보면 캐싱을 해두고 계속 불러다 쓰는 방향을 제시하고 있습니다. 직접 구현하는 방법이 있고 스프링에서 제공하는 클래스를 사용하는 방법 등이 있는 듯합니다. 직접 구현을 해보면 도움이 많이 될듯하나 저는 스프링에서 제공하는 Wrapper 클래스를 사용하겠습니다.

     

    먼저 간단한 위의 이슈를 도식화봅니다.

     

    개선 전

     

    캐싱 처리를 해서 개선하고자 하는 모델입니다.

     

    개선 후

     

     

    Filter0 단계에 캐싱을 위한 필터를 추가합니다.

    ContentCachingRequestWrapper를 사용해서 Request를 wrapping 해줍니다.

    이 상태에서는 아직 body의 내용이 캐싱되지 않은 상태입니다.

     

    두 번째 필터에서 가져온 스트림을 읽어서 IdPasswordSigninDto로 변환해줍니다.

    cachedRequest.getInpuStream은 stream을 생성할 뿐 data를 consume하지 않습니다.

    실제로 생성된 stream을 objectMapper.readValue 가 사용할 때 비로소 버퍼에 담긴 내용이 consume 됩니다.

    ( 실제로 1번 사용이 될 때 ContentCachingRequestWrapper에 캐싱이 됩니다. )

     

    이렇게 한번 사용이된 이후로는 getContentAsByteArray 메소드를 사용해서 계속해서 캐싱된 데이터를 사용할 수 있습니다.

     

     

    5. 정리하기

    길게 적어보았지만, 사실 기본적으로 Buffer에 든 데이터는 consume 되는 매커니즘이라는 것을 생각해보면 특별한 이야기는 아닙니다.

    굳이 캐싱 필터를 또 추가하는 것보다는 request를 한번 읽을 때 필요한 작업들을 모두 처리하는 것도 방법일 수 있겠습니다.

     

    끝으로 getInputStream과 getContentAsByteArray를 순서를 생각하고 써야하는 위험성이 있기때문에 다시 한번 랩핑하는 클래스를 만들어서 사용해보았습니다~

     

    ( 하나씩 만들다 보니 클래스가 계속 늘어나는데, 이게 맞나 싶기도 합니다 ㅎㅎ )

     

     

     

    감사합니다.

Designed by Tistory.