티스토리 뷰

고객사에서 웹로직에서 제우스로 전환하는데 아래와 같은 상황에 대한 버그 리포트가 있었다.

 

  1. filter에서 request.getParameter()를 호출. filter는 /* 로 매핑이 되어있어 무조건 타게 되어 있음.
  2. servlet에서는 request.getInputStream()을 호출. 

이런 상황에서 

request.getInputStream()에서 read()를 하는 경우 EOF가 떨어진다는 것이었다.

 

왜 이런 상황이 벌어졌는지 알려면 서블릿 스펙과 서블릿에서 요청을 읽어들이는 방법에 대해 알아야 한다.

 

크게 3가지 방법이 쓰인다.

 

  1. HttpServletRequest#getInputStream()
  2. HttpServletRequest#getReader()
  3. HttpServletRequest#getParameter() - "family"

family 라고 쓴 이유는 이후 설명에 나온다.

Reader와 InputStream은 자바에서 제공하는 인터페이스를 구현했다고 보면 된다.

getInputStream()의 예시를 들면 어플리케이션에서 사용하는 javax.servlet.ServletInputStream 클래스가 존재한다.

public abstract class ServletInputStream extends InputStream

어플리케이션에서는 다음과 같이 사용된다.

    protected void doPost(HttpServletRequest request,
            HttpServletResponse response)
            throws ServletException, IOException {
        ServletInputStream is = request.getInputStream();

 

디테일한 구현은 을 톰캣 같은 경우엔 CoyoteInputStream이란 구현체를 사용하고 있기 때문에,

is.getClass().getName();

의 값을 찍어보면 org.apache.catalina.connector.CoyoteInputStream 와 같이 나온다.(Tomcat 7.0.96 기준)

제우스의 경우에는 어떤 포트(webtob connector, http listener, ajp listener)로 들어왔느냐에 따라 값이 다르게 찍힌다.

 

Reader 또한 같은 방식을 사용한다.

 

스펙에서는 getReader와 getInputStream을 동시에 사용하는 것을 금지하고 있는데, 이는 둘을 동시에 사용했을 경우에 디자인적으로 올바르지 못하고, byte 단위로 읽어들이는 inputStream에서 일부만 바이트 데이터를 읽어들이는 경우, Reader에서 인코딩이 정상적으로 동작하는 것을 보장할 수 없기 때문으로 보인다.

 

이에 대한 사항은 자바독에서도 쉽게 확인 가능하다.

 

getReader()

 

ServletRequest (Java(TM) EE 7 Specification APIs)

Stores an attribute in this request. Attributes are reset between requests. This method is most often used in conjunction with RequestDispatcher. Attribute names should follow the same conventions as package names. Names beginning with java.*, javax.*, and

docs.oracle.com

getInputStream()

 

ServletRequest (Java(TM) EE 7 Specification APIs)

Stores an attribute in this request. Attributes are reset between requests. This method is most often used in conjunction with RequestDispatcher. Attribute names should follow the same conventions as package names. Names beginning with java.*, javax.*, and

docs.oracle.com

그렇다면 getParameter는 post랑 어떤 연관이 있는 것일까?

이에 대해서는 다음과 같이 나와 있다.

 

...더보기

Request parameters for the servlet are the strings sent by the client to a servlet container as part of its request. When the request is an HttpServletRequest object, and conditions set out in ”When Parameters Are Available” on page 24 are met, the container populates the parameters from the URI query string and POST-ed data. The parameters are stored as a set of name-value pairs. Multiple parameter values can exist for any given parameter name. The following methods of the ServletRequest interface are available to access parameters:

■ getParameter

■ getParameterNames

■ getParameterValues

■ getParameterMap

 

Servlet spec 3.1의 3장 Request의 1절 HTTP Protocol Parameters 에서 발췌함

 

위에서 언급된 4개의 메서드를 getParameter Family라고 부르는데, 뭘 호출하든 무조건 map을 생성해 map에 값을 넣게 되어 있다.

이 때 맵은 Map<String, String[]> 의 구조를 가지게 되는데, key-value 쌍에서 같은 key에 여러 value가 들어가는 경우에 배열 뒤에 계속 추가하는 형태로 되어 있다.

스펙을 보면 알겠지만, 이 맵에 넣을 수 있는 것은 http 요청 URI에 포함되어 있는 query String과 body에 들어가 있는 post data가 있다. post data에는 page24의 조건이 붙는데 이는 뒤에서 다시 살펴볼 것이다. 

 

일단 전자는 request URI을 보면 ? 뒤에 들어가 있는 부분으로,

?type=post&returnURL=%2Fmanage%2Fposts%2F

와 같은 형태로 구성되어 있다. 

 

후자는 post data의 body에 

type=post&returnURL=%2Fmanage%2Fposts%2F

와 같이 들어가게 된다. 

둘다 형태는 동일한데, URI에 들어가느냐, body에 들어가느냐의 차이가 있다.

html 페이지에서는 

 

  <form action="/asdf" method="post" >
    <input name="say" value="Hi">
    <input name="to" value="Mom">
    <button>Send my greetings</button>
  </form>

와 같은 방법으로 HTTP Post 요청을 해당 형태로 보낼 수 있게 되어 있다.

이와 같은 경우, http request body에 

say=Hi&to=Mom

위와 같이 들어가서 요청이 전송되는 것을 볼 수 있다.

 

post body의 데이터는 그냥 parameter 맵에 넣어주지 않는다.

이부분은 위에서 언급한 page 24의 조건을 살펴봐야 한다.

 

...더보기

3.1.1 When Parameters Are Available The following are the conditions that must be met before post form data will be populated to the parameter set:

  1. The request is an HTTP or HTTPS request.
  2. The HTTP method is POST.
  3. The content type is application/x-www-form-urlencoded.
  4. The servlet has made an initial call of any of the getParameter family of methods on the request object.

If the conditions are not met and the post form data is not included in the parameter set, the post data must still be available to the servlet via the request object’s input stream. If the conditions are met, post form data will no longer be available for reading directly from the request object’s input stream.

 

즉, HTTP 요청이여야 하고, POST method를 써야 하며, content-type 또한 지정된 content-type을 써야 한다.

그리고 맵은 처음에 getParameter family 중에 하나가 처음 호출되었을 때에만 만든다. 

 

조건이 충족되지 않으면 이 메서드는 아무 동작을 하지 않는데, 조건을 충족하면 맵을 만들게 된다.

이때 inputStream을 재사용 할 수 없게 된다.

맵을 만드는 과정에서 InputStream을 통해 데이터를 가져와야 하기 때문이다.

 

만약에 filter에서 getParameter를 호출하고, servlet에서 getInputStream을 호출하는 경우, 위 조건을 만족시킨다는 가정 하에, getInputStream에서 더이상 읽을 수 있는 값은 없게 되는 것이다.

 

물론 웹로직에서는 그런 것 없이 재사용을 가능하게 했는데추정하기론 아래와 같은 방법을 쓰지 않았을까 추정한다.

  1. getParameter family가 호출된다
  2. 파싱을 위해 InputStream에서 읽은 데이터를 byte array에 저장한다.
  3. Map을 생성한다.
  4. getParameter를 통해 맵을 생성했다는 것을 flag 등을 사용해 표시한다.
  5. getInputStream이나 getReader()를 호출하는 경우, flag에 표시가 되어 있으면 ByteArrayInputStream을 이용해 저장한 byte array를 제공한다.

톰캣에서는 스펙대로 동작한다. 다만, 구체적인 구현은 제우스랑 다르다. getParameter를 호출한 이후에 read를 하는 경우에 아무것도 byte 배열에 채워주지 않고, 응답을 내보내는 방식으로 동작한다. 응답을 0x00 0x00 0x00 이런 식으로 내보낸다.

제우스의 경우에는 IOException을 던지도록 구현이 되어 있다. InputStream 인터페이스의 read 메서드 자체가 IOException을 던지도록 강제하고 있는데, 이를 활용한 것으로 보인다.

 

스펙은 분명히 오라클에서 만들었는데(앞으로는 아닐거지만) 스펙을 마음대로 위반하고 있다. 물론 사용자 편의에는 좋겠지만, 같은 데이터를 맵에도 저장하고, byte 배열에도 저장하는 비효율을 발생시키고, J2EE 어플리케이션의 호환성을 개박살 내고 있다는 점에선 부정적이라고 할 수 있다.

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