작성해야 할 Servlet API 소스 코드 목록은 다음과 같습니다.
-
src/main/java/javax/servlet/Servlet.java
-
src/main/java/javax/servlet/http/HttpServlet.java
-
src/main/java/javax/servlet/http/HttpServletRequest.java
-
src/main/java/javax/servlet/http/HttpServletRequestImpl.java
-
src/main/java/javax/servlet/http/HttpServletResponse.java
-
src/main/java/javax/servlet/http/HttpServletResponseImpl.java
간단한 설명 이후 [IndexServlet] 부분에서 다시 설명하겠습니다.
package javax.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface Servlet {
public void init(); <===== ❶
public void service(HttpServletRequest request, HttpServletResponse response)
throws IOException; <===== ❷
public void destroy(); <===== ❸
}
[Servlet] 는 서블릿이 어떻게 작동하는지에 대한 명세입니다.
서블릿은 브라우저에서 사용자의 요청이 들어오면 서블릿을 클래스를 인스턴스화 하고 ❶ init() → ❷ service() → ❸ destory() 과정을 거쳐 종료하며 브라우저에 응답하게 됩니다.
package javax.servlet.http;
import javax.servlet.Servlet;
import java.io.IOException;
public class HttpServlet implements Servlet { <===== ❶
@Override
public void init() {
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException { <===== ❷
if ("GET".equals(request.getMethod())) {
doGet(request, response); <===== ❸
} else {
doPost(request, response); <===== ❹
}
}
@Override
public void destroy() { }
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { }
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {}
}
[HttpServlet] 는 ❶ [Servlet] 를 상속받아 구현합니다.
인터페이스 함수 ❷ service 가 간단하게 HTTP Method Type 에 따라 ❸ doGet, ❹ doPost 함수로 분기하는 구현 소스입니다.
개발자는 서블릿 요청에 맞는 기능을 [HttpServlet] 를 상속받아 HTTP Method 종류에 따라서 doGet() 또는 doPost() 메소드를 구현하면 됩니다.
package javax.servlet.http;
public interface HttpServletRequest {
// HTTP Method
public String getMethod();
// 요청 URL 중 URI
public String getRequestURI();
// 요청 URL 중 GET Query String
public String getQueryString();
// 메시지 길이
public int getContentLength();
// 요청 메시지 타입
public String getContentType();
// 요청 Header 특정 값 가져오기
public String getHeaders(String name);
// 요청 Parameter 특정 값 가져오기
public String getParameter(String name);
// 클라이언트 아이피 가져오기
public String getRemoteAddr();
}
[HttpServletRequest]는 앞서 [SimpleHTTPServer1] 소스에서 출력한 정보를 어떻게 꺼내어 사용할 지에 대한 명세입니다.
package javax.servlet.http;
import java.net.SocketAddress;
import java.util.Map;
public class HttpServletRequestImpl implements HttpServletRequest {
private Map headers = null;
private Map paramaters = null;
private String remoteAddr;
private String method;
private String requestUrl;
private String httpVersion;
... 생략 ...
@Override
public String getQueryString() {
String queryString = null;
if (requestUrl != null) {
int idx = requestUrl.indexOf("?");
if (idx > -1) {
queryString = requestUrl.substring(idx + 1);
}
if (queryString != null) {
String[] pairs = queryString.split("&");
for (String pair : pairs) {
idx = pair.indexOf("=");
this.paramaters.put(pair.substring(0, idx), pair.substring(idx + 1));
}
}
}
return queryString;
}
@Override
public int getContentLength() {
String length = this.getHeaders("Content-Length");
if (length == null) {
return 0;
} else {
return Integer.parseInt(length);
}
}
@Override
public String getContentType() {
return getHeaders("Content-Type");
}
@Override
public String getHeaders(String name) {
return headers==null?null:headers.get(name);
}
@Override
public String getParameter(String name) {
return paramaters==null?null:paramaters.get(name);
}
@Override
public String getRemoteAddr() {
return this.remoteAddr;
}
public void setGeneral(String general) {
int firstBlank = general.indexOf(" ");
int secondBlank = general.lastIndexOf(" ");
this.method = general.substring(0, firstBlank);
this.requestUrl = general.substring(firstBlank+1, secondBlank);
this.httpVersion = general.substring(secondBlank+1);
}
public void setHeaders(Map headers) {
this.headers = headers;
}
public void setParamaters(Map paramaters) {
this.paramaters = paramaters;
}
public void setBody(String body) {
if (body != null && body.contains("&")) {
System.out.println("body: " + body);
String[] pairs = body.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
this.paramaters.put(pair.substring(0, idx), pair.substring(idx + 1));
}
}
}
... 생략 ...
}
[HttpServletRequestImp] 는 [HttpServletRequest] 를 상속받아 구현한 소스입니다.
package javax.servlet.http;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
public interface HttpServletResponse {
// 응답 상태 코드
public int getStatus();
public void setStatus(int status);
// 메시지 길이
public void setContentLength(long len);
// 요청 메시지 타입
public String getContentType();
public void setContentType(String type);
// 요청 Header 특정 값 가져오기
public String getHeaders(String name);
public void setHeader(String name, String value);
public Collection getHeaderNames();
public PrintWriter getWriter() throws IOException;
}
[HttpServletResponse] 는 앞서 [SimpleHTTPServer2] 소스에서 사용자 브라우저로 출력해야 하는 정보에 대한 명세입니다.
package javax.servlet.http;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class HttpServletResponseImpl implements HttpServletResponse {
private Map headers = new HashMap<>();
private PrintWriter printWriter = null;
private int status;
public HttpServletResponseImpl(File tmpFile) {
try {
printWriter = new PrintWriter(tmpFile);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public int getStatus() {
return this.status;
}
@Override
public void setStatus(int status) {
this.status = status;
}
@Override
public void setContentLength(long len) {
headers.put("Content-Length", len + "");
}
@Override
public String getContentType() {
return headers.get("Content-Type");
}
@Override
public void setContentType(String type) {
headers.put("Content-Type", type);
}
@Override
public String getHeaders(String name) {
return headers.get(name);
}
@Override
public void setHeader(String name, String value) {
headers.put(name, value);
}
@Override
public Collection getHeaderNames() {
return new ArrayList<>(headers.keySet());
}
@Override
public PrintWriter getWriter() throws IOException {
return printWriter;
}
}
[HttpServletRequestImpl] 는 [HttpServletResponse] 를 상속받아 구현합니다.
사용자가 웹 브라우저로 서버에 접근하면 해당 요청이 Socket 서버가 이를 서블릿으로 변환되어 알맞은 서블릿 구현체로 전달됩니다.
앞에서 설명한 바와 같이, 이 서버로 전달된 메시지는 Socket 프로그램에서 해석되고 재조합돼 개발자가 작성한 서블릿으로 전달되는 과정을 일어납니다.
이때 [HttpServletRequest] 객체와 [HttpServletResponse] 객체를 가지는 service 메서드가 호출되며 브라우저로부터 전달받은 메시지는 [HttpServletRequest] 인터페이스를 통해 사용할 수 있고 다시 브라우저에 메시지를 전달할 때 [HttpServletResponse] 를 활용합니다.
public class SimpleHTTPServer3 {
... 생략 ...
httpServletRequest.setHeaders(REQUEST_HEADERS); <===== ❶
httpServletRequest.setParamaters(REQUEST_PARAMATERS); <===== ❷
httpServletRequest.setBody(getBody(in)); <===== ❸
if ("/".equals(httpServletRequest.getRequestURI())) { <===== ❹
HttpServlet servlet = null;
try {
Class clazz = Class.forName("io.openmaru.test.web.IndexServlet"); <===== ❺
Constructor> constructor = clazz.getConstructor();
servlet = (HttpServlet) constructor.newInstance();
servlet.init(); <===== ❻
servlet.service(httpServletRequest, httpServletResponse); <===== ❼
httpServletResponse.setStatus(200); <===== ❽
} catch (Exception e) {
httpServletResponse.setStatus(500); <===== ❾
} finally {
servlet.destroy(); <===== ❿
}
} else { <===== ⓫
httpServletResponse.getWriter().print("404 Not Found");
httpServletResponse.setContentType("text/html;charset=UTF-8");
httpServletResponse.setStatus(404);
}
httpServletResponse.getWriter().flush();
httpServletResponse.getWriter().close();
httpServletResponse.setContentLength(tmpResponseBody.length()); <===== ⓬
String responseFirstLine = httpServletRequest.getHttpVersion() + " " + httpServletResponse.getStatus() + " ";
if (httpServletResponse.getStatus() == 200) {
responseFirstLine += "OK";
} else if (httpServletResponse.getStatus() == 404) {
responseFirstLine += "Not Found";
} else {
responseFirstLine += "INTERNAL_SERVER_ERROR";
}
out.println(responseFirstLine); <===== ⓭
Collection headerNames = httpServletResponse.getHeaderNames();
for (String headerName : headerNames) {
out.println(headerName + ": " + httpServletResponse.getHeaders(headerName)); <===== ⓮
}
out.println(); <===== ⓯
if (tmpResponseBody.exists()) {
try (BufferedReader fileIn = new BufferedReader(new FileReader(tmpResponseBody))) {
String content;
while ((content = fileIn.readLine()) != null) {
out.println(content); <===== ⓰
}
}
}
out.flush();
... 생략 ...
❶~❸ 을 통해 브라우저에서 보낸 요청을 분석하고 [HttpServletRequest] 객체에 설정한 후 ❹ “/” 요청에 대한 처리를합니다. 만약 일치하는 URI 에 대한 구분이 없다면 ⓫ 과 같이 404 페이지 없음을 돌려줍니다.
❹ “/” 요청에 대한 구현체인 ❺ IndexServlet.class 객체를 Load 하여 인스턴스화 한 후 ❻ init 메소드 호출 후 ❼ service 메소드에 HttpServletRequest/Response 객체 인스턴스를 파라미터로 호출합니다.
그러면 아래의 [IndexServlet] 의 service 메소드가 호출되어 실행됩니다.
구현체의 결과에 따라 ❽, ❾ 에 브라우저에 보내어질 HTTP 상태코드가 설정되고 마지막으로 ❿ 와 같이 destroy 메소드가 호출되며 IndexServlet 서블릿은 종료됩니다.
이후 ⓬ 보내질 데이터의 길이, ⓭ 기본 정보, ⓮ 응답 헤더 정보, ⓯ 공백 라인 이후 ⓰ 응답 컨텐츠 바디 부분을 마지막으로 설정하면 브라우저와 연결된 Socket 연결은 종료되고 사용자 브라우저 화면에 출력됩니다.
※ 서블릿의 service 메소드 이외 init, destroy 의 과정은 현재 소스에서는 호출 시 매번 실행되지만 스펙상의 Servlet Lifecycle 에서는 초기화 시 1회에만 init 이 호출되고, 서블릿 엔진이 종료되는 시점에 destroy 이 1회 호출됨
(즉 엔진이 시작되거나 구현 서블릿이 최초 호출되는 시점에 init 메소드 1회 실행, 엔진이 종료되는 시점에 destroy 메소드 1회 실행)
※ 실제로 ❺ [IndexServlet] 을 읽어오는 부분은 엔진과 달리 별도의 공간에 배치되고 엔진이 시작될 때 Load 되지만 작동 원리를 이해하는 소스로 해당 부분은 구현에서 제외함
위 명세 및 구현 HTTPServer 부분은 웹 애플리케이션 서버에 해당하는 부분으로 개발자가 신경쓰지 않아도 되는 부분이지만, 이후부터는 개발자가 웹 애플리케이션을 만들어가는 코드 영역입니다.
package io.openmaru.test.web;
import io.openmaru.test.util.Log;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class IndexServlet extends HttpServlet { <===== ❶
@Override
public void init() {
Log.info("[init] " + getClass().getName() + " 시작");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { <===== ❷
exec(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { <===== ❸
exec(request, response);
}
private void exec(HttpServletRequest request, HttpServletResponse response) throws IOException { <===== ❹
Log.info("[exec] getMethod: " + request.getMethod()); <===== ❺
Log.info("[exec] getRequestURI: " + request.getRequestURI());
Log.info("[exec] getQueryString: " + request.getQueryString());
Log.info("[exec] getRemoteAddr: " + request.getRemoteAddr());
Log.info("[exec] getContentType: " + request.getContentType());
Log.info("[exec] getContentLength: " + request.getContentLength());
Log.info("[exec] getParameter name: " + request.getParameter("name"));
Log.info("[exec] getParameter age: " + request.getParameter("age"));
// 업무 로지 작성 (예로 DBMS 에서 데이터 조회 후 출력) <===== ❻
response.setContentType("text/html;charset=UTF-8"); <===== ❼
response.getWriter().print("Hello, World!"); <===== ❽
}
@Override
public void destroy() {
Log.info("[destroy] " + getClass().getName() + " 종료");
}
}
IndexServlet 은 ❶ HttpServlet 를 상속받아 구현합니다.
브라우저에서 “/” 요청 후 Socket 연결이 완료되면 요청 정보에 의해 HttpServletRequest 객체가 셋팅되고, 응답을 위한 HttpServletResponse 객체가 준비됩니다.
그런 후 HTTP Method Type에 따라 ❷ doGet(), ❸ doPost() 함수를 호출합니다.
편의를 위해 모든 호출은 ❹ exec 함수를 호출하게 하였고 ❺ 와 같이 브라우저에서 요청 시 보내온 정보를 출력합니다.
❻ 영역에서는 DBMS 와 연결 후 데이터를 가져온 다던지, 다른 서비스의 REST API 를 호출하여 결과를 얻을 수 있습니다.
그런 후 ❼ 브라우저에 보낼 메시지 타입을 설정하고 ❾ 화면에 출력한 HTML 문자열을 지정합니다.
이렇게 간단한 서블릿 API 를 구현하여 “/” 요청을 처리하는 서블릿 프로그램을 만들어 보았습니다.
이러한 규칙을 가지고 개발자는 특정 요청에 대한 기능을 HttpServlet 을 상속받아 구현하기만 하면 됩니다.
[그림 3,4] 에서 처럼 브라우저에서 “/” 요청을 다시 해봅니다.
[그림 5] IndexServlet GET/POST 호출 실행 화면
[그림 3,4] 브라우저 개발자 도구(F12) 로 확인한 정보를 [그림 5] 와 같이 APM 에서 확인해 봅니다.
요청에 대한 URL 을 포함하여 기본정보, Header, Cookie, HTTP Status 응답코드가 잘 나오는 것을 확인 할 수 있습니다.
지금의 웹 어플리케이션 서버에서 응답속도가 0.5 초인 기능을 10명의 사용자가 동시에 요청하면 초당 처리 수(TPS) 는 20 이 나올까요?
예를 들어 “/” 에 대한 평균 응답속도가 0.5 초라고 가정하고 10명의 사용자가 동시에 요청하는 부하 테스트를 진행해 보겠습니다.
❻ 영역에 응답이 지연되는 현상을 아래와 같이 추가하고 Apache JMeter 부하테스트 도구로 10명의 사용자가 10번씩 호출하도록 하고 측정해 보겠습니다.
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
[그림 6-1] “/” 호출에 대한 JMeter 화면
[그림 6-2] “/” 호출에 대한 OPENMARU APM 화면
[그림 6-1,2] 를 확인해 보면 초당 처리 수(TPS) 2 이고, JMeter 로 확인된 사용자의 평균 응답속도는 4.78 초로 많이 느린것을 확인할 수 있습니다.
그 이유는 [소스 SimpleHTTPServer3] 의 경우 싱글 스레드로 동작하고 있어 IndexServlet 의 응답속도가 0.5 초로 초당 2회밖에 처리할 수 없어 발생하는 문제입니다.
이러한 싱글 스레드 문제를 해결하려면 당연하게도 멀티 스레드를 활용해야 합니다.
그러기 위해서는 스레드에 대해서 알아야 하는데요. 스레드와 관련된 기술은 챕터 3에서 설명드리겠습니다.
이구용 (ddakker@openmaru.io)
R&D Center
Pro
2023년 9월 충청 지역 클라우드 네이티브 세미나 자료 다운로드
/in Cloud, OPENMARU, Seminar, 발표자료/by 주하 원2023년 8월 충청권 공공기관에서 진행된 찾아가는 클라우드 네이티브 세미나
/in Cloud, Seminar, 발표자료/by 주하 원찾아가는 클라우드 네이티브 세미나 – 인천 소재 공공기관
/in Cloud, Seminar, 발표자료, 오픈나루 공지사항/by 주하 원