공부해봅시당
[Spring] WebServer에서 CGI, Servlet, Spring Web MVC의 DispatcherServlet까지 본문
[Spring] WebServer에서 CGI, Servlet, Spring Web MVC의 DispatcherServlet까지
tngus 2024. 4. 20. 00:30역사를 통해 알아보는 Servlet의 탄생
1. [초창기] 정적 데이터만 전달하는 Web Server
정적 데이터란?
쉽게 말해 어떤 사용자가 들어와도 같은 화면이라는 뜻
아래 사이트를 들어가보면 어떤 사용자가 들어와도 모두 같은 화면만 보여주게 되는데, 대표적인 정적 페이지만 보여주는 웹사이트의 예시라고 할 수 있겠다.
어떤 문제가 있나?
위 정적페이지 만으로도 충분히 정보를 잘 전달하고 있다.
하지만 우리가 들어갈 때마다 나만의 정보를 받고 싶다면?
당장 쇼핑몰 사이트를 통해 옷을 구매하고 싶어도 로그인을 거쳐 나라는 사용자로 커스텀된 정보들을 받아야 한다.
하지만 위에서는 정해진 정보들만 주기 때문에 불가능하다.
2. [그렇다면 동적인 데이터를 줄 수 있도록 해보자!] CGI(Common Gateway Interface)
위에서 봤던 그림에서 하나 달라졌다. 바로 Web Server 뒤에 CGI 구현체가 생긴 것!
해피해피><
이것으로 인해 우리는 집에서 홈쇼핑도 하고, 블로깅도 할 수 있게 되었다~! 얏호 만세!!
그럼 CGI가 뭐길래 내가 사용자라는 걸 알 수 있게, 그러니까 동적인 데이터를 줄 수 있게 해주는걸까?
누구냐, 넌: CGI(Common Gateway Interface)
CGI는 Web Server와 프로그램 사이의 규약을 의미한다.
예를 들어 Web Server에 Apache가 있다면, CGI 구현체로는 C나 PHP라는 다양한 언어를 사용할 수 있다.
그렇기 때문에 CGI를 만들어서 Apache와 PHP 간의 소통이 가능하게끔 만든 것이다.
이를 통해 동적 컨텐츠 전달이 가능해지면서 집에서 홈쇼핑도 하고, 지금 쓰고 있는 것처럼 티스토리 블로그도 쓰고 다 할 수 있게 되었다!
그렇다면 CGI가 작동하는 방식은?
(1) HTTP 요청(Request)
사용자가 웹 페이지의 특정 부분을 클릭하거나 양식을 제출
(2) CGI 스크립트를 게이트웨이로 다른 파일이나 프로그램에 연계 → 처리 실행
CGI 스크립트는 그 요청을 받아서 필요한 작업을 수행한 후 결과를 웹 서버에 다시 전송
(3) 웹 서버는 이 결과를 사용자의 웹 브라우저에 전송하여 사용자에게 보여줌
하지만 그에게도 문제가 있었으니....
하지만 CGI는 요청이 발생할 때마다 프로세스를 만들어 버린다는 안타깝고도 치명적이라면 치명적인 단점이 있어버렸다....
프로세스가 헷갈리면 고개를 들어 아래 표를 보자.
보면 감이 오겠지만 스레드로 진행하지 않고 프로세스로 진행하면 딱 봐도 엄청 비효율적으로 보인다.
각 요청에 대해 같은 구현체더라도, 새로운 프로세스를 생성하기 때문에 높은 트래픽에서는 성능 저하가 일어날 수 있다.
문제점을 정리하면
1. 프로세스이기 때문에 발생할 수 있는 성능 저하
2. 같은 요청과 같은 구현체를 가짐에도 요청 시마다 발생되는 CGI 구현체
3. [프로세스에서 스레드로! 여러 인스턴스에서 싱글톤으로!] 서블릿의 두둥등장
위에서 발생했던 문제를 서블릿에서는 어떻게 해결했을까?
(1)프로세스에서 스레드로 변경하고, (2)Servlet 구현체를 통해 싱글톤 패턴을 유지하도록 한 것이다.
그리고 여기서의 특징은 하나 더 있는데, 위 사진에서와 다르게 갑자기 Web Application Server가 등장한 것이다.
궁금하다면 관련 포스팅은 링크를 참고하자
그래서 Servlet이란?
Servlet은 사용자의 요청을 처리해서 동적인 컨텐츠를 응답으로 주기 위한 프로그램이다.
Servlet을 통해 스레드로의 변경과 싱글톤 패턴 도입이 가능해졌다.
Servlet의 작업들은 Servlet Container(ex. Tomcat)에 의해 관리된다.
<Servlet>
Servlet은 Java EE 의 표준 API 중 하나로, `javax.servlet.Servlet`을 최상위 인터페이스로 가진다.
Servlet 표준 속에서 중요한 내용은 Servlet은 전체 데이터 처리 과정 속에서 오직 특정한 부분만 다룬다는 것이다.
예를들어 Servlet 은 결코 특정 포트를 listen, 클라이언트와 직접적인 Communication(Socket), 외부 리소스 접근과 같은 역할은 Servlet이 다루지 않는다.
이러한 특징은 작성한 Servlet이 다양한 환경에서 재사용 가능하면서, 다른 개발 설정과의 의존 관계를 분리시켜 줄 수 있도록 한다.
Servlet Container란?
Servlet Container는 Servlet을 관리한다.
Servlet Container의 역할을 하는 것은 SpringBoot의 내장 WAS인 Tomcat이다.
물론 Tomcat 이외에도 같은 역할을 할 수 있는 WAS가 많지만
SpringBoot에서는 Tomcat을 default WAS로 내장하여 사용하고 있기 때문에
앞으로 여기서는 Tomcat으로 Servlet Container에 대한 코드를 살펴보도록 하겠다.
유연한 설정이나 여러 기술을 지원하는 상호운용성 같은 특성 때문에 톰켓을 웹 어플리케이션 서버로 사용했더라도,
톰캣의 핵심적인 역할은 자바 서블릿 컨테이너이다.
<Tomcat>
톰캣은 Java EE Platform의 Java Servlet과 JSP을 구현해서, 클라이언트로부터 요청을 받았을 때, 요청에 대응할 수 있는 서블릿 클래스를 컴파일 후 인스턴스화해주고 요청을 처리해서 동적인 응답을 해줄 수 있다.
자바 서블릿 표준은 다른 주요 자바 웹 기술들과 상호운용을 위해 디자인 되었기 때문에 Tomcat Server에 올려진 Servlet은 톰캣이 제공하는 어떠한 리소스든 사용할 수 있다.
톰켓의 XML 설정 파일은 매우 세밀한 리소스 접근 제어를 가능하게하고, 낮은 결합도를 유지하며, 쉬운 배포를 가능하게 한다.
Servlet의 Life Cycle에 대해 알아보자
마찬가지로 Tomcat에서 동작하는 서블릿 Life cycle이다.
Tomcat을 벗어나는 오른쪽 부분은 스프링부트 부분이라고 생각하면 편할 것이다.
아래 내용이 이해가 가지 않는다면 시간이 많으신 분들을 위해 준비한 코드로 이해해 보는 과정이 있다.
따라서 끝까지 읽어보면 좋을 듯 하다.
- 톰캣이 가지고 있는 Connector 중 하나를 통해서 request를 전달받는다.
- 톰캣은 해당 request를 처리하기 위해 적절한 Engine에 거쳐 적절한 Servlet에 매핑한다. 여기서 Engine은 여러가지의 어플리케이션을 감싸는 그룹이라 생각하면 편하다.
- 일단 요청이 적절한 서블릿에 매핑되면, 톰캣은 서블릿 인스턴스가 올라와있는지 체크해서 만약 존재 하지않다면 톰캣은 JVM에 의해 서블릿이 실행될 수 있게 서블릿 인스턴스를 생성한다.
- 톰캣은 init 메소드를 호출함으로써 서블릿을 초기화한다.
- 일단 서블릿이 초기화되면, 톰캣은 서블릿의 service 메소드를 호출해서 request를 처리하고, 동적인 컨텐츠를 응답으로 반환한다.
- 추가로, 서블릿 Life cycle 동안, 서블릿 컨테이너와 서블릿은 다양한 상태 변화들에 대해서 서블릿을 모니터링할 수 있는 listener 클래스를 사용해서 소통할 수 있다. 톰캣은 다양한 방식으로 상태변화를 저장, 조회할 수 있고 다른 서블릿들도 그것들에 접근하는 것이 가능하다.
이 기능에 대한 대표적인 예는 e-commerce 어플리케이션이 유저가 장바구니에 담아 놓았던 아이템들을 기억하고 결제 프로세스에 데이터를 넘길 수 있는 것이다.
7. 마지막으로 톰캣은 서블릿을 제거하기위해 서블릿의 destory 메소드를 호출한다. 해당 작업은 서블릿의 상태 변화에 의해 혹은 어플리케이션을 undeploy하기 위해 톰캣에 전달된 외부 명령에 의해, 아니면 server shutdown에 의해 발생한다.
뭐가 뭔지 하나하나 살펴보자
WebContainer(Servlet Container - ex.Tomcat)
- 요청이 들어오면 요청마다 스레드를 생성해준다
- 스레드랑 Servlet 구현체를 연결해준다
-> Servlet은 Interface로 되어있는데, 그 메서드를 호출해주는 역할을 하는 것이다.
요청마다 스레드를 생성하고 Servlet을 실행시킨다. 그리고 Servlet Interface에 따라 Servlet을 관리한다.
정리해서 말하면 WebContainer 즉, Servlet Container인 Tomcat이 아래 Servlet Interface의 메소드를 호출해주는 것이다.
Servlet Interface
Servlet에는 주요 메서드 3가지가 있다.
1) init(ServletConfig config): Servlet Instance 생성의 역할
- 초기화 단계에 호출: 한 번 생성되면 destroy되기 전까지 다시 호출되지 않음
- ServletConfig 인터페이스를 구현하는 오브젝트가 전달됨
- 이를 통해 서블릿이 웹 애플리케이션에서 초기화 매개변수(parameter)에 접근할 수 있도록 함
2) service(ServletRequest request, ServletResponse response): 실제 기능이 수행되는 곳
- 초기화 이후 각각의 요청들 이 들어오면 호출
- 각각의 요청들은 별도로 나누어진 스레드에서 처리됨
- 웹 컨테이너는 모든 요청에 대해 서블릿의 service() 메소드를 요청함
- service() 메소드는 요청의 종류(GET, POST, PUT, DELETE 등)를 판별하고 요청을 처리할 적절한 메소드로 전달함
3) destroy(): Servlet Instance가 사라짐
- 서블릿 객체가 파괴되어야 할 때 호출
- 해당 서블릿이 가지고 있던 자원을 release 해줌
- 서블릿 생명주기에서 보통 한 번만 호출되고, 주로 애플리케이션이 종료될 때 호출됨
public interface Servlet {
void init(ServletConfig config) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
위에 있는 ServletConfig가 궁금해서 들어가보니 이렇게 되어 있었다.
ServletConfig는 이름에서부터 느낄 수 있는 것과 같이 아래와 같다.
A servlet configuration object used by a servlet container to pass information to a servlet during initialization.
초기화 중에 서블릿 컨테이너가 서블릿에 정보를 전달하는 데 사용하는 서블릿 구성 객체
public interface ServletConfig {
String getServletName();
ServletContext getServletContext();
String getInitParameter(String name);
Enumeration<String> getInitParameterNames();
}
위 Servlet, ServletConfig, java.io.Serializable을 구현한 GenericServlet 구현체가 만들어지고, GenericServlet을 상속받은 HttpServlet이 실제 HTTP 요청을 처리하게 된다.
// 아래 메서드 이외에도 다양한 메서드가 있지만 주요 3가지 메서드만을 살펴보면 아래와 같음
public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable {
@Override
public void destroy() {
// NOOP by default
// NOOP: No Operation"의 약어로, 이 주석이 포함된 코드 블록이 실제로 아무런 동작도 수행하지 않는다는 것을 의미
// 의도적으로 비워져 있음을 명시
}
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
@Override
public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
}
HttpServlet에는 HTTP 메서드인 POST, GET, PUT, DELETE 등을 수행하기 위한 메서드가 함께 정의되어 있다.
public abstract class HttpServlet extends GenericServlet {
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
cachedUseLegacyDoHead = Boolean.parseBoolean(config.getInitParameter(LEGACY_DO_HEAD));
}
@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); // 아래 service 메서드 호출
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} 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 {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_get_not_supported");
sendMethodNotAllowed(req, resp, msg);
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_post_not_supported");
sendMethodNotAllowed(req, resp, msg);
}
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_put_not_supported");
sendMethodNotAllowed(req, resp, msg);
}
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String msg = lStrings.getString("http.method_delete_not_supported");
sendMethodNotAllowed(req, resp, msg);
}
아래 의문점부터는 위에서 설명한 내용에 대해 구현체를 찾으며
실제로 어떻게 동작하는지 코드적으로 분석한 것이므로 시간이 아주아주 많은 사람들만 확인하길 바란다.
시간이 부족한 사람들은 쭉~~ 스크롤을 아래로 내려서 디스패처 서블릿부터 다시 읽으면 될 듯 하다.
여기서 의문점!
destroy() 메서드가 분명!! Servlet Instance를 사라지게 해준다고 했는데... GenericServlet 구현체를 확인해보면 destroy()에 NOOP만 있고 실제로 내부적인 실행 로직이 존재하지 않는다. HttpServlet을 살펴봐도 GenericServlet의 destroy()를 @Override 하지 않고 GenericServlet의 destroy()를 그대로 사용하고 있다.
아니 그럼 destroy가 어떤 역할을 해주는 거지?
Servlet 인터페이스에 정의되어 있는 destroy의 주석을 살펴보면 아래와 같이 설명되어 있다.
Called by the servlet container to indicate to a servlet that the servlet is being taken out of service. This method is only called once all threads within the servlet's service method have exited or after a timeout period has passed. After the servlet container calls this method, it will not call the service method again on this servlet.
This method gives the servlet an opportunity to clean up any resources that are being held (for example, memory, file handles, threads) and make sure that any persistent state is synchronized with the servlet's current state in memory.
서블릿 컨테이너에 의해 서블릿이 서비스에서 제거되고 있다는 것을 서블릿에게 알리기 위해 호출됩니다. 이 메서드는 서블릿의 서비스 메서드 내의 모든 스레드가 종료되었거나 일정 시간이 지난 후에만 호출됩니다. 서블릿 컨테이너가 이 메서드를 호출한 후에는 이 서블릿에 대해 서비스 메서드를 다시 호출하지 않습니다. 이 메서드는 서블릿이 보유하고 있는 모든 리소스(예: 메모리, 파일 핸들, 스레드 등)를 정리하고 서블릿의 현재 메모리 상태와 영구 상태가 동기화되어 있는지 확인할 기회를 제공합니다.
그럼 도대체 어디서...?
정말 모르겠다.... 바로 GPT에게 물어보기~!
Java의 Servlet 인터페이스의 destroy() 메서드는 일반적으로 서블릿의 수명 주기를 끝내는 데 사용됩니다. 이 인터페이스에는 destroy() 메서드가 정의되어 있지만, 실제 구현 내용은 구현체에 달려 있습니다. 기본적으로 Servlet 인터페이스의 구현은 "no operation" (NOOP)으로 명시되어 있어서, 아무 작업도 하지 않습니다.
실제로 destroy() 메서드가 구현되는 곳은 개발자가 작성하는 구체적인 서블릿 클래스 또는 서블릿 컨테이너의 구현체 내부입니다. 예를 들어, Tomcat, Jetty, GlassFish와 같은 서블릿 컨테이너들은 이 메서드를 다양하게 활용하여 서블릿의 종료 과정에서 필요한 리소스 해제, 로그 기록, 연결 종료 등의 작업을 구현할 수 있습니다.
서블릿을 개발할 때, 개발자는 destroy() 메서드를 오버라이드하여 필요한 정리 작업을 구현할 수 있습니다. 이는 예를 들어 열려 있는 파일 핸들을 닫거나, 데이터베이스 연결을 종료하거나, 실행 중인 백그라운드 스레드를 중단하는 등의 작업을 포함할 수 있습니다.
따라서 destroy()의 실제 동작은 서블릿 클래스를 개발하는 개발자나 서블릿 컨테이너의 개발자에 의해 결정되며, 특정 서블릿 컨테이너의 소스 코드나 개발 문서를 참조하면 이 메서드의 구현 세부사항을 더 자세히 알 수 있습니다.
오호라... 그럼 톰캣에서 destroy를 도대체 어떻게 삭제했다는 건지 너무 궁금해져버렸,,,!
톰캣의 StandardWrapper.java에 들어가면 있다고 한다.
톰캣은 오픈소스로 되어 있기 때문에 구현체를 모두 확인할 수 있다.
바로 Github에 들어가서 확인해보자!!
링크는 다음과 같다.
https://github.com/apache/tomcat/blob/main/java/org/apache/catalina/core/StandardWrapper.java
벌써 설레
StandardWrapper 클래스가 있는 폴더 구조는 아래와 같다.
tomcat-source/
└── java/
└── org/
└── apache/
└── catalina/
└── core/
├── StandardWrapper.java
└── (기타 관련 클래스)
여기서 StandardWrapper를 살펴보면 위에서 살펴봤던 ServletConfig를 구현하고, Servlet 인터페이스의 init(), service(), destroy()를 호출하는 것을 알 수 있다.
위에서 우리는 Servlet의 Life Cycle에 대해서 알아보았다.
위에서는 4번 항목에 `톰캣은 init 메소드를 호출`한다고만 명시되어 있다.
하지만 톰캣의 어떤 클래스의 어떤 메서드가 init 메서드를 호출하는 것이고, 그 자세한 코드는 어떻게 구현되어 있는지 아래 내용을 통해 알아볼 수 있다.
자세히 살펴보자!
1) 톰캣 StandardWrapper 클래스의 loadServlet() 메서드: Servlet 인터페이스의 init()메서드 호출
/**
* Load and initialize an instance of this servlet, if there is not already an initialized instance. This can be
* used, for example, to load servlets that are marked in the deployment descriptor to be loaded at server startup
* time.
*
* @return the loaded Servlet instance
*
* @throws ServletException for a Servlet load error
*/
public synchronized Servlet loadServlet() throws ServletException {
// Nothing to do if we already have an instance or an instance pool
if (instance != null) {
return instance;
}
PrintStream out = System.out;
if (swallowOutput) {
SystemLogHandler.startCapture();
}
Servlet servlet;
try {
long t1 = System.currentTimeMillis();
// Complain if no servlet class has been specified
if (servletClass == null) {
unavailable(null);
throw new ServletException(sm.getString("standardWrapper.notClass", getName()));
}
InstanceManager instanceManager = ((StandardContext) getParent()).getInstanceManager();
try {
servlet = (Servlet) instanceManager.newInstance(servletClass);
} catch (ClassCastException e) {
unavailable(null);
// Restore the context ClassLoader
throw new ServletException(sm.getString("standardWrapper.notServlet", servletClass), e);
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
unavailable(null);
// Added extra log statement for Bugzilla 36630:
// https://bz.apache.org/bugzilla/show_bug.cgi?id=36630
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardWrapper.instantiate", servletClass), e);
}
// Restore the context ClassLoader
throw new ServletException(sm.getString("standardWrapper.instantiate", servletClass), e);
}
if (multipartConfigElement == null) {
MultipartConfig annotation = servlet.getClass().getAnnotation(MultipartConfig.class);
if (annotation != null) {
multipartConfigElement = new MultipartConfigElement(annotation);
}
}
// Special handling for ContainerServlet instances
// Note: The InstanceManager checks if the application is permitted
// to load ContainerServlets
if (servlet instanceof ContainerServlet) {
((ContainerServlet) servlet).setWrapper(this);
}
classLoadTime = (int) (System.currentTimeMillis() - t1);
initServlet(servlet);
fireContainerEvent("load", this);
loadTime = System.currentTimeMillis() - t1;
} finally {
if (swallowOutput) {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
if (getServletContext() != null) {
getServletContext().log(log);
} else {
out.println(log);
}
}
}
}
return servlet;
}
위 메서드를 확인해 보면 finally 이전에 initServlet()메서드를 호출한다.
이 메서드에서 Servlet.init() 메서드를 호출한다.
private synchronized void initServlet(Servlet servlet) throws ServletException {
if (instanceInitialized) {
return;
}
// Call the initialization method of this servlet
try {
servlet.init(facade);
instanceInitialized = true;
} catch (UnavailableException f) {
unavailable(f);
throw f;
} catch (ServletException f) {
// If the servlet wanted to be unavailable it would have
// said so, so do not call unavailable(null).
throw f;
} catch (Throwable f) {
ExceptionUtils.handleThrowable(f);
getServletContext().log(sm.getString("standardWrapper.initException", getName()), f);
// If the servlet wanted to be unavailable it would have
// said so, so do not call unavailable(null).
throw new ServletException(sm.getString("standardWrapper.initException", getName()), f);
}
}
위 두 메서드 모두 synchronized 키워드가 메서드 단에 붙어있다.
자바에서 제공하는 synchronized 키워드는 스레드의 동시성 처리를 위한 것이다.
따라서 아주아주 위에서 이야기 했던 CGI 구현체가 프로세스로 실행되는 문제를 스레드로 수정한 것이 여기 코드에서 드러나는 것이다.
그리고 여담으로,,,
(1) facade 디자인 패턴
servlet.init()에 facade 객체를 넘겨주는 것을 보면 facade 패턴으로 디자인 패턴이 구성되어 있는 것도 알 수 있다.
facade 패턴에 대해 궁금한 분은 링크를 확인해보길 바란다.
(2) Reflection
위 loadServlet() 메서드 코드에서는 getClass().getAnnotaions()라는 코드를 통해 클래스 정보를 받아오고 있다.
이는 자바의 Reflection을 이용한 것이다.
궁금한 분은 링크를 확인해보길 바란다.
(CS 공부를 하면서 학습했던 것들이 등장하니 상당히 반가운 기분이다.)
2) 톰캣 StandardWrapper 클래스의 unload() 메서드: Servlet 인터페이스의 destroy()메서드 호출
확인해보면 try catch문에서 try에 instance.destroy()를 호출하고, 실제로 destroy를 해주는 코드는 finally가 끝나고 `instance = null;`에서 완전히 내용을 비워줌으로써 destroy 해주는 것을 알 수 있다.
(더해서 바로 아래 Registry 코드를 통해서도 뭔가 작업을 해주는 것 같은데 이 부분은 정확히는 모르겠어서 다음 기회에 다시 파보도록 하겠다.)
더해 해당 메서드에도 synchronized 키워드를 통해 동시성 처리를 해주고 있다.
/**
* Unload all initialized instances of this servlet, after calling the <code>destroy()</code> method for each
* instance. This can be used, for example, prior to shutting down the entire servlet engine, or prior to reloading
* all of the classes from the Loader associated with our Loader's repository.
*
* @exception ServletException if an exception is thrown by the destroy() method
*/
@Override
public synchronized void unload() throws ServletException {
// Nothing to do if we have never loaded the instance
if (instance == null) {
return;
}
unloading = true;
// Loaf a while if the current instance is allocated
if (countAllocated.get() > 0) {
int nRetries = 0;
long delay = unloadDelay / 20;
while ((nRetries < 21) && (countAllocated.get() > 0)) {
if ((nRetries % 10) == 0) {
log.info(sm.getString("standardWrapper.waiting", countAllocated.toString(), getName()));
}
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
// Ignore
}
nRetries++;
}
}
if (instanceInitialized) {
PrintStream out = System.out;
if (swallowOutput) {
SystemLogHandler.startCapture();
}
// Call the servlet destroy() method
try {
instance.destroy();
} catch (Throwable t) {
t = ExceptionUtils.unwrapInvocationTargetException(t);
ExceptionUtils.handleThrowable(t);
fireContainerEvent("unload", this);
unloading = false;
throw new ServletException(sm.getString("standardWrapper.destroyException", getName()), t);
} finally {
// Annotation processing
if (!((Context) getParent()).getIgnoreAnnotations()) {
try {
((Context) getParent()).getInstanceManager().destroyInstance(instance);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("standardWrapper.destroyInstance", getName()), t);
}
}
// Write captured output
if (swallowOutput) {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
if (getServletContext() != null) {
getServletContext().log(log);
} else {
out.println(log);
}
}
}
instance = null;
instanceInitialized = false;
}
}
// Deregister the destroyed instance
instance = null;
if (isJspServlet && jspMonitorON != null) {
Registry.getRegistry(null, null).unregisterComponent(jspMonitorON);
}
unloading = false;
fireContainerEvent("unload", this);
}
3) 톰캣 StandardWrapper 말고 StandardWrapperValve 클래스의 invoke() 메서드
길다.. 매우 길다....
그냥 궁금할까봐 전체 코드 가져와봤다...
하지만 다 읽지 않아도 괜찮다.
중요한 부분은 아래에서 다시 보도록 하겠다.
final class StandardWrapperValve extends ValveBase {
/**
* Invoke the servlet we are managing, respecting the rules regarding servlet lifecycle support.
*
* @param request Request to be processed
* @param response Response to be produced
*
* @exception IOException if an input/output error occurred
* @exception ServletException if a servlet error occurred
*/
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// Initialize local variables we may need
boolean unavailable = false;
Throwable throwable = null;
// This should be a Request attribute...
long t1 = System.currentTimeMillis();
requestCount.increment();
StandardWrapper wrapper = (StandardWrapper) getContainer();
Servlet servlet = null;
Context context = (Context) wrapper.getParent();
// Check for the application being marked unavailable
if (!context.getState().isAvailable()) {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardContext.isUnavailable"));
unavailable = true;
}
// Check for the servlet being marked unavailable
if (!unavailable && wrapper.isUnavailable()) {
container.getLogger().info(sm.getString("standardWrapper.isUnavailable", wrapper.getName()));
checkWrapperAvailable(response, wrapper);
unavailable = true;
}
// Allocate a servlet instance to process this request
try {
if (!unavailable) {
servlet = wrapper.allocate();
}
} catch (UnavailableException e) {
container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e);
checkWrapperAvailable(response, wrapper);
} catch (ServletException e) {
container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()),
StandardWrapper.getRootCause(e));
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e);
throwable = e;
exception(request, response, e);
servlet = null;
}
MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType() == DispatcherType.ASYNC) {
dispatcherType = DispatcherType.ASYNC;
}
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR, dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR, requestPathMB);
// Create the filter chain for this request
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
Container container = this.container;
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(), response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(), response.getResponse());
}
}
}
} catch (BadRequestException e) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug(
sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
}
throwable = e;
exception(request, response, e, HttpServletResponse.SC_BAD_REQUEST);
} catch (CloseNowException e) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug(
sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
}
throwable = e;
exception(request, response, e);
} catch (IOException e) {
container.getLogger()
.error(sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
throwable = e;
exception(request, response, e);
} catch (UnavailableException e) {
container.getLogger()
.error(sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
wrapper.unavailable(e);
checkWrapperAvailable(response, wrapper);
// Do not save exception in 'throwable', because we
// do not want to do exception(request, response, e) processing
} catch (ServletException e) {
Throwable rootCause = StandardWrapper.getRootCause(e);
if (!(rootCause instanceof BadRequestException)) {
container.getLogger().error(sm.getString("standardWrapper.serviceExceptionRoot", wrapper.getName(),
context.getName(), e.getMessage()), rootCause);
}
throwable = e;
exception(request, response, e);
} catch (InvalidParameterException e) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug(
sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
}
throwable = e;
exception(request, response, e, e.getErrorCode());
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger()
.error(sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
throwable = e;
exception(request, response, e);
} finally {
// Release the filter chain (if any) for this request
if (filterChain != null) {
filterChain.release();
}
// Deallocate the allocated servlet instance
try {
if (servlet != null) {
wrapper.deallocate(servlet);
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.deallocateException", wrapper.getName()), e);
if (throwable == null) {
throwable = e;
exception(request, response, e);
}
}
// If this servlet has been marked permanently unavailable,
// unload it and release this instance
try {
if ((servlet != null) && (wrapper.getAvailable() == Long.MAX_VALUE)) {
wrapper.unload();
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.unloadException", wrapper.getName()), e);
if (throwable == null) {
exception(request, response, e);
}
}
long t2 = System.currentTimeMillis();
long time = t2 - t1;
processingTime.add(time);
if (time > maxTime) {
maxTime = time;
}
if (time < minTime) {
minTime = time;
}
}
}
아래 코드를 확인해보면 `StandardWrapper wrapper = (StandardWrapper) getContainer();`를 통해 가져온 wrapper를 통해 할당되어 있는지 확인하고, 아니라면 wrapper.allocate()를 통해 할당한다.
// Check for the servlet being marked unavailable
if (!unavailable && wrapper.isUnavailable()) {
container.getLogger().info(sm.getString("standardWrapper.isUnavailable", wrapper.getName()));
checkWrapperAvailable(response, wrapper);
unavailable = true;
}
// Allocate a servlet instance to process this request
try {
if (!unavailable) {
servlet = wrapper.allocate();
}
} catch (UnavailableException e) {
container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e);
checkWrapperAvailable(response, wrapper);
} catch (ServletException e) {
container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()),
StandardWrapper.getRootCause(e));
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.allocateException", wrapper.getName()), e);
throwable = e;
exception(request, response, e);
servlet = null;
}
이후 Filter에 대해서도 확인해보면, 위 코드에서 Filter와 관련한 부분은 아래 코드이다.
확인해보면 StandardWrapperValve가 상속받은 ValveBase에 Container가 있다.
public abstract class ValveBase extends LifecycleMBeanBase implements Contained, Valve {
/**
* The Container whose pipeline this Valve is a component of.
*/
protected Container container = null;
위 Container를 가져와서 StandardWrapperValve 클래스의 invoke()메소드 아래 부분의 catch 예외 처리를 해준다.
여기서 filterChain을 만든다.
여기서 servlet이 null이 아니고 filterChain이 null이 아니면 doFilter를 통해 filter 작업을 진행한다.
여기서 doFilter를 실행시키기 때문에 filter는 서블릿 컨테이너에 의해 관리된다고 말하는 것이다.
Filter에 대해서는 링크를 참고하자.
// Create the filter chain for this request
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
Container container = this.container;
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(), response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter(request.getRequest(), response.getResponse());
}
}
}
} catch (BadRequestException e) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug(
sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
}
throwable = e;
exception(request, response, e, HttpServletResponse.SC_BAD_REQUEST);
} catch (CloseNowException e) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug(
sm.getString("standardWrapper.serviceException", wrapper.getName(), context.getName()), e);
}
throwable = e;
exception(request, response, e);
...... 생략
그리고 doFilter에 대해 설명한 이유...
doFilter 메서드가 service() 메서드를 호출하게 된다!!!
레전드....
FilterChain 인터페이스의 구현체인 ApplicationFilterChain의 doFilter()를 살펴보면 아래와 같다.
public final class ApplicationFilterChain implements FilterChain {
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged((java.security.PrivilegedExceptionAction<Void>) () -> {
internalDoFilter(req, res);
return null;
});
} catch (PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException) {
throw (ServletException) e;
} else if (e instanceof IOException) {
throw (IOException) e;
} else if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new ServletException(e.getMessage(), e);
}
}
} else {
internalDoFilter(request, response);
}
}
위 코드에서 internalDoFilter() 메서드를 호출하는데 이 메서드에서 service()를 호출하는 것이다.
internalDoFilter() 코드에서 중요한 부분만 살펴보면 아래와 같다.
우리가 스프링코드에서 열심히 작성하는 SecurityUtil을 호출해야 하면 SecurityUtil과 관련한 코드를 호출한 이후 service 작업이 진행되는 것이고, 아니라면 servlet에서 service() 메서드를 바로 호출하게 된다.
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal = ((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[] { req, res };
SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal);
} else {
servlet.service(request, response);
}
여담) 톰캣 StandardWrapper 클래스의 getServletMethods() 메서드: Servlet 인터페이스의 doGet(), doPost 등 메서드 호출
자바의 제네릭 상한 제한 `Class<? extends Servlet>`을 사용해 Servlet 인터페이스 구현체의 정보를 가져온다.
(여기서는 HttpServlet이 될 것이다.)
이후 리플렉션 메서드를 활용해 StandardWrapper에서 정의한 `getAllDeclaredMethods()`를 통해 메서드 정보를 모두 가져오고, 메서드 정보를 `Set<String> allow`에 담아둔다.
이후 service()에서드에서 HTTP의 메서드인 GET, POST, PUT, DELETE에 따른 doGet(), doPost(), doPut(), doDelete() 메서드를 실제로 호출하고 실행하게 될 것이다.
@Override
public String[] getServletMethods() throws ServletException {
instance = loadServlet();
Class<? extends Servlet> servletClazz = instance.getClass();
if (!jakarta.servlet.http.HttpServlet.class.isAssignableFrom(servletClazz)) {
return DEFAULT_SERVLET_METHODS;
}
Set<String> allow = new HashSet<>();
allow.add("OPTIONS");
if (isJspServlet) {
allow.add("GET");
allow.add("HEAD");
allow.add("POST");
} else {
allow.add("TRACE");
Method[] methods = getAllDeclaredMethods(servletClazz);
for (int i = 0; methods != null && i < methods.length; i++) {
Method m = methods[i];
if (m.getName().equals("doGet")) {
allow.add("GET");
allow.add("HEAD");
} else if (m.getName().equals("doPost")) {
allow.add("POST");
} else if (m.getName().equals("doPut")) {
allow.add("PUT");
} else if (m.getName().equals("doDelete")) {
allow.add("DELETE");
}
}
}
return allow.toArray(new String[0]);
}
여기까지 코드적으로 Tomcat과 Servlet이 어떤 연관이 있는 것이고, 어떻게 구현되어 있는지 살펴보았다.
4. [공통 로직 분리!] DispatcherServlet까지...!
헉헉... 구현체까지 확인하고 오니 이해가 더 잘 되는 것 같다.
하지만 이런 레전드 Servlet도 문제가 있었으니...
Servlet의 문제
서블릿의 경우에는 `요청 경로마다 서블릿을 정의`해주게 된다.
요청 경로마다 서블릿을 정의해주는 것은 핸들러마다 공통된 로직을 중복 작성한다는 비효율적인 측면이 있다. (공통된 로직이라 하면 한글인코딩처리 등등이 있다)
서블릿을 개별적으로 다루어 공통된 로직을 여러번 작성하지 말고 공통된 로직을 하나의 서블릿만으로 앞단에 두어 모든 클라이언트의 요청을 처리하면 어떨까?
Front Controller 패턴
공통된 로직을 매번 작성하지 않아도 되서 개발자는 핵심 로직에만 집중할 수 있을 것이다.
이러한 디자인 패턴을 프론트 컨트롤러 패턴이라고 한다.
프론트 컨트롤러 패턴을 통해 여러 이점을 얻을 수 있다.
1. 컨트롤러를 구현할 때 직접 서블릿을 다루지 않아도 된다.
2. 공통 로직 처리가 가능하다.
우리가 스프링을 이용해 개발을 해오면서 서블릿을 직접 다룬 적이 없었던 이유도 스프링 자체가 프론트 컨트롤러 패턴을 따르기 때문이다.
Dispatcher Servlet
스프링은 프론트 컨트롤러 패턴을 따르고, 이를 DispatcherServlet이 담당한다.
DispatcherServlet은 클라이언트의 요청을 먼저 받아 필요한 처리를 한 뒤, 개발자가 구현한 요청에 맞는 핸들러에게 요청을 Dispatch하고 해당 핸들러의 실행 결과를 Response형태로 만드는 역할을 한다.
과정을 한 눈에 다시 보면 아래와 같다
Servlet Container에서 Filter의 doFilter를 호출하고, doFilter에서 DispatcherServlet을 호출한 다음 아래 작업이 일어난다고 생각하면 된다.
1) ServletContainer(톰캣)에서 DispatcherServlet 호출
2) HandlerMapping을 통해 매핑할 Controller 확인
3) HadlerAdapter를 통해 Controller 매핑 후 model and view 전달 받음
4) DispatcherServlet은 ViewResolver를 통해 클라이언트에 전달할 view를 가져온다. (주로 JSP가 이에 해당)
5) View에 model을 전달하여 동적인 데이터를 뷰에 삽입함으로써 결과적으로 client에 전달될 화면이 완성되고 다시 client에게 전달한다.
위와 같은 내용이지만 DispatcherServlet부터의 내용이 더 자세하기 때문에 아래 그림도 함께 참고해보자.
DispatcherServlet의 구현체
DispatcherServlet의 구현체에 대한 내용은 여기서는 다루지 않도록 하겠다.
왜냐하면 일단 글이 너무 많이 길어지기도 했고,,, 무엇보다 망나니개발자님 블로그에 진짜 엄청 자세히 잘 다루어져 있기 때문이다.
아래 링크에 들어가 코드를 통해 확인하면서 마무리해보도록 하자!
https://mangkyu.tistory.com/216
[Spring] SpringBoot 소스 코드 분석하기, DispatcherServlet(디스패처 서블릿) 동작 과정 - (7)
이번에는 SpringBoot의 실행 과정을 소스 코드로 직접 살펴보려고 합니다. 지난번에는 빈들을 찾아서 객체로 만들고 후처리를 해주는 refresh 메소드를 자세히 살펴보았는데, 마지막으로 DispatcherServl
mangkyu.tistory.com
느낀 점
코드 내부적으로 뜯어보며 이해하니 도식화된 그림을 더 이해하기 쉬웠다.
스프링을 공부할수록 잘 모르고 썼던 것 같아 반성이 된다.
하지만 앞으로 더 공부하여 스프링에 대해 깊게 알고, 내 코드가 전체에 미치는 영향을 파악할 수 있는 수준이 될 것이라 믿어 의심치 않는다.
출처(라고 쓰고 더 알아보기라고 읽는다...)
https://www.youtube.com/watch?v=2pBsXI01J6M
https://mangkyu.tistory.com/18
[Spring] Dispatcher-Servlet(디스패처 서블릿)이란? 디스패처 서블릿의 개념과 동작 과정
이번에는 servlet의 심화 또는 대표주자인 dispatcher-servlet에 대해서 알아보도록 하겠습니다. 1. Dispatcher-Servlet(디스패처 서블릿)의 개념 [ Dispatcher-Servlet(디스패처 서블릿) 이란? ] 디스패처 서블릿의
mangkyu.tistory.com
CGI와 서블릿, JSP의 연관관계 알아보기
스프링 프레임워크를 제대로 이해하기 위해 CGI와 서블릿, JSP의 연관관계를 알아봅니다.
velog.io
https://velog.io/@suhongkim98/spring-MVC-%EC%83%9D%EB%AA%85%EC%A3%BC%EA%B8%B0
spring MVC
앞서 Apache, Tomcat, 서블릿, MVC패턴, 프론트 컨트롤러 패턴과 DispatcherServlet에 대해 알아보았다. 첫 게시글부터 쭉 읽어보자..! 이번 시간에는 스프링이 클라이언트의 요청을 어떻게 처리하는지를
velog.io
서블릿 컨테이너(Servlet Container) 란?
서블릿들을 위한 상자(Container) 입니다.
velog.io
https://velog.io/@han_been/Servlet
Servlet
About Servlet.. Servlet이란? > - WebProgramming에서 Client의 요청을 처리 > - 그 결과를 다시 Client에게 전송하는 자바 프로그래밍 기술 > - Servlet 클래스의 구현 규칙을 따른다. Servlet이 해야하는 일 >
velog.io
CGI(Common Gateway Interface) 이해하기 : 웹 페이지를 동적으로 만드는 기술
CGI(Common Gateway Interface) 이해하기 : 웹 페이지를 동적으로 만드는 기술
velog.io
https://velog.io/@effirin/Servlet%EA%B3%BC-JSP%EC%97%90-%EB%8C%80%ED%95%B4
[Web] Servlet과 JSP에 대해
Servlet과 JSP 모두 동적 웹페이지(Dynamic Web Page)를 만들거나 데이터 처리를 수행하기 위해 사용되는 웹 어플리케이션 프로그래밍 기술이다. Servlet은 Tomcat이 이해할수 있는 순수 Java 코드로만 이루
velog.io
https://mangkyu.tistory.com/14
[JSP] 서블릿(Servlet)이란?
1. Servlet(서블릿) 서블릿을 한 줄로 정의하자면 아래와 같습니다. 클라이언트의 요청을 처리하고, 그 결과를 반환하는 Servlet 클래스의 구현 규칙을 지킨 자바 웹 프로그래밍 기술 간단히 말해서,
mangkyu.tistory.com
https://velog.io/@falling_star3/Tomcat-%EC%84%9C%EB%B8%94%EB%A6%BFServlet%EC%9D%B4%EB%9E%80
[Servlet] 서블릿(Servlet)이란?
서블릿의 개념과 동작 과정, 생명주기(메서드), 인터페이스, 서블릿 컨테이너에 대해 공부하고 정리한 내용입니다.
velog.io
Servlet Container 정리
Servlet은 사용자의 요청을 처리해서 동적인 컨텐츠를 응답으로 주기 위한 프로그램이며 Java EE 의 표준 API 중 하나 입니다.javax.servlet.ServletServlet 표준 속에서 요구되는 중요한 점은 전체 데이터 처
velog.io
https://pinkcolor.tistory.com/18
아파치 ? 아파치톰캣? WAS와 Web Server 차이점
자바를 배울때 아파치와 아파치톰캣의차이에 대해 너무 헷갈렸다. 사실 구분하기 어려웠다. 처음 자바를 배워 웹페이지를 띄울때 가장 기본 설치가 아파치 톰캣이었다. 그냥 필수로 설치해야
pinkcolor.tistory.com
'STUDY > Spring' 카테고리의 다른 글
[JPA] 1차 캐시와 2차 캐시 (feat. 스프링 프레임워크 캐시) (0) | 2024.05.08 |
---|---|
[JPA] 영속성 컨텍스트 (0) | 2024.05.08 |
[Spring] 흐름으로 이해해보는 AOP와 프록시 (0) | 2024.03.02 |
[Spring] 프록시와 디자인패턴 (0) | 2024.03.01 |
[Spring] 스프링빈(Spring Bean) (0) | 2024.02.28 |