Client 측 에서 http로 요청한 page 또는 Data를 받기위해서 WebServer가 필요하다.
하지만 spring boot를 사용하며 실제로 WebServer가 어떻게 동작하는지 정확하게 어떻게 구동되는지에 대한 궁금증과 servlet의 생명주기 관리 및 Dispatcher Servlet이 어떻게 등록되는지 확실하게 알아보기 위해 해당 주제를 공부하게 되었다.
우선 servlet의 생명주기에 대하여 자세히 알아보자!
아래의 그림과 같이 Add, init, service, distroy 4단계로 크게 볼 수 있다.
이렇한 servlet을 servlet context가 관리해 주며 tomcat 위에서 구동 된다.
그럼 spring boot는 어떻게 이 Tomcat을 내장시킨 것 일까?
코드로 보는 것이 빠를 것 같아 직접 구현해 보았다.
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>11.0.0-M26</version>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
2개의 Dependency를 사용한다.
tomcat-embed-core의 경우 이름 그대로 어플리케이션 내부에서 위한 tomcat을 구동하기 위한 dependency이다.
이것을 통해 tomcat에 여러 설정 및 실행을 할 수 있게하는 것 이었다.
javax.servlet-api의 경우 servlet을 사용하기 위한 dependency이며 java.EE에서 web을 개발하기 위한 모듈로 제공하고 있다.
그럼 실재 코드로 tomcat을 내장시켜 구동해 보자 우선 그냥 tomcat만 구동하는 것 보다 브라우저로 실재 응답하는 것을 확인해 보는게 좋을 것 같아 아래의 요구사항을 구현해 보겠다.
package org.framework.controller;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public interface Controller {
void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
요청을 받고 응답할 수 있다록 controller를 아래와 같이 정의한다.
package org.framework.servlet;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.autumnframework.controller.Controller;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class DispatcherServlet extends HttpServlet {
private static Map<String, Controller> controllerMap = new HashMap<>();
public void addController(String path, Controller controller) {
controllerMap.put(path, controller);
System.out.println("Controller added for path: " + path);
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("DispatcherServlet: service method called");
String path = req.getRequestURI();
System.out.println("Received request for path: " + path);
Controller controller = controllerMap.get(path);
if (controller != null) {
controller.handleRequest(req, resp);
} else {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
@Override
public void init() throws ServletException {
System.out.println("DispatcherServlet initialized");
super.init();
}
}
특정 path에 대하여 controller들을 등록할 수 있도록 Dispatcher Servlet을 아래와 같이 구현하다.
controllerMap은 path를 key로 갖고 controller를 value로 같는 Map이다.
그러므로 어떤 path로 요청이 들어오면 해당 map에서 controller를 찾아 만약 해당 path에 대응되는 controller가 있다면 요청을 수행하고 없다면 404응답을 보내게 된다.
package org.framework.tomcat;
import java.io.File;
import java.io.IOException;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.servlets.DefaultServlet;
import org.apache.catalina.startup.Tomcat;
import org.apache.coyote.http11.Http11NioProtocol;
import org.autumnframework.controller.Controller;
import org.autumnframework.servlet.DispatcherServlet;
public class EmbeddedTomcatServer {
private int port;
private DispatcherServlet dispatcherServlet;
public EmbeddedTomcatServer(int port) {
this.port = port;
this.dispatcherServlet = new DispatcherServlet();
}
public void addController(String path, Controller controller) {
dispatcherServlet.addController(path, controller);
}
public void start() throws LifecycleException {
Tomcat tomcat = new Tomcat();
// 커넥터 설정을 상세히 구성
Connector connector = new Connector(Http11NioProtocol.class.getName());
connector.setPort(port);
connector.setProperty("bindOnInit", "false");
tomcat.setConnector(connector);
String docBase = new File(".").getAbsolutePath();
Context context = tomcat.addContext("", docBase);
// DispatcherServlet 등록
Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet);
context.addServletMappingDecoded("/*", "dispatcherServlet");
tomcat.start();
System.out.println("Server started on port " + port);
// 서버가 실행 중인지 확인
if (tomcat.getServer().getState().isAvailable()) {
System.out.println("Server is running and available");
} else {
System.out.println("Server is not available. State: " + tomcat.getServer().getState());
}
// 포트가 실제로 열려있는지 확인
try {
new java.net.Socket("localhost", port).close();
System.out.println("Port " + port + " is open and accessible");
} catch (IOException e) {
System.out.println("Port " + port + " is not accessible. Error: " + e.getMessage());
}
tomcat.getServer().await();
}
}
이제 최종 목적인 Tomcat을 설정을 정의하고 구동 함수를 실행하면 구동 할수 있게 함수를 아래와 같이 구현한다.
<start 함수>
1. 동할 포트를 설정한다.
2. context Path를 설정한다.
3. context에 DispetcherServlet을 등록한다.
4. tomcat을 구동한다.
package org.framework;
import java.io.IOException;
import org.apache.catalina.LifecycleException;
import org.autumnframework.controller.Controller;
import org.autumnframework.tomcat.EmbeddedTomcatServer;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class Main {
public static void main(String[] args) {
EmbeddedTomcatServer server = new EmbeddedTomcatServer(8000);
// Controller 등록
server.addController("/hello", new Controller() {
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html");
response.getWriter().write("<h1>Hello from Controller!</h1>");
}
});
try {
server.start();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
마지막으로 controller를 등록하고 어플레케이션을 실행하면 tomcat이 구동 된다.
그럼 http://localhost:8000/hello로 접속하여 잘 동작하는지 확인해 보자!!
잘동작한다!!!
어떻게 spring boot에 tomcat이 내장되고 설정되는지 확인해 보것 뿐인데 servlet기반의 웹의 동작원리에 대하여 좀더 이해도가 올라간것 같다.
또한 WebServer의 다른 종류인 netty에 대한 호기심도 생겨버렸다.
다음에는 netty에 대하여 공부해 볼 것 같다.
또한, 정말 간단한 웹 어플리케이션이라면 그냥 spring boot를 사용하는 것 보다 이렇게 tomcat과 간단한 DispatcherServlet, Controller와 같이 그냥 필요한 부분을 직접 구현하면 더 경량화된 어플리케이션을 만들 수 있지 않을까? 라는 생각도 해보았다.
간단한 프로젝트를 할 기회가 된다면 spring boot를 사용하지 않고 그냥 필요한 부분을 직접 넣어 구현해 보아야 겠다.
오늘은 여기까지~~~