WAS 및 서블릿 컨테이너 이해

WAS란 무엇일까

지금까지 자바 서버사이드 웹 개발을 연습해오면서 단 한번도 클라이언트 - 서버간의 통신을 위한 연결 관리를 직접적으로 해본 경험은 거의 드물었던것 같다. 이는 내가 이것을 사용해왔는지에 대해 인지하기 조차 힘들정도로 웹서비스를 구현하기 위해서는 당연하게 WAS를 이용해왔기 때문이다(대표적으로 Tomcat).

WAS는 클라이언트와 서버간의 소켓 통신에 필요한 TCP/IP 연결관리와 HTTP 프로토콜 해석과 같은 네트워크 기반 작업을 추상화 하여 실행환경을 제공한다. 즉 개발자는 네트워크 연결에 관한 작업과정을 생략하고 서비스 로직을 구현하는것 만으로 쉽게 웹을 구현할 수 있었던 것이다.

결국 현재까지의 나는 관련 기술을 제대로 이해하지 못한 채로 WAS가 제공하는 추상화된 API로 네트워크에 접근해왔던 것이다. 지금까지는 WAS를 당연하게 사용해왔지만 애석하게도 이번 스타터스 부트캠프 교육기간에 기반 기술을 제대로 이해하지 못한체로 넘어 가게된다면 복습의 의미가 없을것이기 때문에 가능한한 최대한 상세히 WAS를 씹고 뜯어서 이해해보고자 한다.

서블릿

서블릿의 이해

  • 원칙적으로 javax(jakarta).servlet.Servlet 인터페이스를 구현한 것이 서블릿이다.

  • 서블릿은 독립적으로 실행되지 않는다(Main Method 없음)
  • 서블릿 컨터이너의 의해 서블릿의 상태가 결정된다.
  • 서블릿 인터페이스를 구현하면 서블릿 컨테이너가 서블릿에 해당 구현을 통해 생성 소멸 등의 생명주기 관리작업을 할 수 있다.

GenericServlet

  • 서블릿 컨테이너가 서블릿을 관리하기 위해 필요한 기능을 미리 작성하여 GenericServlet이라는 추상클래스를 제공한다
  • service 메서드를 제외하고는 모두 구현된 일종의 서블릿을 위한 어댑터 역할을 제공
    • 서블릿 컨테이너에 의해 서블릿의 생명주기를 관리받기 위해서 Servlet 인터페이스를 반복적으로 구현하는 대신 GenericServlet을 상속해서 사용한다
    • 필요에 따라 하위 구현체에서 init(), destroy()와 같은 메서드를 오버라이딩 할 수 있다.
    • 서블릿이 호출될 때 (인스턴스화) init() 메서드가 한번 실행된다. 이후에는 인스턴스가 메모리에 상주하게 되고 서블릿 종료시(WAS 실행종료 및 재배포) destory() 실행

HttpServlet

  • HttpServletGenericServlet을 상속받으며 일반적으로 서블릿이라 하면 거의 대부분 이 HttpServlet을 상속받은 서블릿을 의미한다.
  • GenericServlet의 유일한 추상 메서드인 service를 HTTP 프로토콜 요청 메서드에 적합하게 재구현한 것이다.
  • 하위 클래스에서 처리하고자 하는 서비스의 HTTP 요청의 메서드 유형에 따라 doGet(), doPost() … 등의 메서드를 구현한다
  • 서블릿 컨테이너는 받은 요청에 대해 서블릿을 선택한 후 Servlet 인터페이스에 정의된 service()를 호출한다. 그러면 클래스 상속 위계에 따라 그 처리가 부모 클래스인 GenericServlet에서 자식 클래스인 HttpServlet으로 넘어온다. HttpServlet의 service 메서드 내에서는 HTTP 요청 메서드에 의해 여러 do…() 메서드로 분기되어 처리된다.
protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException
    {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                if (ifModifiedSince < lastModified) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
            
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
            
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
            
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
            
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
            
        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

Apache Tomcat

아파치 톰캣은 대표족인 오픈소스 서블릿 컨테이너이다.

톰캣 서버 디렉터리의 구조

user@DESKTOP-J26ER00:/mnt/c/Program Files/Tomcat 9.0$ tree . -L 2
.
├── LICENSE
├── NOTICE
├── RELEASE-NOTES
├── Uninstall.exe
├── bin
│   ├── Tomcat9.exe
│   ├── Tomcat9w.exe
│   ├── **bootstrap.jar
│   ├── **catalina.sh(catalina.bat)** 
│   ├── ciphers.bat
│   ├── configtest.bat
│   ├── digest.bat
│   ├── makebase.bat
│   ├── service.bat
│   ├── setclasspath.bat
│   ├── shutdown.bat
│   ├── **startup.sh(startup.bat)**
│   ├── **tomcat-juli.jar**
│   ├── tool-wrapper.bat
│   └── version.bat
├── conf
│   ├── Catalina
│   ├── catalina.policy
│   ├── catalina.properties
│   ├── context.xml
│   ├── jaspic-providers.xml
│   ├── jaspic-providers.xsd
│   ├── logging.properties
│   ├── server.xml
│   ├── tomcat-users.xml
│   ├── tomcat-users.xsd
│   └── web.xml
├── lib
│   ├── annotations-api.jar
│   ├── catalina-ant.jar
│   ├── catalina-ha.jar
│   ├── catalina-ssi.jar
│   ├── catalina-storeconfig.jar
│   ├── catalina-tribes.jar
│   ├── catalina.jar
│   ├── ecj-4.20.jar
│   ├── el-api.jar
│   ├── jasper-el.jar
│   ├── jasper.jar
│   ├── jaspic-api.jar
│   ├── jsp-api.jar
│   ├── servlet-api.jar**
│   ├── tomcat-api.jar
│   ├── tomcat-coyote.jar
│   ├── tomcat-dbcp.jar
│   ├── tomcat-i18n-cs.jar
│   ├── tomcat-i18n-de.jar
│   ├── tomcat-i18n-es.jar
│   ├── tomcat-i18n-fr.jar
│   ├── tomcat-i18n-ja.jar
│   ├── tomcat-i18n-ko.jar
│   ├── tomcat-i18n-pt-BR.jar
│   ├── tomcat-i18n-ru.jar
│   ├── tomcat-i18n-zh-CN.jar
│   ├── tomcat-jdbc.jar
│   ├── tomcat-jni.jar
│   ├── tomcat-util-scan.jar
│   ├── tomcat-util.jar
│   ├── tomcat-websocket.jar
│   └── websocket-api.jar
├── logs
...
├── temp
├── tomcat.ico
├── webapps
│   ├── ... (웹 애플리케이션 프로젝트별 클래스패스에서 컴파일된 클래스들을 war파일로 배포됨)
└── work
    └── Catalina
  • bin/ 내부는 톰켓 엔진에 해당한다
  • /webapps는 애플리케이션을 배치하는 위치다.
  • /logs는 톰캣이 로그를 출력하는 위치다. 톰캣의 로그는 기본적으로 /logs/catalina.out으로 이동한다. 이 파일을 사용해 애플리케이션별 로그 파일과 함께 문제를 디버깅할 수 있다.
  • /lib은 톰캣서버 애플리케이션이 사용하는 라이브러리들에 대한 JAR을 찾는 위치다.
  • /conf는 톰캣용 설정 XML이며, 톰캣에 사용자 및 역할을 추가할 수 있다.

톰캣 서버 실행시 벌어지는 부팅과정

톰캣 서버가 실행되기 위해서는 서버 구동 스크립트 startup.sh(bat) 이 실행되고 해당 스크립트가 톰켓 엔진 실행을 위한 스크립트파일 catalina.sh 혹은 catalina.bat 를 실행한다.

catalina.sh

# Add on extra jar files to CLASSPATH
if [ ! -z "$CLASSPATH" ] ; then
  CLASSPATH="$CLASSPATH":
fi
CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar

if [ -z "$CATALINA_OUT" ] ; then
  CATALINA_OUT="$CATALINA_BASE"/logs/catalina.out
fi

if [ -z "$CATALINA_TMPDIR" ] ; then
  # Define the java.io.tmpdir to use for Catalina
  CATALINA_TMPDIR="$CATALINA_BASE"/temp
fi

# Add tomcat-juli.jar to classpath
# tomcat-juli.jar can be over-ridden per instance
if [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
  CLASSPATH=$CLASSPATH:$CATALINA_BASE/bin/tomcat-juli.jar
else
  CLASSPATH=$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar
fi
  • 톰캣이 실행될 떄 jar파일 두개(bootstrap.jar, tomcat-juli.jar)만 클래스패스에 지정되어 있으므로 실행하고자 하는 웹애플리케이션에 대핸 클래스들을 로딩하는 과정이 필요하다
  • 톰캣 서버는 시작할 때 /webapps 디렉토리 아래에서 웹 애플리케이션을 탐색한다.

톰켓 서블릿 컨테이너가 실행되는 메인메서드(Bootstrap.main())의 실행 로직을 살펴보자

부트스트랩 클래스

톰켓 서블릿 컨테이너 시작클래스

Bootstrap.class

/**
     * Main method and entry point when starting Tomcat via the provided
     * scripts.
     *
     * @param args Command line arguments to be processed
     */
    public static void main(String args[]) {

        if (daemon == null) {
            // Don't set daemon until init() has completed
            Bootstrap bootstrap = new Bootstrap();
            try {
                bootstrap.init();
            } catch (Throwable t) {
                handleThrowable(t);
                t.printStackTrace();
                return;
            }
            daemon = bootstrap;
        } else {
            // When running as a service the call to stop will be on a new
            // thread so make sure the correct class loader is used to prevent
            // a range of class not found exceptions.
            Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
        }

        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();
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null==daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }
  • Bootstrap 클래스를 생성하고 init() 메서드를 호출해 초기화한다.
  • daemon 멤버변수에 Bootstrap 인스턴스를 할당한다.
  • 이후 main 메서드의 args 배열로 전달받은 명령행 인자 값에 따라 Bootstrap 클래스의 인스턴스인 daemon의 load, start, stop 등의 메서드가 호출된다.
  • 예외가 발생하지 않는다면 main메소드는 종료되겠지만 톰켓서버는 실행시 곧바로 종료되지 않고 서블릿 컨테이너로서 클라이언트로부터 HTTP요청을 대기하며 요청에 알맞는 서블릿을 생성하며 서비스를 제공한다. ⇒ init, load, start 등의 메소드에 서블릿 컨테이너를 종료하지 않고 HTTP 요청을 받아들일 수 있게 소켓을 열고, 서블릿을 초기화하며 스레드를 관리하는 등 웹 서비스를 동작하게 하는 기능이 존재한다.

    init()

    /**
     * Initialize daemon.
     * @throws Exception Fatal initialization error
     */
    public void init() throws Exception {
    
        initClassLoaders();
    
        Thread.currentThread().setContextClassLoader(catalinaLoader);
    
        SecurityClassLoad.securityClassLoad(catalinaLoader);
    
        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");
        Class<?> startupClass =
            catalinaLoader.loadClass
            ("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.newInstance();
    
        // Set the shared extensions class loader
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        String methodName = "setParentClassLoader";
        Class<?> paramTypes[] = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object paramValues[] = new Object[1];
        paramValues[0] = sharedLoader;
        Method method =
            startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);
    
        catalinaDaemon = startupInstance;
    
    }
    initClassLoaders()
    private void initClassLoaders() {
        try {
            commonLoader = createClassLoader("common", null);
            if( commonLoader == null ) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader=this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }
    • init() 메서드를 살펴보면 톰캣 서버가 실행되고 여러 클래스 로더들을 생성하여 서블릿컨테이너에 필요한 클래스 및 웹 애플리케이션에 필요한 클래스들을 로드할 수 있도록 한다는 것을 알 수 있다.
    • initClassLoader()는 모두 세개의 클래스로더를 생성한다
      • common class loader ⇒ 루트 클래스로더, 최상위 클래스로더
      • server class loader ⇒ 부모 클래스 로더 : common class loader, 실질적인 톰캣 서버 클래스인 카탈리나 클래스
      • shared class loader ⇒ 부모 클래스 로더 : common class loader,
      • common, shared, server 클래스로더는 catalina.properties 파일에 정의된 디렉토리의 jar 파일을 읽어들여, 파일 안에 포함된 클래스를 로딩한다.(기본으로 제공되는 catalina.properties에는 common 클래스로더만 설치 디렉토리 아래 lib 디렉토리로 지정되어 있음)
      • Thread.currentThread().setContextClassLoader(catalinaLoader)
        는 카탈리나 클래스로더(server class loader)를 현재 클래스로더로 바꾼다. ( 가장 처음 톰캣 엔진 구동 스크립트에서 2개의 jar파일만 로드했지만 init() 메소드 실행과정에서 클래스 로더가 변경되어 톰캣 애플리케이션이 /lib 아래의 클래스도 로드할 수 있게 된다.)
      • server class loader는 common class loader($CATALINA_HOME/lib아래 클래스 로드)를 부모클래스로더로 하고 톰캣 서버클래스인 Catalina클래스를 비롯한 실질적인 톰캣 서버관련 클래스들을 로드한다

JVM 전체 관점에서의 톰캣 웹앱 클래스 로드 순서

  1. JVM bootstrap loader가 코어 자바 라이브러리들을 로드한다(JVM은 JAVA_HOME 변수를 사용하여 코어 라이브러리들을 찾는다).
  2. Startup.sh는 "start" 파라미터와 함께 Catalina.sh를 호출해서 system classpath를 overwrites하고 bootstrap.jar와 tomcat-juli.jar를 로드한다. 이러한 리소스들은 톰캣에서만 볼 수 있다.
  3. 클래스로더들은 각각 디플로이된 컨텍스트에 대해 생성되며 deployed Context는 각 web 애플리케이션의 WEB-INF/classesWEB-INF/lib에 있는 모든 클래스들과 JAR 파일들을 순서대로 로드한다. 이러한 리소스들은 그것들을 로드한 웹 애플리케이션에서만 볼 수 있다.
  4. The Common class loader는 $CATALINA_HOME/lib에 있는 모든 클래스들과 JAR 파일들을 로드한다. 이러한 리소스들은 톰캣과 모든 애플리케이션에서 볼 수 있다.
  5. server class loader가 컨텍스트 클래스로더로 교체되고 system classpath 하위의 클래스 로더에 접근할 수 있게 된다

생명주기 관리

톰캣 서블릿 컨테이너를 사용한 경험이 있는 웹 프로그래머라면 한 번쯤 config 디렉토리 안에 있는 XML 파일이 어떤 의미가 있으며, 어떤 시점에 어떻게 로딩되는지 의문을 가졌을 것이라 생각한다. 이런 설정 값을 읽어들이는 과정은 바로 Catalina 클래스의 load 메서드에서 찾아볼 수 있다.

  /**
  
  * Start a new server instance.
  
  */
  
  public void load() {
  
  long t1 = System.nanoTime();
  
  initDirs();
  
  // Before digester - it may be needed
  
  initNaming();
  
  // Create and execute our Digester
  
  Digester digester = createStartDigester(); // 설정 파일을 개체화해 접근할 수 있도록 한다.
  
  InputSource inputSource = null;
  
  InputStream inputStream = null;
  
  File file = null;
  
  try {
  
  try {
  
  file = configFile(); //conf/server.xml
  
  inputStream = new FileInputStream(file);
  
  inputSource = new InputSource(file.toURI().toURL().toString());
  
  } catch (Exception e) {
  • 톰캣 서블릿 컨테이너는 Digester를 사용해 부팅 시 설정 파일을 객체화해 접근하는 방식을 지원한다. 그러므로 server.xml![]
    파일이 로딩되면 XML 태크게 해당하는 객체가 생성된다. 톰캣 서블릿 컨테이너는 설정 파일에 지정된 객체들을 시동 시, 자동으로 생성될 뿐만 아니라 유지, 관리, 소멸 등의 생명주기를 관리함으로써 동작 중인 서버를 재시동하지 않고서 기능 변경을 하는 방법을 제공한다.
  • 정리하면 사용자가 설정 파일에 정의한 각 XML element는 Digester에 의해 서버 객체로 변경돼 로딩되며, 이런 객체는 Lifecycle 인터페이스를 구현했으므로 프로그래밍적으로 초기화, 시작, 종료, 소멸 등을 컨트롤할 수 있다는 의미가 된다.

0개의 댓글