자, WAS(Web Application Server)의 정체를 알아보자.
Web Server 중간에 Application이 들어간 것이 WAS이다. 중간에 Application이 들어간 이유는, 웹 서버의 역할을 하면서 추가로 애플리케이션, 그러니까 프로그램 코드도 수행 하기 때문이다.
정리하면 웹(HTTP)를 기반으로 작동하는 서버인데, 이 서버를 통해서 프로그램의 코드도 같이 실행할 수 있는 서버라는 뜻이다.
여기서 말하는 프로그램의 코드는 바로 앞서 우리가 작성한 서블릿 구현체들이다.
우리가 작성한 서버는 HTTP 요청을 처리하는데, 이때 프로그램의 코드를 실행해서 HTTP 요청을 처리한다. 이것이 바로 웹 애플리케이션 서버(WAS)이다
그리고 WAS는 web server + 컨테이너
라고 볼 수 있다.
응? 컨테이너? 이게 뭘까?
was의 정체를 알려면, 서블릿 즉, 서블릿 컨테이너라는 개념을 잘 알아야 한다!
(정확하게는, 서블릿 컨테이너 = Servlet + JSP + EJB 컨테이너
)
서블릿 컨테이너는 개발자가 웹서버와 통신하기 위하여 소켓을 생성하고, 특정 포트에 리스닝하고, 스트림을 생성하는 등의 복잡한 일들을 할 필요가 없게 해주는, 자바 웹 어플리케이션 동적 페이지를 만들어 주는 기술
이다.(원래 소켓, 스트림 등, TCP/IP 기술로 통신하는 것이었음..)
💡 Spring과 같이 넓게 보자!
WAS의 대표적인 예시가 Tomcat 이다.
이 Tomcat은 Java로 코드로 구현되어 있는데, Servlet이라는 최상위 인터페이스를 참조하고 있고, Tomcat에게 Reqeust가 오면, Tomcat이 추가적으로 내장하고 있는 WS(웹서버)에게 정적인 페이지를 요청하고, 추가적으로 동적데이터가 필요한 경우, Servlet에게 책임을 떠넘긴다!(프로그램 코드를 실행-WAS의 역할!)
그럼 Spring 어플리케이션인 경우를 보자, Spring 내부에는 Dispatcher Servlet이란 것이 있다. 이 Dispatcher Servlet은 앞에서 말한 Servelt을 구현하는 클래스로, 작동flow는 다음과 같다.
WS에게 정적인 페이지 요청 OR 동적인 페이지 필요한 경우 WAS에게 요청
1) WAS(tomcat)는 요청의 처리 책임을 Servelt에게 위임
2) Spring의 Dispatcher Servelt이 작동
3) FrontController인 Dispatcher Servelt이 개발자가 구현한 Controller들을 탐색, 처리 책임 위임(커멘트패턴)
@WebServlet(name = "helloServelt", urlPatterns = "/hello")
public class HelloServelt extends HttpServelt {
@Overrride
protected void service(HttpServeltRequest request, HttpServeltResponse response){
// 애플리케이션 로직
}
}
/hello
)의 URL이 호출되면 서블릿 코드가 실행된다.Servelt이란 웹 어플리케이션을 위한 JAVA 클래스 구조를 가진 자바 서버 프로그래임
이다. 이것이 왜 필요하냐면 웹페이지를 서버에서 동적(프로그램 코드가 실행)으로 생성하기 위함이다.
컨테이너에 의해 실행되면, 개발자가 임의로 프로그래밍을 하는 것이 아닌, 특정 클래스를 상속받아 구현을 자동으로 생성해 주는 구조이다. 그래서 개발자가 편하게 웹구동을 위한 개발을 할 수 있다.
일반적으로 Request 요청(HttpServletRequest)
이나, Response 응답(HttpServletResponse)
을 위한 Servelt 객체들은 javax.servlet.http.HttpServelt 클래스
를 상속받아 구현한다.
컨테이너란 서블릿 객체의 생성, 초기화, 호출, 소멸까지의 인생주기(Life Cycle)를 관리하는 것이다.
⭐ 서블릿 컨테이너는 가장 핵심적인 flow는 다음과 같다!
1)
요청(Request)이 들어올 때마다
2) 새로운 자바 스레드를 만든다!
3) 그리고 이 스레드가 servlet을 호출한다. (-> 향후 스레드 Pool을 만들어서 스레드 생성 비용을 줄인다.
)
서블릿 컨테이너의 기본 5가지 기능을 제공한다.
1) 네트워크 통신
2) 서블릿 객체 인생주기 관리
3) 서블릿 객체 싱글톤으로 관리
4) JSP도 서블릿으로 변환 되어서 사용
5) 동시 요청을 위한 멀티 쓰레드 처리 지원
우리가 알고 있는 대표적인 Servlet Container가 있다. 바로 WAS의 대표주자, Tomcat이다~
지금까지 사용 많이 한 톰캣은
war파일을 java파일에서 Class로 만들고 컴파일하고 메모리에 올려 servlet객체를 만들었던 것
이다.
서블릿 컨테이너는 사용자로부터 요청을 받을 때 마다 요청을 처리할 스레드를 생성한다.(요청 request : 1 Thread
)
그리고 그 스레드에서 필요한 서블릿 메소드를 호출
하게 되는 것이다.
그렇다고 해서 스레드를 무제한으로 생성하기만 하는 것은 아니고(메모리 비용이 많이 들기때문에
) 컨테이너 내부에 Thread Pool(스레드풀)에 스레드를 미리 할당하고, 요청할 때 꺼내 재사용
하는 것이다.
✳️ 참고로, Tomcat에서는 기본적으로 클라이언트의 요청을 받을 받기 위해 200개의 Thread를 Thread Pool(스레드풀)에 생생해 둔다.
그리고 서블릿 객체는 싱글톤으로 관리된다.
이것도 요청이 올때마다 계속 객체를 생성하는것은 비효율이기 때문이다.
✅ 서블릿 메모리 비효율을 막기 위한 장치
1) 스레드풀
2) 싱글톤
즉, 최초 로딩 시점에 서블릿 객체를 미리 만들어 두고 재활용(스레드풀)하며 모든 요청은 동일한 서블릿 객체 인스턴스(싱글톤)에 접근하게 됩니다.
<김영한 서블릿 컨테이너 was 구조>
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
// 서블릿을 매핑할 URL 패턴 지정
@WebServlet("/example")
public class ExampleServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
// GET 요청 처리
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 요청에서 파라미터 가져오기
String name = request.getParameter("name");
if (name == null) {
name = "Guest"; // 기본값 설정
}
// 응답의 콘텐츠 타입 설정
response.setContentType("text/html");
// 클라이언트에게 HTML 응답 작성
response.getWriter().write("<html><body>");
response.getWriter().write("<h1>Hello, " + name + "!</h1>");
response.getWriter().write("</body></html>");
}
// POST 요청 처리
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 요청에서 데이터 가져오기 (예: 폼 데이터)
String data = request.getParameter("data");
// 데이터를 처리하는 로직을 추가할 수 있음
System.out.println("Received data: " + data);
// 클라이언트에 응답 보내기
response.setContentType("text/plain");
response.getWriter().write("Data received successfully: " + data);
}
}
1) 클라이언트 요청 수신:
서블릿 컨테이너가 HTTP 요청을 받아 HttpServletRequest 객체와 HttpServletResponse 객체를 생성합니다.
2) 스레드 처리:
서블릿 컨테이너가 내부적으로 스레드를 생성해 서블릿의 doGet 또는 doPost 메서드를 호출합니다.
3) 서비스 로직 수행:
request 객체에서 요청 데이터를 읽어 처리하고, response 객체를 통해 클라이언트에 응답을 전송합니다.
다시 정리하자면,
1) 사용자가 URL을 클릭하면 HTTP Request를 Servlet Container에 보낸다.
2) Servlet Container는 HttpServletRequest
, HttpServletResponse
두 객체를 생성한다.
3) 사용자가 요청한 URL을 분석하여 어느 서블릿에 대한 요청인지 찾는다.
4) 컨테이너는 서블릿 service()메소드를 호출하며, POST/GET 여부에 따라 doGet() 또는 doPost()가 호출된다.
5) doGet()이나 doPost() 메소드는 동적인 페이지를 생성한 후 HttpServletResponse 객체에 응답을 보낸다.
6) 응답이 완료되면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킨다.
서블릿 내부의 스레드 처리 코드는 개발자가 직접 작성하는 것이 아니라, 서블릿 컨테이너(예: Tomcat, Jetty)가 이를 관리합니다. 컨테이너는 요청을 처리하기 위해 스레드를 생성하거나 스레드 풀을 사용하여 효율적으로 동작합니다. 따라서 서블릿 컨테이너 내부의 스레드 처리 코드는 일반적으로 직접 접근하거나 수정할 수 없습니다.
하지만 스레드 풀을 사용하여 비슷한 동작을 구현하는 자바 코드는 유추를 하면 다음과 같을 것이다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadHandlingExample {
private static final int THREAD_POOL_SIZE = 10;
private ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
public void handleRequest(String request) {
threadPool.submit(() -> {
// 요청 처리 로직
System.out.println("Handling request: " + request + " by thread " + Thread.currentThread().getName());
try {
// 처리 시뮬레이션 (예: 데이터베이스 작업 또는 파일 처리)
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Finished processing request: " + request);
});
}
public static void main(String[] args) {
ThreadHandlingExample example = new ThreadHandlingExample();
// 여러 요청 처리 시뮬레이션
for (int i = 1; i <= 20; i++) {
example.handleRequest("Request " + i);
}
example.threadPool.shutdown(); // 모든 작업 완료 후 스레드 풀 종료
}
}
설명)
스레드 풀
: Executors.newFixedThreadPool을 사용해 정해진 수의 스레드(예: 10개)를 생성합니다.
비동기 작업 제출
: 각 요청은 threadPool.submit()을 통해 처리됩니다.
멀티스레드 환경
: 각 스레드는 요청을 독립적으로 처리하며, 서로 간섭하지 않습니다.
서블릿 컨테이너의 내부적인 구현은 이와 유사한 방식으로 동작하지만, 훨씬 더 복잡하고 최적화된 스레드 관리 메커니즘을 가지고 있습니다.
init(): 서버가 켜질 때 한번만 실행
service(): 모든 유저들의 요청들을 받는다.
destroy(): 서버가 꺼질 때 한번만 실행
서블릿 컨테이너는 클라이언트로부터 처음 요청이 들어오면 현재 실행할 서블릿이 최초의 요청인지 판단한고 없으면 해당 서블릿을 새로 생성한다. 이 작업은 최초 1회만 일어난다.
init() 해당 사용자 서블릿이 최초 생성되고 바로 호출되는 메소드이다.
service() 최초의 요청이든 2번째 요청이든 계속 호출되는 메소드이다.
여기에서 서블릿컨테이너가 종료된다면 사용자 정의 HttpServlet의 destroy()가 호출될 것이다.
서블릿의 실행 순서는 개발자가 관리하는게 아닌 서블릿 컨테이너가 관리를 한다. 즉 서블릿에 의해 사용자가 정의한 서블릿 객체가 생성되고 호출되고 사라진다.
이렇게 개발자가 아닌 프로그램에 의해 객체들이 관리되는 것을 IoC(Inversion of Control)이라고 한다.
Servlet은 서블릿 프로그램을 개발할 때 반드시 구현해야 하는 메서드를 선언하고 있는 인터페이스이다. 이 표준을 구현해야 서블릿 컨테이너가 해당 서블릿을 실행할 수 있다.
GenericServlet은 Servlet인터페이스를 상속하여 클라이언트-서버 환경에서 서버단의 애플리케이션으로서 필요한 기능을 구현한 추상 클래스이다. service()메소드를 제외한 모든 메서드를 재정의하여 적절한 기능으로 구현했다. GenericServlet클래스를 상속하면 애플리케이션의 프로토콜에 따라 메서드 재정의 구문을 적용해야 한다.
일반적으로 서블릿이라하면 거의 대부분 HttpServlet을 상속받는 서블릿을 의미한다. HttpServlet은 GenericServlet을 상속받았으며, GenericServlet의 유일한 추상 메서드인 service를 HTTP 프로토콜 요청 메서드에 적합하게 재구현해놨다.
이미 DELETE, GET, HEAD, OPTIONS, POST, TRACE를 처리하는 메소드가 모두 정의되어 있다.