티스토리 뷰
PATCH 메소드는 2010년도에 뒤늦게 HTTP 표준 스펙으로 추가되었습니다. 그리고 이로 인해 스프링 코드도 영향을 받게 되었는데, 왜 영향을 받게 되었고 어떠한 영향을 받게 되었는지 살펴보도록 하겠습니다.
그렇게 중요하지도 않은 내용이고 기록용으로 작성하는 것이라서 편하게 읽어주시면 될 것 같습니다.
1. 뒤늦게 등장한 HTTP PATCH 메소드
[ 뒤늦게 등장한 HTTP PATCH 메소드 ]
PUT과 PATCH의 용도
개발을 하다보면 PATCH 메소드에 대해 잘 모르거나, 익숙해하지 않는 분들이 많이 있다. 또한 PUT과 PATCH의 용도를 헷갈리는 개발자들도 많은데, PUT과 PATCH의 용도를 쉽게 정리하면 다음과 같다.
- PUT: 리소스의 전체 수정
- PATCH: 리소스의 부분 수정
PATCH에 대해 잘 모르거나, PUT과 PATCH를 헷갈리는 이유는 뒤늦은 PATCH 메소드의 탄생과 관련이 있을 것 같다.
뒤늦은 PATCH의 등장
과거 HTTP 표준에는 PATCH 메소드가 존재하지 않았다. 그러다가 2010년도에 Ruby on Rails가 부분 수정의 필요를 주장하였고, 2010년도에 공식 HTTP 프로토콜로 추가되었다.
스프링 프레임워크는 2002년 10월 1일에 첫 릴리즈를 하였는데, 스프링 프레임워크가 탄생하고도 한참 뒤에 등장한 것이다. 그리고 이렇게 스프링 프레임워크보다 늦게 등장한 PATCH 메소드에 의해 HTTP 요청을 처리하는 프론트 컨트롤러인 디스패처 서블릿도 영향을 받게 되었는데, 어떠한 영향을 받았는지 코드로 자세히 살펴보도록 하자.
2. 스프링의 디스패처 서블릿에 미친 영향
[ 스프링의 디스패처 서블릿에 미친 영향 ]
스프링 프레임워크에는 모든 요청을 가장 먼저 받는 프론트 컨트롤러인 디스패처 서블릿이 존재한다. 디스패처 서블릿이 모든 요청을 먼저 받은 다음에 요청을 처리할 컨트롤러를 찾아서 요청을 위임해준다.
그리고 이 디스패처 서블릿은 J2EE 표준 서블릿 기술인 HttpServlet 추상 클래스를 기반으로 한다. 그런데 문제는 HttpServlet이 PATCH 메소드가 등장하기 이전에 만들어졌다는 것이다. 이러한 이유로 HttpServlet에는 다음과 같은 HTTP 메소드들에 대한 추상 메소드들만이 존재하고, patch를 처리하기 위한 메소드(doPatch)는 존재하지 않는다.
- doGet
- doPost
- doPut
- doDelete
- 기타 등등
하지만 2010년에 PATCH 메소드가 HTTP 표준으로 채택되면서 스프링 프레임워크는 PATCH 메소드도 지원을 해야 했다. 하지만 HttpServlet에는 doPatch가 존재하지 않으면서, 해당 코드에 대한 처리는 스프링 프레임워크가 떠앉게 되었다. 그리고 이러한 흔적은 디스패처 서블릿 코드에 고스란히 남아있는데, 해당 코드를 보면서 자세히 살펴보도록 하자.
[ 디스패처 서블릿의 계층 구조와 PATCH 처리 방식 ]
디스패처 서블릿의 계층 구조
가장 먼저 디스패처 서블릿의 계층 구조부터 살펴보도록 하자. 디스패처 서블릿의 계층 구조는 다음과 같다.
위의 여기서 우리가 주목해야 할 부분은 크게 다음과 같다.
- HttpServlet
- Http 서블릿을 구현하기 위한 J2EE 스펙의 추상 클래스
- 특정 HTTP 메소드를 지원하기 위해서는 doX 메소드를 오버라이딩해야 함(템플릿 메소드 패턴)
- doPatch는 지원하지 않음
- HttpServletBean
- HttpServlet을 Spring이 구현한 추상 클래스
- 스프링이 모든 유형의 서블릿 구현을 위해 정의한 공통 클래스
- FrameworkServlet
- 스프링 웹 프레임워크의 기반이 되는 서블릿
- doX 메소드를 오버라이딩하고 있으며, doX 요청들을 공통된 요청 처리 메소드인 processRequest로 전달함
- DispatcherServlet
- 컨트롤러로 요청을 전달하는 중앙 집중형 프론트 컨트롤러(서블릿 구현체)
이러한 상속 구조를 갖는 디스패처 서블릿은 필터들을 통과하면 요청을 처리하는데, PATCH를 지원하는 방법을 살펴보도록 하자.
디스패처 서블릿의 PATCH 처리 방식
필터들을 거쳐 디스패처 서블릿이 가장 먼저 요청을 받으면 하는 것은 서블릿 요청/응답을 HTTP 서블릿 요청/응답으로 변경하는 것이다.
그리도 해당 코드는 DispatcherServlet의 부모 클래스 중 하나인 HttpServlet에 다음과 같이 구현되어 있다.
public abstract class HttpServlet extends GenericServlet {
...
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException(lStrings.getString("http.non_http"));
}
service(request, response);
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
...
}
...
}
그리고 이제 그 다음 service 메소드를 호출하는 부분이 중요하다. service 메소드의 역할은 각각의 HTTP 메소드에 맞게 자식 클래스(FrameworkServlet)에 오버라이딩된 doX 메소드를 호출해주는 것이다. 예를 들어 POST 요청이라면 doPost 메소드를 호출해주어야 한다. 그런데 문제는 HttpServlet은 PATCH 메소드가 등장하기 이전에 탄생하여, PATCH 메소드에 대해 doPatch를 호출해주는 부분이 없다는 것이다. 실제로 HttpServlet의 service 메소드를 보면 다음과 같다.
public abstract class HttpServlet extends GenericServlet {
...
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
... // lastModifed에 따른 doGet 처리(요약함)
doGet(req, resp);
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
... // 에러 처리
}
}
...
}
각각의 HTTP 메소드들에 대해 doX 메소드를 호출해주도록 하고 있는데, 해당 doX를 오버라이딩한 코드를 살펴보면 다음과 같다. doX 메소드에서 호출하는 processRequest는 실제로 요청을 처리하는 메소드이다.
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
...
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected final void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
...
}
HttpServlet에는 doPatch 메소드가 존재하지 않으므로 PATCH 메소드를 지원하는 것이 불가능하였다. J2EE 표준 기술인 HttpServlet를 스프링 개발자들이 함부로 수정할수는 없었고, 그렇다고 표준 기술로 채택된 PATCH 메소드를 지원하지 않을수도 없는 노릇이다.
그래서 스프링은 PATCH를 지원하고자 자식 클래스인 FrameworkServlet에 HttpServlet의 service 메소드를 오버라이딩하였다.
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
...
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response);
}
else {
super.service(request, response);
}
...
}
}
만약 요청 메소드가 PATCH라면 바로 요청을 처리하는 processRequest를 호출하고, PATCH가 아니라면 부모 클래스인 HttpServlet의 service를 호출하도록 구현한 것이다. 이러한 구조로 PATCH 메소드를 지원하도록 처리하였다.
처음 위와 같은 디스패처 서블릿 코드 구조를 보고서는 의아함을 가졌지만, PATCH 메소드가 뒤늦게 탄생한 것임을 알고 나서는 이해할 수 있었습니다. 전체적인 디스패처 서블릿의 동작 과정을 코드로 살펴보기를 원한다면 이 글을 참고하면 좋을 것 같습니다!
혹시 피드백 있으면 댓글로 남겨주세요! 감사합니다:)