티스토리 뷰
문제 상황
기존의 Tomcat 8 버전에서는 정상적으로 동작하던 코드가, Tomcat 11 버전에서는 작동하지 않았다.
프로그램이 프로세스로써 기동은 되지만, 외부에서 해당 프로세스에 접근되지 않았다.
Tomcat 8 버전에서 정상 동작하는 기동 코드이다.
import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class WebApplicationServer {
private static final Logger log = LoggerFactory.getLogger(WebApplicationServer.class);
public static void main(String[] args) throws Exception {
String webappDirLocation = "webapps/";
Tomcat tomcat = new Tomcat();
tomcat.setPort(8083);
tomcat.addWebapp("", new File(webappDirLocation).getAbsolutePath());
log.info("configuring app with basedir: {}", new File(webappDirLocation).getAbsolutePath());
tomcat.start();
tomcat.getServer().await();
}
}
그러나 동일한 코드로 Tomcat 11 버전을 사용할 때는 서버가 제대로 동작하지 않고, 브라우저에서 접근할 수 없었다. 서버가 시작되었음에도 불구하고 포트로 접근할 수 없었다.
해결 방법
tomcat.getConnector();
위 코드를 await() 호출 이전에 추가해 주는 것이다.
수정된 코드 (정상 기동)
import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class WebApplicationServer {
private static final Logger log = LoggerFactory.getLogger(WebApplicationServer.class);
public static void main(String[] args) throws Exception {
String webappDirLocation = "webapps/";
Tomcat tomcat = new Tomcat();
tomcat.setPort(8083);
tomcat.addWebapp("", new File(webappDirLocation).getAbsolutePath());
log.info("configuring app with basedir: {}", new File(webappDirLocation).getAbsolutePath());
tomcat.start();
tomcat.getConnector();
tomcat.getServer().await();
}
}
환경 설정
11버전
build.gradle.kts 중 dependencies 일부
dependencies {
implementation("org.apache.tomcat.embed:tomcat-embed-core:11.0.0-M19")
implementation("org.apache.tomcat.embed:tomcat-embed-jasper:11.0.0-M19")
implementation("jakarta.servlet:jakarta.servlet-api:6.1.0")
implementation("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0")
implementation("org.glassfish.web:jakarta.servlet.jsp.jstl:3.0.0")
}
https://tomcat.apache.org/whichversion.html
8버전
build.gradle 중 dependencies 일부
dependencies {
implementation 'org.apache.tomcat.embed:tomcat-embed-core:8.5.42'
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:8.5.42'
implementation 'javax.servlet:jstl:1.2'
implementation 'javax.servlet:javax.servlet-api:4.0.1'
}
원인 분석
로그 분석
만약, 정상적인 기동이 성공했다면 아래와 같은 로그를 출력한다.
5월 14, 2024 10:45:24 오후 org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
5월 14, 2024 10:45:24 오후 org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/11.0.0-M19]
5월 14, 2024 10:45:24 오후 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
INFO: No global web.xml found
5월 14, 2024 10:45:25 오후 org.apache.jasper.servlet.TldScanner scanJars
INFO: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
5월 14, 2024 10:45:25 오후 org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8083"]
5월 14, 2024 10:45:25 오후 org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-nio-8083"]
하지만, 문제상황에서는 아래와 같은 로그를 출력했다.
5월 14, 2024 11:04:32 오후 org.apache.catalina.core.StandardService startInternal
INFO: Starting service [Tomcat]
5월 14, 2024 11:04:32 오후 org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet engine: [Apache Tomcat/11.0.0-M19]
5월 14, 2024 11:04:32 오후 org.apache.catalina.startup.ContextConfig getDefaultWebXmlFragment
INFO: No global web.xml found
5월 14, 2024 11:04:32 오후 org.apache.jasper.servlet.TldScanner scanJars
INFO: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
로그의 차이를 보면 2군데와 관련하여 문제를 분석하면 될 것 같다.
- org.apache.coyote.AbstractProtocol init
- org.apache.coyote.AbstractProtocol start
코드 분석
Tomcat 클래스 소스를 분석했다. Tomcat 11 버전과 Tomcat 8 버전에서 start() 메서드에서 차이가 있음을 확인했다.
Tomcat 8에서는 start() 메서드 내부에서 커넥터를 생성하고 추가했지만, Tomcat 11 버전에서는 그러지 않았다.
Tomcat 11 버전의 Tomcat 클래스 중 일부
org.apache.catalina.startup.Tomcat
public class Tomcat {
protected Server server;
public void start() throws LifecycleException {
getServer();
server.start();
}
public Connector getConnector() {
Service service = getService();
if (service.findConnectors().length > 0) {
return service.findConnectors()[0];
}
// The same as in standard Tomcat configuration.
// This creates a NIO HTTP connector.
Connector connector = new Connector("HTTP/1.1");
connector.setPort(port);
service.addConnector(connector);
return connector;
}
}
Tomcat 8 버전의 Tomcat 클래스 중 일부
org.apache.catalina.startup.Tomcat
public class Tomcat {
protected Server server;
protected boolean defaultConnectorCreated = false;
public void start() throws LifecycleException {
getServer();
getConnector();
server.start();
}
public Connector getConnector() {
Service service = getService();
if (service.findConnectors().length > 0) {
return service.findConnectors()[0];
}
if (defaultConnectorCreated) {
return null;
}
// The same as in standard Tomcat configuration.
// This creates an APR HTTP connector if AprLifecycleListener has been
// configured (created) and Tomcat Native library is available.
// Otherwise it creates a NIO HTTP connector.
Connector connector = new Connector("HTTP/1.1");
connector.setPort(port);
service.addConnector(connector);
defaultConnectorCreated = true;
return connector;
}
}
start() 메서드에서 getConnector() 호출이 사라진 것을 확인할 수 있다.
따라서, Tomcat 11 버전에서는 이를 클라이언트 코드에서 명시적으로 호출해줘야 한다.
getConnector() 메서드를 따라가 보자. Tomcat 11 버전을 기준으로 따라갔다.
Connector 클래스
Connector 클래스 중 일부
org.apache.catalina.connector.Connector
public class Connector extends LifecycleMBeanBase {
private static final Log log = LogFactory.getLog(Connector.class);
public Connector(String protocol) {
configuredProtocol = protocol;
ProtocolHandler p = null;
try {
p = ProtocolHandler.create(protocol);
} catch (Exception e) {
log.error(sm.getString("coyoteConnector.protocolHandlerInstantiationFailed"), e);
}
if (p != null) {
protocolHandler = p;
protocolHandlerClassName = protocolHandler.getClass().getName();
} else {
protocolHandler = null;
protocolHandlerClassName = protocol;
}
// Default for Connector depends on this system property
setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));
}
// ----------------------------------------------------- Instance Variables
/**
* Name of the protocol that was configured.
*/
protected final String configuredProtocol;
/**
* The string manager for this package.
*/
protected static final StringManager sm = StringManager.getManager(Connector.class);
/**
* Coyote protocol handler.
*/
protected final ProtocolHandler protocolHandler;
/**
* Coyote Protocol handler class name. See {@link #Connector()} for current default.
*/
protected final String protocolHandlerClassName;
}
ProtocolHandler 인터페이스
ProtocolHandler 인터페이스 중 일부
org.apache.coyote.ProtocolHandler
public interface ProtocolHandler {
static ProtocolHandler create(String protocol)
throws ClassNotFoundException, InstantiationException, IllegalAccessException, IllegalArgumentException,
InvocationTargetException, NoSuchMethodException, SecurityException {
if (protocol == null || "HTTP/1.1".equals(protocol) ||
org.apache.coyote.http11.Http11NioProtocol.class.getName().equals(protocol)) {
return new org.apache.coyote.http11.Http11NioProtocol();
} else if ("AJP/1.3".equals(protocol) ||
org.apache.coyote.ajp.AjpNioProtocol.class.getName().equals(protocol)) {
return new org.apache.coyote.ajp.AjpNioProtocol();
} else {
// Instantiate protocol handler
Class<?> clazz = Class.forName(protocol);
return (ProtocolHandler) clazz.getConstructor().newInstance();
}
}
}
Http11NioProtocol 클래스
Http11NioProtocol 클래스 중 일부
org.apache.coyote.http11.Http11NioProtocol
public class Http11NioProtocol extends AbstractHttp11Protocol<NioChannel> {
public Http11NioProtocol() {
this(new NioEndpoint());
}
}
NioEndpoint 클래스
NioEndpoint 클래스 중 일부
org.apache.tomcat.util.net.NioEndpoint
public class NioEndpoint extends AbstractNetworkChannelEndpoint<NioChannel, SocketChannel> {
// ----------------------------------------------------------------- Fields
/**
* Server socket "pointer".
*/
private volatile ServerSocketChannel serverSock = null;
/**
* Stop latch used to wait for poller stop
*/
private volatile CountDownLatch stopLatch = null;
/**
* Cache for poller events
*/
private SynchronizedStack<PollerEvent> eventCache;
/**
* Bytebuffer cache, each channel holds a set of buffers (two, except for SSL holds four)
*/
private SynchronizedStack<NioChannel> nioChannels;
private SocketAddress previousAcceptedSocketRemoteAddress = null;
private long previousAcceptedSocketNanoTime = 0;
}
NioEndpoint 클래스의 메서드 목록
NioEndpoint 클래스의 이름에서부터 티가 난다. 엔드포인트 역할이다.
해당 클래스에서 소켓 관련한 기능을 설정하는 것을 확인할 수 있었다.
클래스 다이어그램
Remark
정리해 보자.
Tomcat 클래스의 start() 메서드에서 Server 멤버변수가 start()를 호출하면서 소켓을 열어두지 않았기 때문에 발생한 이슈였다.
cf) 라이프사이클을 확인해 보면 아래와 같다.
org.apache.catalina.Lifecycle
왜 이런 변화가 생겼을까?
해당 변화는 8.5.x 버전에서 9.0.x 버전으로 넘어가면서 생겼다.
https://github.com/apache/tomcat/blob/8.5.x/java/org/apache/catalina/startup/Tomcat.java#L394
https://github.com/apache/tomcat/blob/9.0.x/java/org/apache/catalina/startup/Tomcat.java#L436
tomcat 공식 페이지의 released 노트를 확인해도, 이유에 대해서는 직접적인 언급은 딱히 찾지 못했다.
100% 추측을 해보자면, 2가지를 떠올릴 것 같다.
- 책임을 클라이언트 코드로 옮김
- 아키텍처의 변화로 다른 방식으로 소켓을 생성하는 방식을 추구하는 방향성이지 않을까 싶다.
정확한 이유를 아시는 분은 댓글로 알려주시면 감사하겠습니다
- Total
- Today
- Yesterday
- core c++
- reader-writer lock
- generic sort
- CPU
- servlet
- PS
- 객체 변조 방어
- tree
- generic swap
- 엔티티 설계 주의점
- 백준
- 논문추천
- condition variable
- thread
- C
- Spring MVC
- S4
- pocu
- tomcat11
- S1
- Dispatcher Servlet
- sleep lock
- Memory
- 이진탐색
- 개발 공부 자료
- JPA
- OOP
- Java
- 톰캣11
- 연관관계 편의 메서드
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |