[Spring] 서블릿에서 헤더값을 어떻게 설정할까 ? ?

노유성·2024년 1월 16일
0
post-thumbnail

들어가며

Spring MVC 강의를 돌아보던 중에 헤더에 값을 설정하고 싶으면

@WebServlet(name = "helloServlet", urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setHeader("my-header", "hello");
    }
}

위처럼 setHeaer()를 이용하라는 것을 보았다. 그래서 나는 아무런 의심없이 'Map 형태로 되어있는 필드에 저장하겠거니...' 하고 들어갔다.

하지만 HttpServletResponse은 인터페이스이다. 그래서 실제로 어떻게 동작을 하는지 알기위해서는 구현체를 찾아봐야한다.

찾아보니 HttpServletResponse의 구현체를 Spring에서는 ResponseFacade를 이용한다고 한다.

지금부터 어떤 방식으로 setHeader()를 호출하면은 헤더값이 저장이 되는지 알아보자.

ResponseFacade

    @Override
    public void setHeader(String name, String value) {
        if (isCommitted()) { // 여기
            return;
        }
        response.setHeader(name, value);
    }

먼저 우리가 setHeader()를 호출하면은 isCommitted()가 호출된다.

isCommitted()

    @Override
    public boolean isCommitted() {
        checkFacade(); // 여기
        return response.isAppCommitted();
    }

그 안에서는 또 checkFacade()를 호출한다.

checkFacade()

    private void checkFacade() {
        if (response == null) {
            throw new IllegalStateException(sm.getString("responseFacade.nullResponse"));
        }
    }

해당 메소드는 response라는 필드가 null인지 체크하는 메소드였다.

ResponseFacade.response

 /**
* The wrapped response.
*/
protected Response response = null;

음~ response 필드는 Response 객체였다는 사실을 알게되었다. 아무튼 다시 돌아가서

    @Override
    public boolean isCommitted() {
        checkFacade(); 
        return response.isAppCommitted();// 여기
    }

isCommitted()에서는 isAppCommitted()가 호출된다.

Response.isAppCommitted()

    /**
     * Application commit flag accessor.
     *
     * @return <code>true</code> if the application has committed the response
     */
    public boolean isAppCommitted() {
        return this.appCommitted || isCommitted() || isSuspended() ||
                ((getContentLength() > 0) && (getContentWritten() >= getContentLength()));
    }

음..... 그만 들어가보자. 아무튼 어플리케이션이 잘 커밋이 되었는지를 확인하는 거 아닐까 ? ?

다시 처음으로 돌아와서

    @Override
    public void setHeader(String name, String value) {
        if (isCommitted()) { // 여기
            return;
        }
        response.setHeader(name, value);
    }

아무튼 처음의 isCommited()가 호출이 되고 if문을 타지 않으면은 이제서야 response.setHeader()가 호출된다.

connector.Response

setHeader()

    @Override
    public void setHeader(String name, String value) {

        if (name == null || name.length() == 0 || value == null) {
            return;
        }
        if (isCommitted()) {
            return;
        }
        // Ignore any call from an included servlet
        if (included) {
            return;
        }
        char cc = name.charAt(0);
        if (cc == 'C' || cc == 'c') {
            if (checkSpecialHeader(name, value)) {
                return;
            }
        }
        getCoyoteResponse().setHeader(name, value);
    }

파라미터의 name은 헤더값이다. 그 값이 null이거나, 공백 문자 혹은 value가 null이면은 종료한다.

isCommitted()는... 아까 살펴본 바랑 비슷하다. 이후에 coyote 패키지의 Response 객체도 살펴볼텐데, 해당 객체의 필드 중 boolean 필드값을 보는 것이다.

if(included)도 마찬가지로 필드값을 본다. 해당 필드는 접근제어자가 protected이고 초기값이 false인데 getter도 없고, 클래스 내에서는 해당 필드를 true로 만드는 코드가 없어서 추적은 포기했다 ㅠㅠ

자자 그 다음! 그 다음에는 헤더 값의 첫글자가 C로 시작하는 지를 살핀다. 그리고 checkSpecialHeader()를 호출하는데...

checkSpecialHeader()

    private boolean checkSpecialHeader(String name, String value) {
        if (name.equalsIgnoreCase("Content-Type")) {
            setContentType(value);
            return true;
        }
        return false;
    }

!! 만약에 사용자가 setHeader()를 이용해서 Content-type의 필드를 변경하고 싶었다면은 setContentType()을 호출해서 위 메소드 내에서 처리하고 종료하는 것이다!

다시 setHeader()

그 다음엔 getCoyoteResponse()를 호출하는데...

getCoyoteResponse()

    public org.apache.coyote.Response getCoyoteResponse() {
        return this.coyoteResponse;
    }

오호라 coyote 패키지의 Response 객체를 반환하는 메소드이다. 그리고 해당 메소드의 setHeader()를 호출하는 것이었다!

coyote.Response

setHeader()

아직도 setHeader()라니....

    public void setHeader(String name, String value) {
        char cc = name.charAt(0);
        if (cc == 'C' || cc == 'c') {
            if (checkSpecialHeader(name, value)) {
                return;
            }
        }
        headers.setValue(name).setString(value);
    }

위에서 봤던 비슷한 코드가 보인다..? checkSpecialHeader()가 등장했다. 그리고 첫 글자가 c인지 확인을 한다.

checkSpecialHeader()

    private boolean checkSpecialHeader(String name, String value) {
        // XXX Eliminate redundant fields !!!
        // ( both header and in special fields )
        if (name.equalsIgnoreCase("Content-Type")) {
            setContentType(value);
            return true;
        }
        if (name.equalsIgnoreCase("Content-Length")) {
            try {
                long cL = Long.parseLong(value);
                setContentLength(cL);
                return true;
            } catch (NumberFormatException ex) {
                // Do nothing - the spec doesn't have any "throws"
                // and the user might know what they're doing
                return false;
            }
        }
        return false;
    }

여기서도 사용자가 설정하고자 하는 헤더값이 Content-type, Content-Length인지 확인하고 있다!

다시 setHeader()

그러고 나서는 headers 필드에 setValue()로 파라미터로 받은 name을 넘기고 있는데... headers는 뭘까?

MimeHeaders

final MimeHeaders headers = new MimeHeaders();

오호라 새로 보이는 클래스이다?

MimeHeaders

여기서부터는 조금 헷갈릴 수도 있다! 천천히 알아보자.
먼저 이전에 coyote.Respnose에서

 headers.setValue(name).setString(value);

이 부분에서 여기까지 넘어온건데 그 전에 MimeHeaders의 필드 중

public static final int DEFAULT_HEADER_SIZE = 8;
private MimeHeaderField[] headers = new MimeHeaderField[DEFAULT_HEADER_SIZE];

요런 필드가 있다?

MimeHeaderField

class MimeHeaderField {

    private final MessageBytes nameB = MessageBytes.newInstance();
    private final MessageBytes valueB = MessageBytes.newInstance();

    /**
     * Creates a new, uninitialized header field.
     */
    MimeHeaderField() {
        // NO-OP
    }

    public void recycle() {
        nameB.recycle();
        valueB.recycle();
    }

    public MessageBytes getName() {
        return nameB;
    }

    public MessageBytes getValue() {
        return valueB;
    }

    @Override
    public String toString() {
        return nameB + ": " + valueB;
    }
}

이렇게 생겼고... 오케이 여기까지. 다시 돌아가보자.

다시 돌아가서

headers.setValue(name).setString(value);

여기서 setValue()를 호출하면?

setValue()

    public MessageBytes setValue(String name) {
        for (int i = 0; i < count; i++) {
            if (headers[i].getName().equalsIgnoreCase(name)) {
                for (int j = i + 1; j < count; j++) {
                    if (headers[j].getName().equalsIgnoreCase(name)) {
                        removeHeader(j--);
                    }
                }
                return headers[i].getValue();
            }
        }
        MimeHeaderField mh = createHeader();
        mh.getName().setString(name);
        return mh.getValue();
    }

무지하게 복잡한 코드가 있다.. 일단 for문은 잠깐 넘기고
createHeader()에 대해서 알아볼까?

createHeader()

    private MimeHeaderField createHeader() {
        if (limit > -1 && count >= limit) {
            throw new IllegalStateException(sm.getString("headers.maxCountFail", Integer.valueOf(limit)));
        }
        MimeHeaderField mh;
        int len = headers.length;
        if (count >= len) {
            // expand header list array
            int newLength = count * 2;
            if (limit > 0 && newLength > limit) {
                newLength = limit;
            }
            MimeHeaderField tmp[] = new MimeHeaderField[newLength];
            System.arraycopy(headers, 0, tmp, 0, len);
            headers = tmp;
        }
        if ((mh = headers[count]) == null) {
            headers[count] = mh = new MimeHeaderField();
        }
        count++;
        return mh;
    }

음... 뭔가 엄청나게 복잡해보이는데 간단하게 요약하면 이렇다.

  1. count: 현재 MimeHeaderField.headers에 몇 개의 값이 들어가있는지 알려주는 변수
  2. len: MimeHeaderField.headers는 정적 array 필드인데 몇 개인지

이 정도만 알고 위 글을 보면은 처음에 8개만 생성했던 headers 배열을 늘리는 메소드 인 것 같다. newLength라는 새로운 변수에 기존보다 더 큰(기존에는 8개) 수를 담고 tmp[]라는 새로운 MimeHeaderField 배열을 만들고 있다.

그리고 System.arrayCopy()를 이용해서 headers에 들어있는 값을 tmp로 복사하구, headers에는 tmp[]를 넣는다!

그리고 새로운 MimeHeaderField 객체를 만들어서 headers에 추가하고, 객체를 반환하고 있다!

음...추측하건데 MimeHeaderField가 우리가 처음에 RequestFacade에 저장하고자 했던 값들을 하나하나 저장해두는 객체인 것 같다!

다시 setValue()

```java
    public MessageBytes setValue(String name) {
        for (int i = 0; i < count; i++) {
            if (headers[i].getName().equalsIgnoreCase(name)) {
                for (int j = i + 1; j < count; j++) {
                    if (headers[j].getName().equalsIgnoreCase(name)) {
                        removeHeader(j--);
                    }
                }
                return headers[i].getValue();
            }
        }
        MimeHeaderField mh = createHeader();
        mh.getName().setString(name);
        return mh.getValue();
    }

그러면 예측하건데 createHeader() 이전에는 'headers에 빈자리가 있나...'하고 찾아보는 과정같다!

그 다음에는 mh.getName().setString()을 살펴보았다.

MimeHeaderField

getName()

   private final MessageBytes nameB = MessageBytes.newInstance();
   private final MessageBytes valueB = MessageBytes.newInstance();
    
   public MessageBytes getName() {
        return nameB;
    }

해당 메소드는 MimeHeaderField의 필드 중 nameB를 반환하는 함수이다!

setString은??

그러면 mh.getName().setStringMessageBytes라는 객체의 메소드이다. 한번 들어가보자.

MessageBytes

setString

    private String strValue;
    private boolean hasHashCode = false;
    private boolean hasLongValue = false;

    public void setString(String s) {
        strValue = s;
        hasHashCode = false;
        hasLongValue = false;
        if (s == null) {
            type = T_NULL;
        } else {
            type = T_STR;
        }
    }

!!!!!! 더이상 스코프가 들어가지 않는다. 여기가 종점인가보다.

참고로 type 변수는

    private int type = T_NULL;

    public static final int T_NULL = 0;
    /**
     * getType() is T_STR if the the object used to create the MessageBytes was a String.
     */
    public static final int T_STR = 1;
    /**
     * getType() is T_BYTES if the the object used to create the MessageBytes was a byte[].
     */
    public static final int T_BYTES = 2;
    /**
     * getType() is T_CHARS if the the object used to create the MessageBytes was a char[].
     */
    public static final int T_CHARS = 3;

위 필드들을 참고하면 되는데, 현재 MessageBytes 객체에 들어있는 값의 타입이 무엇인지를 갖고 있는 상태 필드이다!

무튼 마지막으로 setString()을 호출하면 strValue에 우리가 맨 처음에 기입하고자 했던 헤더명을 저장하는 것을 알 수 있다!

추가

아까 coyote.Response 객체에서

    public void setHeader(String name, String value) {
        char cc = name.charAt(0);
        if (cc == 'C' || cc == 'c') {
            if (checkSpecialHeader(name, value)) {
                return;
            }
        }
        headers.setValue(name).setString(value); // 요기
    }

마지막 문장에서 setValue()를 했을 때 호출된 과정을 방금 살펴봤다! 그리고 해당 과정에서 헤더명이 저장된다는 것을 확인했다. 그리고 setValue()의 반환값은
아까 살펴본 MimeHeaders.setValue()를 확인하면은,

MimeHeaders.setValue()

    public MessageBytes setValue(String name) {
        for (int i = 0; i < count; i++) {
            if (headers[i].getName().equalsIgnoreCase(name)) {
                for (int j = i + 1; j < count; j++) {
                    if (headers[j].getName().equalsIgnoreCase(name)) {
                        removeHeader(j--);
                    }
                }
                return headers[i].getValue();
            }
        }
        MimeHeaderField mh = createHeader();
        mh.getName().setString(name);
        return mh.getValue();
    }

MimeHeaderField.getValue()를 반환받는 것을 확인할 수 있는데 그 값은

    private final MessageBytes nameB = MessageBytes.newInstance();
    private final MessageBytes valueB = MessageBytes.newInstance();

    public MessageBytes getValue() {
        return valueB;
    }

아까 사용하지 않았던 valueB 변수이다! 아까 살펴본 MessageBytes.setValue()를 메소드 체인으로 호출해서 헤더명에 더하여, 헤더값을 저장하는 것이었다!

진짜 마지막으로

MimeHeaders.setValue()headers필드에 중복된 값이 존재하는지, 혹은 아예 자리가 없는지를 파악하는 validation이었다! 이거까지 적기에는 귀찮아서 헤헤

정리하며

response.setHeader() 하나에 이렇게나 복잡하게(전부 알아본 것도 아님) 구성되어있는지는 꿈에도 몰랐다... 처음에 Map으로 되었겠거니 했던 안일한 내 생각을 반성하게 되었다 ㅠㅠ 앞으로는 프레임워크를 쓸 때 감사하는 마음을 갖고 써야겠다!

정리되지 않은 긴 글 읽어주셔서 감사합니다~~~

profile
풀스택개발자가되고싶습니다:)

0개의 댓글