Tomcat : 사용자 요청을 멀티스레드로 처리하는 방법(2)
이번 장은 init / load 과정을 설명한 Tomcat : 사용자 요청을 멀티스레드로 처리하는 방법(1) 에 이어서
start 메서드의 동작 방식에 대해 알아보겠습니다.
/** Bootstrap.class */
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);
}
load메서드와 마찬가지로 bootstrap의 start 메서드는 catalina의 start 메서드를 호출합니다.
그리고 catalina start 메서드의 가장 핵심 로직은 load를 통해 만들고 초기화시켜놓은 StandardServer의 start() 메서드를 호출하는거에요.
이 StandardServer의 start 메서드를 파헤쳐보면 어떻게 멀티스레딩을 사용하여 사용자 요청을 처리하는지 알 수 있습니다.
설명에 앞서 server의 계층구조를 한번 더 살펴보면 대략적으로 이렇게 생겼습니다.
모든 컴퍼넌트들이 초기화를 위해 init() (or initInternal()) 메서드를 가지고 있었는데요,
마찬가지로 실행을 위해 start() (or startInternal()) 메서드도 모두 가지고 있습니다.
그리고 init 메서드의 연쇄 호출처럼 start 메서드 역시 컴퍼넌트을 연쇄적으로 start() 시킵니다.
컴퍼넌트들이 모두 start() 하면서 서버가 서버다워지는거죠
이번 게시글들은 톰캣이 어떻게 멀티스레딩으로 사용자 요청을 처리하는지에 대해 다루고 있기때문에
이 중 사용자 요청을 처리하는 기능들의 start() 메서드들을 따라가보겠습니다.
이 중 실제로 사용자의 요청을 처리하는 컴퍼넌트는 바로 Connector의 하위 프로퍼티에 있는 NioEndpoint 입니다.
NioEndpoint코드를 자세히 살펴보기 위해 start() 들을 타고타고 들어가보겠습니다.
1. Catalina.class - start() -> this.getServer().start()
/** Catalina.class */
public void start() {
if (this.getServer() == null) {
this.load();
}
try {
this.getServer().start();
} catch (LifecycleException var6) {
/** 생략 */
}
/** 생략 */
}
2. LifecycleBase.class - start() -> this.startInternal() -> StandardServer.class - startInternal() -> services[].start()
/** StandardServer.class */
protected void startInternal() throws LifecycleException {
/** 생략 */
synchronized(this.servicesLock) {
Service[] var2 = this.services;
int var3 = var2.length;
int var4 = 0;
while(true) {
if (var4 >= var3) {
break;
}
Service service = var2[var4];
service.start();
++var4;
}
}
/** 생략 */
}
3. LifecycleBase.class - start() -> this.startInternal() -> StandardService.class - startInternal() -> connector.start()
/** StandardService.class */
protected void startInternal() throws LifecycleException {
/** 생략 (engine, executors, mapperListener start) */
synchronized(this.connectorsLock) {
Connector[] var10 = this.connectors;
int var11 = var10.length;
for(int var4 = 0; var4 < var11; ++var4) {
Connector connector = var10[var4];
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
}
}
}
4. LifecycleBase.class - start() -> this.startInternal() -> Connector.class - startInternal() -> this.protocolHandler.start()
/** Connector.class */
protected void startInternal() throws LifecycleException {
/** 생략 */
this.protocolHandler.start();
/** 생략 */
}
5. Http11NioProtocol.class(AbstractProtocol.class) - start() -> this.endpoint.start()
/** AbstractProtocol.class (Http11NioProtocol.class의 추상클래스) */
public void start() throws Exception {
/** 생략 */
this.endpoint.start();
/** 생략 */
}
6. NioEndpoint(AbstractEndpoint.class) - start() - this.startInternal()
/** NioEndpoint */
public void startInternal() throws Exception {
if (!this.running) {
this.running = true;
this.paused = false;
if (this.socketProperties.getProcessorCache() != 0) {
this.processorCache = new SynchronizedStack(128, this.socketProperties.getProcessorCache());
}
if (this.socketProperties.getEventCache() != 0) {
this.eventCache = new SynchronizedStack(128, this.socketProperties.getEventCache());
}
if (this.socketProperties.getBufferPool() != 0) {
this.nioChannels = new SynchronizedStack(128, this.socketProperties.getBufferPool());
}
if (this.getExecutor() == null) {
// org.apache.tomcat.util.threads.ThreadPoolExecutor를 생성
this.createExecutor();
}
this.initializeConnectionLatch();
this.poller = new NioEndpoint.Poller();
Thread pollerThread = new Thread(this.poller, this.getName() + "-Poller");
pollerThread.setPriority(this.threadPriority);
pollerThread.setDaemon(true);
// 별도의 스레드에서 사용자 요청을 받는 poller Runner 실행
// 이 Poller가 사용자 요청을 받아, ThreadPoolExecutor에서 스레드를 할당받아 처리하는 것을 예상할 수 있습니다.
pollerThread.start();
this.startAcceptorThread();
}
}
보시다시피 server.start() 를 실행하면 연쇄적으로 호출되는 로직 중 하나인 NioEndpoint의 start 로직이 나옵니다.
NioEndpoint의 start에서는 먼저 executor를 생성하는데, 여기서 생성되는 executor가 바로 익숙한 톰캣 스레드풀 엑시큐터입니다.
앞으로 사용자 요청이 오면 NioEndpoint가 이 엑시큐터를 사용해서 스레드를 할당할거라는것을 알 수 있죠.
로직으로 돌아와서 요청을 폴링하는 Poller Runnable 인스턴스를 Thread.start()로 실행시켰습니다.
Poller Runnable 의 run는 메서드는 쓰레드 start()가 호출되면 방금 만들어진 별도의 스레드에서 동작을 하게되겠죠.
로직을 들여다보면 while(true) 내부에서 소켓을 통해 event를 계속 읽고 있음을 알수 있습니다.
루프 중에 event가 존재하면 생성해놓은 톰캣 스레드풀 엑시큐터를 사용해 새로운 스레드에서 이벤트 처리를 진행하도록 로직이 짜여있어요. 이게 톰캣 카타리나 자바 프로그램이 사용자 요청을 멀티스레드로 처리하는 방식입니다.
블로그 글을 통해서 설명할 수 있는 부분은 한정적이기 때문에 생략된 부분이 매우 많습니다.
1장에서도 설명했듯, 톰캣 소스코드를 다운받아 디버깅을 통해 차근차근 따라가 보면 톰캣의 내부 동작에 대해 쉽게 이해할 수 있습니다.
긴 글 읽어 주셔서 감사합니다.