ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아파치 톰캣 내부구조 (Apache Tomcat Internals)
    Java 2021. 2. 17. 18:53
    반응형
     

    컨텍스트를 이해하며 알아보는 Nginx 내부구조

    아파치 톰캣 내부구조 (Apache Tomcat Internals) 아파치 톰캣은 Java Servlet, JavaServer Pages, Java Expression Language와 WebSocket 기술의 오픈소스 구현체로, Java 코드가 실행될 수 있는 "pure Java" HTTP Web server 환경을

    kadensungbincho.tistory.com

     

    아파치 톰캣은 Java Servlet, JavaServer Pages, Java Expression Language와 WebSocket 기술의 오픈소스 구현체로, Java 코드가 실행될 수 있는 "pure Java" HTTP Web server 환경을 제공합니다 [1].

     

    웹 서버 역할을 하는 중심에는 톰캣이 서블릿 컨테이너(Servlet Container, 또는 Engine으로도 불림)라는 부분에 기반합니다. 그렇기에 사용자는 소켓 처리, HTTP 요청을 설정된 Servlet에 연결 등의 공통적인 사항을 간단한 설정을 통해 톰캣이 담당하도록 할 수 있습니다. 그리고 사용자는 Servlet을 통해 구현하는 비지니스 로직과 같은 부분에 집중할 수 있게 됩니다.

     

    이번 글에서는 톰캣의 내부구조를 컴포넌트별로 알아보고, 관련 코드도 살펴보도록 하겠습니다.

     

    • 서블릿 컨테이너란?
    • 톰캣 아키텍쳐

    서블릿 컨테이너란? (Java Servlet Specification 4.0 기준)

    서블릿 컨테이너는 클라이언트로부터 받은 요청들을 실행하고, 그 요청들에 기반한 응답을 리턴하는 객체입니다 [3]. 서블릿 컨테이너는 '요청과 응답이 보내지고, MIME-기반의 요청 복호화 및 응답을 생성하는 네트워크 서비스'를 제공하는 웹 서버 또는 애플리케이션 서버의 일부로 존재합니다 [2]. 또한, 서블릿 컨테이너는 서블릿을 가지며 서블릿 라이프사이클을 통해 서블릿을 관리합니다. 

     

    서블릿 컨테이너는  호스트 웹 서버 안에 만들어지거나, 웹 서버의 확장 API를 통해서 add-on 컴포넌트로 설치될 수 있습니다. 또한, 서블릿 컨테이너는 웹-enabled 애플리케이션 서버 안에 구성되거나 설치될 수 있습니다.

     

    모든 서블릿 컨테이너는 요청과 응답을 위한 프로토콜로 반드시 HTTP를 지원해야 합니다. 그러나 HTTPS와 같은 추가적인 요청/응답 기반의 프로토콜 역시 지원할 수 있습니다. 컨테이너가 반드시 구현하여야 하는 HTTP 명세 버젼은 HTTP/1.1과 HTTP/2입니다. HTTP/2를 지원할 때에는, 서블릿 컨테이너는 반드시 'h2'와 'h2c' 프로토콜 identifiers를 지원해야 합니다. 

     

    컨테이너는 캐싱 메커니즘을 지원하기에, 요청을 변형하여 서블릿에 전달하고, 받은 응답을 다시 변형하여 클라이언트에 전달하거나, 그러한 기능을 지원하지 않는 요청의 경우 RFC 7234를 따르는 서블릿을 통해 처리할 수 있습니다. 

     

    그리고 서블릿 컨테이너는 서블릿이 실행되는 환경의 security 제약을 가합니다. 예로, 몇몇의 애플리케이션 서버는 컨테이너 컴포넌트가 영향을 받지 않도록 쓰레드 생성 갯수를 제한합니다.

     

    그러면 위와 같은 서블릿 컨테이너 명세가 톰캣에는 어떻게 구현되어 있는지 구조를 살펴보며 확인해보겠습니다.

     

    톰캣 아키텍쳐

    톰캣은 아래와 같이 컴포넌트가 nested된 위계구조를 이루고 있습니다. 이러한 컴포넌트 중 일부는 서로 단단한 관계를 가지며 컴포넌트 위계주고의 최상위에 존재하기에 top-level  컴포넌트라 불립니다. 컨테이너(위 서블릿 컨테이너와 다른 용어)는 컴포넌트로 다른 컴포넌트들을 포함하고 있습니다. 컨테이너들 안에 존재하며 스스로 다른 컴포넌트를 포함하지 못하는 컴포넌트는 nested 컴포넌트라고 불립니다. 

    Image from Author inspired by [4]

    위 이미지는 한 서버의 완전한 토폴로지이지만, 성능에 영향을 미치지 않고 특정 객체는 생략될 수도 있습니다. 예로, 외부 웹 서버(Apache 같은)가 웹 애플리케이션에 요청을 resolve해준다면 엔진과 호스트는 불필요하게 됩니다. 

     

    위 이미지에서 여러 개가 존재하는 객체는 실행 시 여러 인스턴스가 존재할 수 있습니다: Logger, Valve, Host, 그리고 Context. 코넥터의 경우 특징을 담기 위해 따로 나타냈습니다 [4]. 

     

    The Server

    서버는 톰캣 그 자체로 웹 애플리케이션 서버의 인스턴스이며 top-level 컴포넌트입니다. 서버는 서버를 종료하기 위한 한 개의 포트를 가지며, 디버그 모드 설정을 통해 JVM 디버깅을 시작할 수 있습니다.

     

     하나의 머신에 애플리케이션을 분리하여 각각 재시작할 수 있도록 각각 서버들을 설정할 수도 있습니다. 또한, 하나의 JVM에서 실행되는 한 서버에서 오류가 발생하더라도 다른 서버에는 영향을 주지 않고 분리되어 운영될 수 있습니다. 

     

    The Service

    서비스는 하나의 컨테이너(주로 엔진)를 컨테이너의 커넥터들과 묶는 top-level 컴포넌트입니다. 각 서비스에서 발생하는 로그메시지를 관리자가 쉽게 알아볼 수 있도록, 각 서비스에는 이름이 주어집니다.

     

    The Connectors 

    커넥터는 애플리케이션을 클라이언트와 연결합니다. 커넥터는 어느 접점(point)에서 클라이언트로부터의 요청이 받아지는지 나타내며, 서버 상의 포트를 할당합니다. Nonsecure HTTP 애플리케이션을 위한 디폴트 포트는 기존 웹 서버의 80과 충돌을 피하기 위해 8080입니다만, 설정을 통해 쉽게 변경할 수 있습니다. 여러 개의 커넥터가 하나의 엔진 또는 엔진-레벨 컴포넌트에 설정될 수 있으나, 각각 유니크한 포트 숫자를 지녀야 합니다. 

     

    디폴트 커넥터는 Coyote입니다.

     

    Connector는 아래와 같이 생성자에서 ProtocolHandler의 Create 메소드를 통해, 프로토콜에 맞는 ProtocolHandler를 설정하여 클라이언트의 요청을 컨테이너에 연결합니다.

     

    public interface ProtocolHandler {
        // ...
        public static ProtocolHandler create(String protocol)
                throws // ... {
            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();
            }
        }
    }
    
    public class Connector extends LifecycleMBeanBase {
        // ...
        
        public Connector(String protocol) {
            configuredProtocol = protocol;
            ProtocolHandler p = null;
            try {
                p = ProtocolHandler.create(protocol);
            } 
            // ...
        }
        
        // ...
    }

    Engine

    엔진은 top-level 컨테이너로 다른 컨테이너가 포함할 수 없습니다 (parent 컨테이너를 가지지 못합니다). 이 레벨에서부터 객체들이 child 컴포넌트들을 가지기 시작합니다. 

     

    컨테이너는 꼭 엔진일 필요는 없고, 위에서 제시된 서블릿 컨테이너의 명세를 만족하면 됩니다. 하지만, 보통 이 레벨의 컨테이너는 엔진이기에 엔진이라는 가정을 깔고 기술하도록 하겠습니다. 

     

    엔진(Engine)은 하나의 컨테이너로 전체 Catalina 서블릿 엔진을 나타냅니다. 엔진은 HTTP 헤더를 체크하여 특정 요청이 어느 가상 호스트 또는 어느 컨텍스트에 연결되어야 하는지 판단합니다. 

     

    특정 설정 변경 없이 실행된다면,  디폴트 엔진을 사용합니다. 이 엔진은 위에서 언급된 체크를 진행합니다. 톰캣이 웹 서버를 위해서 Java Servlet 지원을 제공하도록 설정된 경우에는, 보통 웹 서버가 요청에 대한 연결을 진행하기 때문에 요청에 사용하도록 디폴트로 설정된 클래스가 overriden됩니다.

     

     

    Catalina에서 top-level 컨테이너인 엔진의 Standard 구현체는 StandardEngine 클래스로 아래와 같은 상속 관계를 지닙니다 [3]:

     

    // Lifecycle.java
    public interface Lifecycle {
        // ...
    }
    // Container.java
    public interface Container extends Lifecycle {
        // ...
    }
    // Engine.java
    public interface Engine extends Container {
        // ...
    }
    // StandardEngine.java
    public class StandardEngine extends ContainerBase implements Engine {
        // ...
    }

     

    The Realm

    하나의 엔진을 위한 렐름(Realm)은 사용자 인증과 인가를 담당합니다. 애플리케이션 설정 중에, 관리자는 리소스에 접근이 허용된 롤을 정의하고 렐름이 이러한 정책을 실행하는데 사용됩니다. 렐름은 텍스트 파일, 데이터베이스 테이블, LDAP 서버 등을 통해 인증할 수 있습니다. 

     

    하나의 렐름은 전체 엔진 또는 top-level 컨테이너에 적용되기에, 한 컨테이너 안의 애플리케이션들은 인증을 위한 리소스를 공유합니다.

     

    The Valves

    밸브는 하나의 특정 컨테이너와 관련있는 요청 처리 컴포넌트입니다. 밸브는 서블릿 명세의 필터 메커니즘과 유사하지만, 톰캣의 고유한 것입니다. 호스트, 컨텍스트, 엔진은 밸브를 가집니다: 예) StandardHostValve, StandardContextValve, StandardEngineValve, StandardWrapperValve.

     

    밸브 인터페이스는 아래와 같이 간단한 기본 구조를 가지며 (주석 제거), 각 밸브의 로직은 invoke 메소드에 다양한 형태로 구현되게 됩니다:

     

    public interface Valve {
    
        public Valve getNext();
    
        public void setNext(Valve valve);
    
        public void invoke(Request request, Response response)
            throws IOException, ServletException;
    
        public boolean isAsyncSupported();
    }

     

    예시로 아래와 같은 StandardEngineValve는 요청에서 호스트 객체를 꺼내고, 없으면 에러를 있으면 호스트의 파이프라인에서 가장 처음의 밸브를 꺼내어 invoke(request, response)를 실행합니다:

     

    final class StandardEngineValve extends ValveBase {
    
        @Override
        public final void invoke(Request request, Response response)
            throws IOException, ServletException {
    
            // Select the Host to be used for this Request
            Host host = request.getHost();
            if (host == null) {
                // HTTP 0.9 or HTTP 1.0 request without a host when no default host
                // is defined.
                // Don't overwrite an existing error
                if (!response.isError()) {
                    response.sendError(404);
                }
                return;
            }
            if (request.isAsyncSupported()) {
                request.setAsyncSupported(host.getPipeline().isAsyncSupported());
            }
    
            // Ask this Host to process this request
            host.getPipeline().getFirst().invoke(request, response);
        }
    }

     

    The Loggers

    로거는 컴포넌트의 내부 상태를 리포팅합니다. 로거는 top-level 컨테이너부터 아래로 컴포넌트에 세팅할 수 있습니다. 로깅 특성은 상속되기에, 엔진-레벨에 설정된 로거는 child에 의해 overridden되지 않는 한 그대로 child에 할당됩니다. 

     

    The Host

    호스트는 유명한 Apache virtual host와 유사합니다. 아파치에서 호스트는 여러 서버가 같은 머신에서 사용되며 IP address나 호스트명에 따라 구분되도록 합니다. 톰캣에서 가상호스트는 완전한 호스트명으로 구분됩니다. 그러므로 www.websitea.com과 www.websiteb.com은 같은 서버에 존재할 수 있으며 다른 그룹의 웹 애플리케이션에 요청이 라우팅됩니다.   

     

    호스트를 설정하는 것은 호스트명을 설정하는 것을 포함합니다. 대부분의 클라이언트는 서버의 IP주소와 IP주소에 사용되는 호스트명을 같이 보내는 방식을 취합니다. 호스트명은 HTTP 헤더로 제공되기에 엔진은 헤더를 확인해서 요청이 어느 호스트에 연결되는지 결정할 수 있습니다.

     

    The Context

    컨텍스트는 웹 애플리케이션으로도 불립니다. 웹 애플리케이션의 설정에는 애플리케이션의 루트 폴더 위치를 엔진과 호스트에 알려주는 것도 포함됩니다. 동적 리로딩(reloading)은 어떤 변경된 클래스라도 다시 메모리에 로드될 수 있도록 합니다. 그러나 이것은 리소스를 많이 사용하기에 상용배포 환경에는 권장되지 않습니다.

     

    컨텍스트는 시스템관리자가 설정할 수 있는 특정 에러 페이지도 포함합니다. 마지막으로 컨텍스트에 애플리케이션 또는 접근 통제 등을 위한 초기 파라미터를 설정할 수 있습니다. 

     

    Reference
    [1] Apache Tomcat

    [2] Java Servlet Specification 4.0

    [3] github.com/apache/tomcat/blob/10.0.0/java/org/apache/catalina/Container.java#L30

    [4] Professional Apache Tomcat 5

     

     

    Spring MVC Internals (내부구조)

    Spring MVC는 Spring Framework의 일부로 Spring Web Layer에서 MVC(Model-View-Controller)를 구현한 Web-Servlet 모듈입니다. 이번 글에서는 클라이언트 요청과 응답 시 Spring MVC가 어떻게 동작하는지를 중점으로 그 구

    kadensungbincho.tistory.com

     

    반응형
Kaden Sungbin Cho