작성해야 할 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)
이구용 (ddakker@openmaru.io)
R&D Center
Pro













클라우드 네이티브 무상 세미나 – 서울시 소재 지자체
/카테고리: OPENMARU, Seminar, 발표자료/작성자: OM marketing클라우드 네이티브를 위한 Observability와 모니터링 방안 : K-AI PaaS Summit 2024 발표자료
/카테고리: Container, Kubernetes, OPENMARU, Seminar, 발표자료, 분류되지 않음/작성자: OM marketing클라우드 네이티브 무상 컨설팅 – 서울 강서 소재 미디어 커머스 기업
/카테고리: OPENMARU, Seminar, 발표자료/작성자: OM marketing