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()
를 호출하면은 헤더값이 저장이 되는지 알아보자.
@Override
public void setHeader(String name, String value) {
if (isCommitted()) { // 여기
return;
}
response.setHeader(name, value);
}
먼저 우리가 setHeader()
를 호출하면은 isCommitted()
가 호출된다.
@Override
public boolean isCommitted() {
checkFacade(); // 여기
return response.isAppCommitted();
}
그 안에서는 또 checkFacade()
를 호출한다.
private void checkFacade() {
if (response == null) {
throw new IllegalStateException(sm.getString("responseFacade.nullResponse"));
}
}
해당 메소드는 response
라는 필드가 null
인지 체크하는 메소드였다.
/**
* The wrapped response.
*/
protected Response response = null;
음~ response
필드는 Response
객체였다는 사실을 알게되었다. 아무튼 다시 돌아가서
@Override
public boolean isCommitted() {
checkFacade();
return response.isAppCommitted();// 여기
}
isCommitted()
에서는 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()
가 호출된다.
@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()
를 호출하는데...
private boolean checkSpecialHeader(String name, String value) {
if (name.equalsIgnoreCase("Content-Type")) {
setContentType(value);
return true;
}
return false;
}
!! 만약에 사용자가 setHeader()
를 이용해서 Content-type
의 필드를 변경하고 싶었다면은 setContentType()
을 호출해서 위 메소드 내에서 처리하고 종료하는 것이다!
그 다음엔 getCoyoteResponse()
를 호출하는데...
public org.apache.coyote.Response getCoyoteResponse() {
return this.coyoteResponse;
}
오호라 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
인지 확인을 한다.
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
인지 확인하고 있다!
그러고 나서는 headers
필드에 setValue()
로 파라미터로 받은 name
을 넘기고 있는데... headers
는 뭘까?
final MimeHeaders headers = new 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()
를 호출하면?
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()
에 대해서 알아볼까?
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;
}
음... 뭔가 엄청나게 복잡해보이는데 간단하게 요약하면 이렇다.
count
: 현재 MimeHeaderField.headers
에 몇 개의 값이 들어가있는지 알려주는 변수len
: MimeHeaderField.headers
는 정적 array
필드인데 몇 개인지이 정도만 알고 위 글을 보면은 처음에 8개만 생성했던 headers
배열을 늘리는 메소드 인 것 같다. newLength
라는 새로운 변수에 기존보다 더 큰(기존에는 8개) 수를 담고 tmp[]
라는 새로운 MimeHeaderField
배열을 만들고 있다.
그리고 System.arrayCopy()
를 이용해서 headers
에 들어있는 값을 tmp
로 복사하구, headers
에는 tmp[]
를 넣는다!
그리고 새로운 MimeHeaderField
객체를 만들어서 headers
에 추가하고, 객체를 반환하고 있다!
음...추측하건데 MimeHeaderField
가 우리가 처음에 RequestFacade
에 저장하고자 했던 값들을 하나하나 저장해두는 객체인 것 같다!
```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()
을 살펴보았다.
private final MessageBytes nameB = MessageBytes.newInstance();
private final MessageBytes valueB = MessageBytes.newInstance();
public MessageBytes getName() {
return nameB;
}
해당 메소드는 MimeHeaderField
의 필드 중 nameB
를 반환하는 함수이다!
그러면 mh.getName().setString
은 MessageBytes
라는 객체의 메소드이다. 한번 들어가보자.
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()
를 확인하면은,
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
으로 되었겠거니 했던 안일한 내 생각을 반성하게 되었다 ㅠㅠ 앞으로는 프레임워크를 쓸 때 감사하는 마음을 갖고 써야겠다!
정리되지 않은 긴 글 읽어주셔서 감사합니다~~~