티스토리 뷰

문제 상황

기존의 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

 

Apache Tomcat® - Which Version Do I Want?

Apache Tomcat® is an open source software implementation of a subset of the Jakarta EE (formally Java EE) technologies. Different versions of Apache Tomcat are available for different versions of the specifications. The mapping between the specifications

tomcat.apache.org

공식 페이지에서 안내하는 11버전 의존성

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 클래스의 메서드 목록

IntelliJ를 이용하여 메서드 목록 확인

 

NioEndpoint 클래스의 이름에서부터 티가 난다. 엔드포인트 역할이다.

해당 클래스에서 소켓 관련한 기능을 설정하는 것을 확인할 수 있었다. 

 

클래스 다이어그램

 

 

 

Remark

정리해 보자. 

Tomcat 클래스의 start() 메서드에서 Server 멤버변수가 start()를 호출하면서 소켓을 열어두지 않았기 때문에 발생한 이슈였다.

 

cf) 라이프사이클을 확인해 보면 아래와 같다.

org.apache.catalina.Lifecycle

Lifecycle 인터페이스의 주석

 

왜 이런 변화가 생겼을까?

해당 변화는 8.5.x 버전에서 9.0.x 버전으로 넘어가면서 생겼다.

https://github.com/apache/tomcat/blob/8.5.x/java/org/apache/catalina/startup/Tomcat.java#L394

 

tomcat/java/org/apache/catalina/startup/Tomcat.java at 8.5.x · apache/tomcat

Apache Tomcat. Contribute to apache/tomcat development by creating an account on GitHub.

github.com

https://github.com/apache/tomcat/blob/9.0.x/java/org/apache/catalina/startup/Tomcat.java#L436

 

tomcat/java/org/apache/catalina/startup/Tomcat.java at 9.0.x · apache/tomcat

Apache Tomcat. Contribute to apache/tomcat development by creating an account on GitHub.

github.com

 

tomcat 공식 페이지의 released 노트를 확인해도, 이유에 대해서는 직접적인 언급은 딱히 찾지 못했다. 

 

https://tomcat.apache.org/

 

Apache Tomcat® - Welcome!

The Apache Tomcat® software is an open source implementation of the Jakarta Servlet, Jakarta Pages, Jakarta Expression Language, Jakarta WebSocket, Jakarta Annotations and Jakarta Authentication specifications. These specifications are part of the Jakarta

tomcat.apache.org

 

100% 추측을 해보자면, 2가지를 떠올릴 것 같다.

  • 책임을 클라이언트 코드로 옮김
  • 아키텍처의 변화로 다른 방식으로 소켓을 생성하는 방식을 추구하는 방향성이지 않을까 싶다.

정확한 이유를 아시는 분은 댓글로 알려주시면 감사하겠습니다

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/01   »
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
글 보관함