Tomcat : 사용자 요청을 멀티스레드로 처리하는 방법(1)
이전 장에서는 톰캣이 Jasper & Catalina & Coyote 세가지 컴퍼넌트로 이루어져있는 자바 애플리케이션이고, 톰캣을 실행시킨다는 것은 Bootstrap 자바 클래스파일을 실행시키는 것과 같으며 Bootstrap의 main 메서드는 실행 시 init, load, start 메서드를 차례로 실행한다는 것 까지 간단하게 알아봤습니다.
이번 장에서는 init -> load -> start 세가지 메서드를 더 자세히 파헤치며,
톰캣이 사용자 요청을 어떻게 멀티스레드로 처리하는지 확인해보겠습니다.
Init
먼저 init에서는 catalina.class 파일(실질적인 로직들이 구현된 파일)을 가져와 인스턴스화 하는 동작만 합니다.
코드를 통해 보여드리면
package org.apache.catalina.startup;
/** import들 */
public final class Bootstrap {
/** 생략 */
public void init() throws Exception {
// classLoader들 만들기
this.initClassLoaders();
// catalinaLoader로 Catalina class 로드
Thread.currentThread().setContextClassLoader(this.catalinaLoader);
SecurityClassLoad.securityClassLoad(this.catalinaLoader);
if (log.isDebugEnabled()) {
log.debug("Loading startup class");
}
Class<?> startupClass = this.catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
// 로드된 catalina class를 인스턴스화 한 후, catalinaDaemon 변수에 할당
Object startupInstance = startupClass.getConstructor().newInstance();
if (log.isDebugEnabled()) {
log.debug("Setting startup class properties");
}
String methodName = "setParentClassLoader";
Class<?>[] paramTypes = new Class[]{Class.forName("java.lang.ClassLoader")};
Object[] paramValues = new Object[]{this.sharedLoader};
Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
this.catalinaDaemon = startupInstance;
}
/** 생략 (load, start, ... 메서드들 정의) */
public static void main(String[] args) {
synchronized(daemonLock) {
if (daemon == null) {
Bootstrap bootstrap = new Bootstrap();
try {
// init 지점
bootstrap.init();
} catch (Throwable var5) {
handleThrowable(var5);
var5.printStackTrace();
return;
}
daemon = bootstrap;
} else {
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}
// load & start
}
/** 생략 */
}
먼저 클래스로더들을 만듭니다. (클래스 로더를 만들기 위해 catalina.properties 파일 참고)
그리고 그 클래스로더들 중 catalinaLoader를 사용하여 catalina.class를 로딩한 후, 인스턴스로 만듭니다. 그리고 카타리나 데몬 멤버 변수에 저장합니다.
실질적인 주요 로직들은 catalina 클래스에 들어있기 때문에, 이 init 메서드는 해당 catalina를 사용하기 위한 초기화작업이라고 생각하면 됩니다.
Load
이렇게 카탈리나 인스턴스를 만들면 init이 끝나고, load 메서드가 실행이 되는데
bootstrap의 load 메서드는 거의 catalina의 load 메서드를 실행하는 역할만 합니다.
public final class Bootstrap {
/** 생략 */
private void load(String[] arguments) throws Exception {
String methodName = "load";
Object[] param;
Class[] paramTypes;
if (arguments != null && arguments.length != 0) {
paramTypes = new Class[]{arguments.getClass()};
param = new Object[]{arguments};
} else {
paramTypes = null;
param = null;
}
Method method = this.catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled()) {
log.debug("Calling startup class " + method);
}
/** catalina 인스턴스의 load 메서드 invoke */
method.invoke(this.catalinaDaemon, param);
}
private Object getServer() throws Exception {
String methodName = "getServer";
Method method = this.catalinaDaemon.getClass().getMethod(methodName);
return method.invoke(this.catalinaDaemon);
}
public void start() throws Exception {
if (this.catalinaDaemon == null) {
this.init();
}
Method method = this.catalinaDaemon.getClass().getMethod("start", (Class[])null);
method.invoke(this.catalinaDaemon, (Object[])null);
}
public static void main(String[] args) {
/** 생략 (init 메서드 호출) **/
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();
if (null == daemon.getServer()) {
System.exit(1);
}
/** 일반적으로 실행된 서버를 멈출 때 타는 분기점입니다.
} else if (command.equals("stop")) {
daemon.stopServer(args);
}
/** 생략 */
}
}
/** 생략 */
}
그리고 카탈리나 load 메서드의 핵심은 digester라는 객체와 server.xml 파일을 조합하여 저희가 분석할 타겟인 서버라는 객체를 만들어내고 만들어낸 서버를 초기화 하는 역할입니다.
만들어진 서버는 대략 다음과 같이 생겼는데,
Server는 앞으로 톰캣으로 오는 모든 요청에 대한 니즈를 충족하는 복잡한 로직을 가지고 있는 객체이기때문에,
이 서버 객체는 엄격한 규칙에 의해 잘 만들어져야하고 이 규칙들을 Digester가 가지고 있습니다.
load 메서드에서 서버를 만들어내는 부분은 이 parseServerXml 함수입니다.
public class Catalina {
/** 여러 가지 룰을 가진 Digester를 만들어내는 부분 */
protected Digester createStartDigester() {
Digester digester = new Digester();
digester.setValidating(false);
digester.setRulesValidation(true);
Map<Class<?>, List<String>> fakeAttributes = new HashMap();
List<String> objectAttrs = new ArrayList();
objectAttrs.add("className");
fakeAttributes.put(Object.class, objectAttrs);
List<String> contextAttrs = new ArrayList();
contextAttrs.add("source");
fakeAttributes.put(StandardContext.class, contextAttrs);
List<String> connectorAttrs = new ArrayList();
connectorAttrs.add("portOffset");
fakeAttributes.put(Connector.class, connectorAttrs);
digester.setFakeAttributes(fakeAttributes);
digester.setUseContextClassLoader(true);
digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className");
digester.addSetProperties("Server");
digester.addSetNext("Server", "setServer", "org.apache.catalina.Server");
digester.addObjectCreate("Server/GlobalNamingResources", "org.apache.catalina.deploy.NamingResourcesImpl");
digester.addSetProperties("Server/GlobalNamingResources");
digester.addSetNext("Server/GlobalNamingResources", "setGlobalNamingResources", "org.apache.catalina.deploy.NamingResourcesImpl");
digester.addRule("Server/Listener", new ListenerCreateRule((String)null, "className"));
digester.addSetProperties("Server/Listener");
digester.addSetNext("Server/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener");
digester.addObjectCreate("Server/Service", "org.apache.catalina.core.StandardService", "className");
digester.addSetProperties("Server/Service");
digester.addSetNext("Server/Service", "addService", "org.apache.catalina.Service");
digester.addObjectCreate("Server/Service/Listener", (String)null, "className");
digester.addSetProperties("Server/Service/Listener");
digester.addSetNext("Server/Service/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener");
digester.addObjectCreate("Server/Service/Executor", "org.apache.catalina.core.StandardThreadExecutor", "className");
digester.addSetProperties("Server/Service/Executor");
digester.addSetNext("Server/Service/Executor", "addExecutor", "org.apache.catalina.Executor");
digester.addRule("Server/Service/Connector", new ConnectorCreateRule());
digester.addSetProperties("Server/Service/Connector", new String[]{"executor", "sslImplementationName", "protocol"});
digester.addSetNext("Server/Service/Connector", "addConnector", "org.apache.catalina.connector.Connector");
digester.addRule("Server/Service/Connector", new AddPortOffsetRule());
digester.addObjectCreate("Server/Service/Connector/SSLHostConfig", "org.apache.tomcat.util.net.SSLHostConfig");
digester.addSetProperties("Server/Service/Connector/SSLHostConfig");
digester.addSetNext("Server/Service/Connector/SSLHostConfig", "addSslHostConfig", "org.apache.tomcat.util.net.SSLHostConfig");
digester.addRule("Server/Service/Connector/SSLHostConfig/Certificate", new CertificateCreateRule());
digester.addSetProperties("Server/Service/Connector/SSLHostConfig/Certificate", new String[]{"type"});
digester.addSetNext("Server/Service/Connector/SSLHostConfig/Certificate", "addCertificate", "org.apache.tomcat.util.net.SSLHostConfigCertificate");
digester.addObjectCreate("Server/Service/Connector/SSLHostConfig/OpenSSLConf", "org.apache.tomcat.util.net.openssl.OpenSSLConf");
digester.addSetProperties("Server/Service/Connector/SSLHostConfig/OpenSSLConf");
digester.addSetNext("Server/Service/Connector/SSLHostConfig/OpenSSLConf", "setOpenSslConf", "org.apache.tomcat.util.net.openssl.OpenSSLConf");
digester.addObjectCreate("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd", "org.apache.tomcat.util.net.openssl.OpenSSLConfCmd");
digester.addSetProperties("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd");
digester.addSetNext("Server/Service/Connector/SSLHostConfig/OpenSSLConf/OpenSSLConfCmd", "addCmd", "org.apache.tomcat.util.net.openssl.OpenSSLConfCmd");
digester.addObjectCreate("Server/Service/Connector/Listener", (String)null, "className");
digester.addSetProperties("Server/Service/Connector/Listener");
digester.addSetNext("Server/Service/Connector/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener");
digester.addObjectCreate("Server/Service/Connector/UpgradeProtocol", (String)null, "className");
digester.addSetProperties("Server/Service/Connector/UpgradeProtocol");
digester.addSetNext("Server/Service/Connector/UpgradeProtocol", "addUpgradeProtocol", "org.apache.coyote.UpgradeProtocol");
digester.addRuleSet(new NamingRuleSet("Server/GlobalNamingResources/"));
digester.addRuleSet(new EngineRuleSet("Server/Service/"));
digester.addRuleSet(new HostRuleSet("Server/Service/Engine/"));
digester.addRuleSet(new ContextRuleSet("Server/Service/Engine/Host/"));
this.addClusterRuleSet(digester, "Server/Service/Engine/Host/Cluster/");
digester.addRuleSet(new NamingRuleSet("Server/Service/Engine/Host/Context/"));
digester.addRule("Server/Service/Engine", new Catalina.SetParentClassLoaderRule(this.parentClassLoader));
this.addClusterRuleSet(digester, "Server/Service/Engine/Cluster/");
return digester;
}
/** 서버를 만들어 내는 부분 */
protected void parseServerXml(boolean start) {
try {
/** (2) server.xml 파일을 가져온다. */
Resource resource = ConfigFileLoader.getSource().getServerXml();
Throwable var7 = null;
try {
/** (3) digester를 만든다. */
Digester digester = start ? this.createStartDigester() : this.createStopDigester();
InputStream inputStream = resource.getInputStream();
InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
inputSource.setByteStream(inputStream);
digester.push(this);
if (this.generateCode) {
digester.startGeneratingCode();
this.generateClassHeader(digester, start);
}
/** (4) digester와 server.xml로 server 객체를 만들어낸다. */
digester.parse(inputSource);
if (this.generateCode) {
this.generateClassFooter(digester);
FileWriter writer = new FileWriter(new File(serverXmlLocation, start ? "ServerXml.java" : "ServerXmlStop.java"));
Throwable var12 = null;
/** 생략 */
digester.endGeneratingCode();
Digester.addGeneratedClass(xmlClassName);
}
/** 생략 */
}
}
/** 생략 */
}
/** 생략 */
public void load() {
if (!this.loaded) {
this.loaded = true;
long t1 = System.nanoTime();
this.initDirs();
this.initNaming();
/** (1) server를 만들어 낸다. */
this.parseServerXml(true);
Server s = this.getServer();
if (s != null) {
this.getServer().setCatalina(this);
this.getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
this.getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
this.initStreams();
try {
/** (5) 만들어낸 서버 객체를 초기화한다. */
this.getServer().init();
} catch (LifecycleException var5) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new Error(var5);
}
log.error(sm.getString("catalina.initError"), var5);
}
if (log.isInfoEnabled()) {
log.info(sm.getString("catalina.init", new Object[]{Long.toString(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - t1))}));
}
}
}
}
/** 생략 */
}
내부에 들어가서 보시면
(1) load메서드의 핵심인 parseServerXml 메서드를 호출합니다. 이 메서드는 server를 만들어내는 역할을 합니다. 이 때, 중요한 것은 위에서 설명한 듯이 server는 엄청 복잡한 객체이기 때문에 Digester라는 객체를 활용해 만든다는 것입니다.
(2) parseServerXml 메서드에서는 먼저, server.xml 파일을 가져옵니다.
(3) digester 객체를 만듭니다. 코드의 윗부분(createStartDigester)을 보면 알 수 있듯, Digester는 엄청 복잡한 규칙들을 가지고 있습니다.
(4) 해당 digester로 server.xml을 파싱합니다. 이 파싱과 동시에 server가 만들어집니다. 실행시켜보면 파싱전에는 server가 없고 파싱이 끝나고 서버가 할당되는것을 볼수 있습니다.
Digester 객체를 보시면 많은 룰이 들어있는 것을 확인하실 수 있는데, 이 룰들은 서버를 구성하는 여러 컴퍼넌트들을 어떻게 조합할지에 대한 룰이고, 간단히 정리하면 다음과 같습니다.
보시다시피 Server, Service, Context, Engine, Connector 등 어떤 컴퍼넌트들을 조합할지에 대한 규칙정의가 들어있어요.
그리고 각 컴퍼넌트들을 어떻게 생성할지에 대한 룰들이 있습니다.
예를들어서, Server 컴퍼넌트에 대한 ObjectCreateRule은 서버 객체를 만들 때 어떤클래스로 만들지에 대한 규칙입니다.
Server 객체를 만들 때에는 org.apache.catalina.core.StandardServer 클래스를 사용해야한다 같은 규칙이죠.
이러한 규칙들과 server.xml의 정보를 조합하여 다음과 같은 계층구조를 가진 server 객체가 만들어지는 것이죠.
이렇게 만들어진 StandardServer 인스턴스를 초기화하면 bootstrap load() 메서드는 완료됩니다.
여기서 중요한 포인트가 있는데, standardServer 초기화는 자식 컴퍼넌트들의 init을 연쇄적으로 호출하여 초기화하는 것을 말합니다.
초기화는 위 코드의 (5)번을 말하는 것입니다. 코드를 따라가보면
1. Catalina.class - this.getServer().init()
/** Catalina.class */
try {
this.getServer().init();
} /** 생략 */
2. LifecycleBase.class - init() -> StandardServer.initInternal() -> servces[].init()
/** StandardServer.class */
protected void initInternal() throws LifecycleException {
/** 생략 */
for(int var12 = 0; var12 < var11; ++var12) {
Service service = var10[var12];
service.init(); // service는 StandardService입니다. (Digester 규칙에 따라 service 프로퍼티에 StandardService 객체가 들어있음)
}
}
3. LifecycleBase.class - init() -> StandardService.initInternal() -> engine.init() / executors[].init() / connectors[].init()
/** StandardService.class */
protected void initInternal() throws LifecycleException {
super.initInternal();
// engine 초기화
if (this.engine != null) {
this.engine.init();
}
Executor[] var1 = this.findExecutors();
int var2 = var1.length;
// executor들 초기화
int var3;
for(var3 = 0; var3 < var2; ++var3) {
Executor executor = var1[var3];
if (executor instanceof JmxEnabled) {
((JmxEnabled)executor).setDomain(this.getDomain());
}
executor.init();
}
// connector들 초기화
this.mapperListener.init();
synchronized(this.connectorsLock) {
Connector[] var8 = this.connectors;
var3 = var8.length;
for(int var9 = 0; var9 < var3; ++var9) {
Connector connector = var8[var9];
connector.init();
}
}
}
4. 이와 같이 계층구조 tree 속 모든 컴퍼넌트의 init(initInternal) 메서드를 연쇄적으로호출합니다.
이 연쇄 호출은 start() 메서드에서도 마찬가지의 메커니즘을 갖기 때문에 이번 장에서 슬쩍 먼저 설명드렸어요.
이렇게 server 객체를 만들고 초기화하는 역할을 Bootstrap load 메서드가 하게 되고, 이렇게 load 메서드는 끝납니다.
마지막으로 start 메서드는 다음 장에서 이어가도록 하겠습니다.