지금까지 자바 서버사이드 웹 개발을 연습해오면서 단 한번도 클라이언트 - 서버간의 통신을 위한 연결 관리를 직접적으로 해본 경험은 거의 드물었던것 같다. 이는 내가 이것을 사용해왔는지에 대해 인지하기 조차 힘들정도로 웹서비스를 구현하기 위해서는 당연하게 WAS를 이용해왔기 때문이다(대표적으로 Tomcat).
WAS는 클라이언트와 서버간의 소켓 통신에 필요한 TCP/IP 연결관리와 HTTP 프로토콜 해석과 같은 네트워크 기반 작업을 추상화 하여 실행환경을 제공한다. 즉 개발자는 네트워크 연결에 관한 작업과정을 생략하고 서비스 로직을 구현하는것 만으로 쉽게 웹을 구현할 수 있었던 것이다.
결국 현재까지의 나는 관련 기술을 제대로 이해하지 못한 채로 WAS가 제공하는 추상화된 API로 네트워크에 접근해왔던 것이다. 지금까지는 WAS를 당연하게 사용해왔지만 애석하게도 이번 스타터스 부트캠프 교육기간에 기반 기술을 제대로 이해하지 못한체로 넘어 가게된다면 복습의 의미가 없을것이기 때문에 가능한한 최대한 상세히 WAS를 씹고 뜯어서 이해해보고자 한다.
javax(jakarta).servlet.Servlet
인터페이스를 구현한 것이 서블릿이다.GenericServlet
이라는 추상클래스를 제공한다init()
메서드가 한번 실행된다. 이후에는 인스턴스가 메모리에 상주하게 되고 서블릿 종료시(WAS 실행종료 및 재배포) destory()
실행HttpServlet
은 GenericServlet
을 상속받으며 일반적으로 서블릿이라 하면 거의 대부분 이 HttpServlet
을 상속받은 서블릿을 의미한다.GenericServlet
의 유일한 추상 메서드인 service를 HTTP 프로토콜 요청 메서드에 적합하게 재구현한 것이다.service()
를 호출한다. 그러면 클래스 상속 위계에 따라 그 처리가 부모 클래스인 GenericServlet
에서 자식 클래스인 HttpServlet
으로 넘어온다. HttpServlet의 service 메서드 내에서는 HTTP 요청 메서드에 의해 여러 do…()
메서드로 분기되어 처리된다.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 = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// 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);
}
}
아파치 톰캣은 대표족인 오픈소스 서블릿 컨테이너이다.
user@DESKTOP-J26ER00:/mnt/c/Program Files/Tomcat 9.0$ tree . -L 2
.
├── LICENSE
├── NOTICE
├── RELEASE-NOTES
├── Uninstall.exe
├── bin
│ ├── Tomcat9.exe
│ ├── Tomcat9w.exe
│ ├── **bootstrap.jar
│ ├── **catalina.sh(catalina.bat)**
│ ├── ciphers.bat
│ ├── configtest.bat
│ ├── digest.bat
│ ├── makebase.bat
│ ├── service.bat
│ ├── setclasspath.bat
│ ├── shutdown.bat
│ ├── **startup.sh(startup.bat)**
│ ├── **tomcat-juli.jar**
│ ├── tool-wrapper.bat
│ └── version.bat
├── conf
│ ├── Catalina
│ ├── catalina.policy
│ ├── catalina.properties
│ ├── context.xml
│ ├── jaspic-providers.xml
│ ├── jaspic-providers.xsd
│ ├── logging.properties
│ ├── server.xml
│ ├── tomcat-users.xml
│ ├── tomcat-users.xsd
│ └── web.xml
├── lib
│ ├── annotations-api.jar
│ ├── catalina-ant.jar
│ ├── catalina-ha.jar
│ ├── catalina-ssi.jar
│ ├── catalina-storeconfig.jar
│ ├── catalina-tribes.jar
│ ├── catalina.jar
│ ├── ecj-4.20.jar
│ ├── el-api.jar
│ ├── jasper-el.jar
│ ├── jasper.jar
│ ├── jaspic-api.jar
│ ├── jsp-api.jar
│ ├── servlet-api.jar**
│ ├── tomcat-api.jar
│ ├── tomcat-coyote.jar
│ ├── tomcat-dbcp.jar
│ ├── tomcat-i18n-cs.jar
│ ├── tomcat-i18n-de.jar
│ ├── tomcat-i18n-es.jar
│ ├── tomcat-i18n-fr.jar
│ ├── tomcat-i18n-ja.jar
│ ├── tomcat-i18n-ko.jar
│ ├── tomcat-i18n-pt-BR.jar
│ ├── tomcat-i18n-ru.jar
│ ├── tomcat-i18n-zh-CN.jar
│ ├── tomcat-jdbc.jar
│ ├── tomcat-jni.jar
│ ├── tomcat-util-scan.jar
│ ├── tomcat-util.jar
│ ├── tomcat-websocket.jar
│ └── websocket-api.jar
├── logs
...
├── temp
├── tomcat.ico
├── webapps
│ ├── ... (웹 애플리케이션 프로젝트별 클래스패스에서 컴파일된 클래스들을 war파일로 배포됨)
└── work
└── Catalina
bin/
내부는 톰켓 엔진에 해당한다/webapps
는 애플리케이션을 배치하는 위치다./logs
는 톰캣이 로그를 출력하는 위치다. 톰캣의 로그는 기본적으로 /logs/catalina.out으로 이동한다. 이 파일을 사용해 애플리케이션별 로그 파일과 함께 문제를 디버깅할 수 있다./lib
은 톰캣서버 애플리케이션이 사용하는 라이브러리들에 대한 JAR을 찾는 위치다./conf
는 톰캣용 설정 XML이며, 톰캣에 사용자 및 역할을 추가할 수 있다.톰캣 서버가 실행되기 위해서는 서버 구동 스크립트 startup.sh(bat)
이 실행되고 해당 스크립트가 톰켓 엔진 실행을 위한 스크립트파일 catalina.sh
혹은 catalina.bat
를 실행한다.
catalina.sh
# Add on extra jar files to CLASSPATH
if [ ! -z "$CLASSPATH" ] ; then
CLASSPATH="$CLASSPATH":
fi
CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar
if [ -z "$CATALINA_OUT" ] ; then
CATALINA_OUT="$CATALINA_BASE"/logs/catalina.out
fi
if [ -z "$CATALINA_TMPDIR" ] ; then
# Define the java.io.tmpdir to use for Catalina
CATALINA_TMPDIR="$CATALINA_BASE"/temp
fi
# Add tomcat-juli.jar to classpath
# tomcat-juli.jar can be over-ridden per instance
if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar
else
CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar
fi
bootstrap.jar
, tomcat-juli.jar
)만 클래스패스에 지정되어 있으므로 실행하고자 하는 웹애플리케이션에 대핸 클래스들을 로딩하는 과정이 필요하다/webapps
디렉토리 아래에서 웹 애플리케이션을 탐색한다.톰켓 서블릿 컨테이너가 실행되는 메인메서드(Bootstrap.main()
)의 실행 로직을 살펴보자
톰켓 서블릿 컨테이너 시작클래스
Bootstrap.class
/**
* Main method and entry point when starting Tomcat via the provided
* scripts.
*
* @param args Command line arguments to be processed
*/
public static void main(String args[]) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to prevent
// a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}
if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null==daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}
init()
메서드를 호출해 초기화한다.Bootstrap
인스턴스를 할당한다.init()
/**
* Initialize daemon.
* @throws Exception Fatal initialization error
*/
public void init() throws Exception {
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader);
// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass =
catalinaLoader.loadClass
("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.newInstance();
// Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
catalinaDaemon = startupInstance;
}
initClassLoaders()
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
initClassLoader()
는 모두 세개의 클래스로더를 생성한다Thread.currentThread().setContextClassLoader(catalinaLoader)
/lib
아래의 클래스도 로드할 수 있게 된다.)WEB-INF/classes
와 WEB-INF/lib
에 있는 모든 클래스들과 JAR 파일들을 순서대로 로드한다. 이러한 리소스들은 그것들을 로드한 웹 애플리케이션에서만 볼 수 있다.$CATALINA_HOME/lib
에 있는 모든 클래스들과 JAR 파일들을 로드한다. 이러한 리소스들은 톰캣과 모든 애플리케이션에서 볼 수 있다.톰캣 서블릿 컨테이너를 사용한 경험이 있는 웹 프로그래머라면 한 번쯤 config 디렉토리 안에 있는 XML 파일이 어떤 의미가 있으며, 어떤 시점에 어떻게 로딩되는지 의문을 가졌을 것이라 생각한다. 이런 설정 값을 읽어들이는 과정은 바로 Catalina 클래스의 load 메서드에서 찾아볼 수 있다.
/**
* Start a new server instance.
*/
public void load() {
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Create and execute our Digester
Digester digester = createStartDigester(); // 설정 파일을 개체화해 접근할 수 있도록 한다.
InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile(); //conf/server.xml
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
…
server.xml
![]