CGI와 서블릿, JSP의 연관관계 알아보기

ssongkim·2021년 12월 25일
29
post-thumbnail

서블릿과 JSP, 스프링은 모두 자바 언어를 활용해 웹페이지를 동적으로 생성하는데 사용하는 기술이다. 오늘은 스프링 프레임워크의 구조를 완벽하게 이해하기 위해 서블릿과 JSP, 스프링의 연관 관계를 알아보고자 자바 기준으로 바닥부터 공부해보았다.

Apache? Apache 재단..?

ApacheApache HTTP웹서버Apache 오픈소스 소프트웨어 재단(Apache Software Foundation, ASF)을 뜻하는 중의적인 의미를 가지고 있다.
아파치 HTTP 서버는 아파치 소프트웨어 재단에서 관리하는 HTTP 웹 서버 소프트웨어이다.

웹이 처음 등장했을 때는 HTML과 이미지를 전달해주는 웹서버 밖에 없었다. Apache HTTP서버는 클라이언트의 요청에 대해 정적인 데이터(HTML, 이미지 등)만을 처리하는 웹서버이다. 정적인 페이지 밖에 보여줄 수 없어 같은 HTTP요청에 대해서 항상 똑같은 페이지만을 보여주어야하기 때문에 한계가 존재한다. 동적으로 사용자에 따라, 상황에 따라 다른 페이지를 보여주고 싶을 것이다. 그래서 동적으로 페이지를 보여주기 위해 처음 등장한 기술이 CGI(Common Gateway Interface)이다.

태초에 우리의 조상, CGI가 있었으니..


기존의 웹서버는 정적인 페이지를 보여주는용으로 만들었기 때문에 사용자의 요청을 받아 정보를 동적으로 생성하고 이를 다시 클라이언트로 보내주는 것이 불가능 했다. 따라서 서버에서 다른 프로그램을 불러내고 그 프로그램의 처리결과를 클라이언트로 보내줄수 있는 인터페이스가 필요했고 이것이 CGI이다.
CGI는 환경변수나 표준입출력을 다룰 수 있는 프로그램 언어라면(C, Java, PHP, Python 등) 확장하여 이용이 가능했다. 사용자가 데이터를 입력해 요청을 보내면 해당하는 요청에 대한 CGI프로그램이 동작하고 표준 출력으로 돌려 보낸 내용을 그대로 응답으로 돌려준다.
이러한 CGI는 문제점이 있었는데... 요청이 들어올 때마다 CGI프로그램이 프로세스 단위로 실행이 되어 웹 사용자가 많아짐에 따라 서버에 부하가 크게 가서 많은 사용자를 처리하기에는 무리가 있었다.

프로세스 단위로 실행되던 것을 스레드 단위로 실행되게끔 해 이와같은 문제점을 해결하고자 하였고 이때 등장한 것이 바로 Java Servlet이다. (mod perl, mode php, FastCGI 등도 있었음)

두둥 등장 S.e.r.v.l.e.t

서블릿(servlet)은 자바 기반으로 데이터를 동적으로 멀티스레드 개념을 통해 처리하기 위해 등장한 프로그램이다.

서블릿은 각 요청에 대해 프로세스를 생성해 처리하는 것이 아니라, 프로세스 1개가 있고 그 내부에 스레드 풀이라는 스레드들이 생성될 수 있는 공간을 만들어 멀티스레드로 처리한다. 서블릿은 CGI와 완전히 다른 개념이 아니다. 서블릿도 내부적으로 CGI통신규약을 지켜 통신이 이루어진다.
서블릿은 JSP, Spring MVC의 부모님과도 같은 존재이며 자식인 JSP, Spring MVC는 내부적으로 서블릿 기술을 사용하고 있다. 그래서 스프링 프레임워크의 구조를 완벽하게 이해하고자 한다면 서블릿부터 제대로 알아야 한다. 먼저 서블릿이 실행되는 환경을 제공해주는 Apache Tomcat부터 알아보자

Tomcat이란

아파치 톰캣은 동적인 데이터 처리를 위해 자바 서블릿이 실행될 수 있는 웹 컨테이너(서블릿 컨테이너) 환경을 제공하는 Web Application Server이다.

일반적인 웹서버는 정적인 처리만 가능하지만 TomcatWeb Application Server로 웹서버와는 다르게 DB연결, 다른 응용프로그램과 상호 작용 등 동적인 기능들을 사용할 수 있다. Tomcat은 Apache 웹서버의 일부 기능 + web container 조합으로, 웹서버의 정적 데이터 처리 기능과 동적 데이터 처리 기능 모두를 포함하고 있다. 하나의 톰캣은 하나의 JVM을 가지고 있다.

톰캣의 구조


서블릿은 Web Application Server 안에서 구동된다.
처음 클라이언트의 요청을 받은 웹서버(nginx, apache, haproxy..)는 WAS Server의 웹서버(apache)에 전달하고 WAS Server의 웹서버는 해당 요청이 동적 페이지 요청일 시 HTTP 요청을 Servlet Container에 전달한다. 서블릿 컨테이너는 요청을 처리해 WAS의 웹서버로 전달하고 WAS Server의 웹서버는 HTTP 응답을 웹서버에게 전달, Web Server는 받은 HTTP 응답을 클라이언트에 전달한다.

하나의 톰캣에 여러 웹 애플리케이션을 띄울 수 있지 않을까?

하나의 톰캣 서버는 여러 개의 웹 어플리케이션을 배포하여 운영할 수 있다. 각각의 웹 애플리케이션은 별도의 서블릿 컨텍스트를 가진다. 사용자는 같은 톰캣에 배포되는 애플리케이션이라도 세션은 별도로 할당받는다.

서블릿 컨텍스트란 서블릿 컨테이너와 통신하기 위해서 사용되는 메소드를 지원하는 인터페이스로 서블릿과 서블릿 컨테이너 간 연동을 위해 생성되는 Context이다.
톰캣을 구동하여 서블릿 컨테이너가 생성되면 웹애플리케이션 별 web.xml을 이용해 웹 애플리케이션마다 서블릿 컨텍스트를 초기화하며 생성하고 서블릿 컨테이너가 종료되면 소멸된다.

톰캣에 웹애플리케이션을 추가하는 방법

구형방식(Tomcat 4.x이하)

1. 톰캣 서버가 실행되면 server.xml을 읽는다.

server.xml을 열어보면 가장 하단부에 Context 엘리먼트에 docBase, path와 source에 프로젝트명이 적혀져 있는 것을 볼 수 있다. WAS는 웹 애플리케이션을 Context로 인식한다.
path 경로로 요청을 전달해주어야할 프로젝트를 구분한다.

<Context docBase="myWeb" path="/web" reloadable="true" source="org.eclipse.jst.jee.server:webTest"/>

서버는 server.xml을 읽어 서비스해줘야할 프로젝트를 확인하게 된다.

2. 프로젝트의 web.xml을 읽는다.

서버는 웹 애플리케이션의 환경을 설정하는 web.xml을 읽어들인다. web.xml에는 서블릿이나 필터 등을 작성해 웹 애플리케이션의 환경을 설정한다.

요즘 방식(Tomcat 4.x 이상)

웹 어플리케이션의 폴더구조만 가지고 있다면 자동으로 웹어플리케이션이 설정된다. webapp 디렉토리 아래에 넣어주면 된다.
webapp/WEB-INF 디렉토리 아래에 web.xml 배포기술자 파일을 위치시키면 http://localhost/어플리케이션명으로 접근이 가능해진다.

web.xml

톰캣과 같은 서블릿 컨테이너에 애플리케이션을 배포하는 방법을 설명하는, 서블릿 컨텍스트를 초기화하기 위해 사용되는 배포 기술자 파일이다.

톰캣은 web.xml 설정파일을 통해 해당 웹애플리케이션의 서블릿 컨텍스트를 초기화하며 배포할 서블릿이 무엇인지, 서블릿 매핑 경로는 무엇인지 등 서블릿에 대한 정보를 읽는다.

서블릿3.0버전 이후부터 web.xml 없이도 서블릿 컨텍스트 초기화 작업이 가능해졌다. 프레임워크 레벨에서 직접 초기화할 수 있게 도와주는 ServletContainerInitializer API를 제공하기 때문이다.

ServletContainerInitializer 인터페이스 구현체를 만들면 서블릿 컨테이너는 해당 클래스의 onStartUp 메서드 영역을 호출하여 웹애플리케이션의 서블릿 컨텍스트를 초기화할 수 있다.

서블릿 코드를 간단히 살펴보자


우리는 스프링을 이해하기 위해 서블릿을 알아보고 있으니 코드를 예제로 간단히 살펴보도록 하자
doGet메서드를 오버라이드 해주면 클라이언트가 get방식으로 요청을 보냈을 경우 service 메서드 영역이 호출된 후 doGet메서드가 호출이 된다. 서블릿은 여러개의 자바파일 형태로 작성하며 각각의 서블릿에 요청 받을 URL을 매핑해주면 된다.

서블릿의 생명주기

1. init()
서블릿을 처음 메모리에 올릴때 실행되어, 서블릿을 초기화하며 처음에 한번만 실행

2. service()
요청/응답(request/response)을 처리하며 요청이 GET인지 POST인지 구분하여 doGet() 또는 doPost() 메소드로 분기

3. destroy()
서블릿 종료요청이 있을때 destroy() 메소드가 실행

서블릿의 요청 처리 단계

클라이언트에게 요청이 오면 서블릿 컨테이너는 어떻게 요청 URL에 해당하는 서블릿을 찾아 처리해 응답으로 보내주는 것일까? web.xml설정파일에 그 정보가 기록되어 있다고 한다.

/hello라는 요청을 보내면 HelloServlet이라는 서블릿을 찾아 처리한다고 기술되어 있다.



WAS로 요청이 넘어온 후 요청을 처리하는 처리 단계는 다음과 같다.

1. WAS의 웹서버는 HTTP 요청을 서블릿 컨테이너에 전달

2. 서블릿 컨테이너는 HttpServletRequest / HttpServletResponse 객체를 생성

3. 서블릿 컨테이너web.xml 설정파일을 참고하여 매핑할 서블릿을 찾고 요청 처리에 필요한 서블릿 인스턴스가 컨테이너에 존재하는 지 확인

4. 존재하지 않는다면 서블릿 인스턴스를 생성하고 해당 서블릿 인스턴스의 init() method를 호출하여 서블릿 인스턴스를 초기화

5. 서블릿 컨테이너는 스레드를 생성하고 res, req를 인자로 서블릿 인스턴스의 service() 메소드를 호출하여 요청을 처리 후 WAS의 웹서버에게 처리 결과를 전달한 다음 HttpServletRequest / HttpServletResponse 객체를 소멸

6. WAS의 웹서버는 HTTP 응답을 웹서버(nginx, apache..)에게 전달하고 웹서버는 받은 HTTP 응답을 브라우저(클라이언트)에 전달

HttpServletRequest / HttpServletResponse 객체는 소멸되는데 서블릿 인스턴스는 소멸되지 않는다. 왜냐하면 서블릿 컨테이너에서 서블릿 객체는 싱글톤으로 관리되기 때문이다. 다음에 같은 경로로 요청이 들어오면 객체를 생성하지 않고 해당 서블릿 인스턴스를 재사용한다. 하나의 서블릿 인스턴스를 재사용해 여러개의 HTTP 요청을 동시에 처리하므로 서블릿 인스턴스는 Thread-Safe하지 않다.

이와같이 서블릿 컨테이너는 서블릿 인스턴스를 싱글톤으로 생명주기를 관리하는 역할을 수행한다.
서블릿은 클라이언트로부터 요청이 들어올 때마다 각 요청에 대해 스레드를 생성하여 멀티 스레드로 처리가 이루어진다.

이와같이 서블릿은 전통적으로 개발자가 주도하던 객체 생성 및 소멸 등의 프로그램 흐름 제어를 반대로 서블릿 컨테이너가 주도해 하는데 제어권이 반전됐다고 해서 이를 제어 반전(IoC, Inversion of Control)이라 한다.

IoC개념은 스프링이 아니라 서블릿에서부터 존재했던 것...!

servlet의 lazy loading과 pre loading

지금까지는 해당 서블릿에 대한 클라이언트의 최초 요청을 받을 때 해당 서블릿 인스턴스를 생성하여 싱글톤으로 활용하는 lazy loading 방식을 설명했다. 이것의 단점은 최초요청 시 지연시간이 발생할 수 있는데 이를 해결하기 위해 web.xml의 해당 서블릿 영역에 load-on-startup옵션을 주면 서블릿 컨텍스트를 초기화하는 시점에 미리 서블릿 인스턴스를 생성할 수 있다(pre loading)

JSP의 등.장

서블릿은 HTML코드를 작성하기가 매우 불편하다. 예제를 보자

import java.io.*, javax.servlet.*, javax.servlet.http.*;
public class ServletForm extends HttpServlet {
 public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
  PrintWriter out;
  String name=req.getParameter("UserName");
  String pass=req.getParameter("UserPass");
  res.setContentType("text/html;charset=utf-8");
  out=res.getWriter( );
  out.println("<html>");; out.println("<body>");;
  out.println("</body>"); out.println("</html>");
  out.close( );
 }
}

서블릿은 자바 코드 안에 HTML코드를 프린트해 출력하는 형태인데 너무너무 번거롭다. 이러한 불편함을 해소하고자 JSP가 등장하였다.

JSP 예제 코드

<%@ page language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="EUC-KR">
    <title>title</title>
  </head>
  <body>

    <%-- 자바 코드 삽입 --%>
    <% 
        for(int i = 0; i < 10; i++) {
            out.println(i);
        }
    %>

  </body>
</html>

JSP는 서블릿과 반대로 HTML코드 안에 자바코드를 삽입하는 형태로 view를 구현하는데 훨씬 편리해졌다. 하지만 JSP와 서블릿은 완전히 다른 개념이 아니다. JSP코드는 결국 서블릿으로 변환이 이루어지는 과정을 거치게 된다.

JSP -> 서블릿 변환

JSP는 해당 JSP페이지에 대해 요청이 들어오면 서블릿 컨테이너에 해당 JSP페이지에 대한 서블릿 인스턴스가 존재하는지 검사 후 존재하지 않다면 JSP컨테이너에서 JSP페이지를 서블릿으로 변환해 컴파일하여 class파일을 생성한다.

1. JSP 스크립트 요소 중 스크립트릿에 작성된 소스는 변환된 Servlet의 service() 메서드 안에 들어간다.
2. JSP 스크립트 요소 중 표현식은 변환된 Servlet의 service() 메서드 안에서 out.println() 으로 변환된다.
3. JSP 스크립트 요소 중 선언문에 작성된 소스는 변환된 Servlet의 멤버메서드로 변환된다.
4. JSP에 작성된 일반 HTML 태그들은 변환된 Servlet의 service() 메서드 안에서 out.write() 메서드로 변환된다.
5. page 디렉티브의 속성값들은 Servlet으로 변환시 참고할 정보로 활용된다.

서블릿 컨테이너는 해당 서블릿 클래스를 통해 객체를 생성하여 싱글톤으로 활용한다. 서블릿 컨테이너에 해당 요청에 대한 서블릿 객체가 존재하므로 위에서 설명했던 서블릿 요청 처리 과정를 거치게 된다.
JSP 컨테이너는 JSP 파일을 서블릿으로 변환 및 컴파일까지만 담당하는 프로그램이며 변환된 서블릿의 수행은 서블릿 컨테이너가 담당한다.

마무리

서블릿의 탄생배경과 JSP와 서블릿의 연관 관계에 대해 알게 되었으며 제어반전(IoC)에 대해 제대로 이해할 수 있었다. 다음은 servlet과 JSP의 불편함을 해소하기 위해 탄생한 MVC패턴과 스프링 MVC에 대해 이해해보려고 한다.

참고자료

1. 우아한 테코톡
https://www.youtube.com/watch?v=2pBsXI01J6M&t=

2. 웹프로그래밍 수업자료

3. 서블릿
https://jordy-torvalds.tistory.com/14

4. 서블릿 컨텍스트
https://docs.oracle.com/javaee/6/api/javax/servlet/ServletContext.html?is-external=true

https://docs.spring.io/spring-framework/docs/3.2.x/javadoc-api/org/springframework/web/WebApplicationInitializer.html

https://docs.spring.io/spring-framework/docs/3.2.x/javadoc-api/org/springframework/web/SpringServletContainerInitializer.html

profile
鈍筆勝聰✍️

0개의 댓글