
프로젝트와 병행하느라 호로록 지나가버린 레벨 4 미션을 다시 돌아보고 정리한다. 다시 구현하는 미션임에도 어려운 개념들이 많아 계속해서 내용을 보충하고자 한다.
HTTP 데이터를 처리하기 위해서는 InputStream과 OutputStream에 대해 이해해야 한다.
InputStream은 바이트 단위로 데이터를 읽으며 데이터를 읽기 위해 read() 메서드를 사용한다. InputStream은 추상 클래스이기 때문에 FileInputStream, ByteArrayInputStream과 같은 서브 클래스들을 통해 인스턴스화할 수 있다.
InputStreamReader는 InputStream과 다르게 문자열을 다룬다. 바이트를 문자(char)로 처리하려면 인코딩에 신경 써야 한다. InputStreamReader를 사용하면 지정된 인코딩에 따라 유니코드 문자로 변환할 수 있다. 자바의 기본 문자열은 UTF-16 유니코드 인코딩을 사용한다.
다음은 InputStream과 InputStreamReader를 통해 문자열을 읽는 코드이다.
byte[] bytes = {-16, -97, -92, -87};
InputStream inputStream = new ByteArrayInputStream(bytes);
// inputStream에서 바이트로 변환한 값을 문자열로 바꾸기 위해 Reader를 사용한다.
Reader reader = new InputStreamReader(inputStream);
// -1을 반환할 때까지 계속 값을 읽어들인다.
StringBuilder stringBuilder = new StringBuilder();
for (int data = reader.read(); data != -1; data = reader.read()){
// 바이트를 문자(char)로 읽어온다.
stringBuilder.append((char) data);
}
assertThat(stringBuilder.toString()).isEqualTo("🤩");
BufferedInputStream을 사용하면 데이터를 버퍼에 저장하여 처리 속도를 높인다. BufferedReader는 readLine() 이라는 메서들 제공하는데, 캐리지 리턴 \r 라인피드 \n 으로 구분된 행 단위의 문자열을 한 번에 가져올 수 있다. 그리고 더 이상 읽어올 데이터가 없다면 null을 반환한다.
String emoji = String.join("\n",
"😀😃😄😁😆😅😂🤣🥲☺️😊",
"😇🙂🙃😉😌😍🥰😘😗😙😚",
"😋😛😝😜🤪🤨🧐🤓😎🥸🤩",
"");
InputStream inputStream = new ByteArrayInputStream(emoji.getBytes());
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder actual = new StringBuilder();
String line = "";
while ((line = br.readLine()) != null) {
actual.append(line).append("\n");
}
서블릿은 동적인 웹 페이지를 만들기 위해 사용되는 자바 프로그래밍 기술이다. 서블릿은 클라이언트의 요청을 처리하고 동적인 데이터를 생성하여 그 결과를 클라이언트에게 반환하는 자바 인터페이스이다.
HTTP 요청과 응답은 다음과 같은 형식으로 이루어져 있다.

서블릿은 위와 같은 문자열을 파싱해서 HttpSerlvetRequest, HttpServletResponse를 만들어준다. 서블릿이 없었다면 손수 하나하나 파싱해줘야 한다는 사실 ,, 서블릿 덕분에 우리는 문자열 파싱에 열을 올리지 않고 비즈니스 로직에만 집중할 수 있다.
이러한 서블릿들을 관리해주는 것이 서블릿 컨테이너이다. 서블릿 컨테이너는 서블릿들의 생성, 실행, 파괴, 즉 생명주기를 담당한다. 서블릿 컨테이너는 구현되어 있는 서블릿 클래스의 규칙에 맞게 서블릿을 관리해주며 클라이언트의 요청에 따라 HttpServletRequest, HttpServletResponse 두 객체를 생성하여 HttpMethod 여부에 따라 동적인 페이지를 생성하여 응답을 내보낸다. 서블릿 컨테이너의 대표적인 예시로는 톰캣이 있으며 톰캣은 웹 서버와 통신하여 JSP(자바 서버 페이지)와 Servlet이 작동하는 환경을 제공해준다.

자바에서 제공하는 서블릿 인터페이스에는 다음의 3가지 메서드가 있다.
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
void destroy();
}
init()
서블릿을 초기화하는 메서드이다. 한 번 초기화된 서블릿은 싱글톤으로 관리되어 한 번 더 해당 서블릿 클래스를 호출하면 초기화가 다시 일어나는 것이 아니라 기존에 있던 서블릿 클래스를 호출한다.
service()
서블릿이 요청에 응답하도록 서블릿 컨테이너에서 호출되는 메서드이다.
destroy()
더 이상 사용되지 않는 서블릿 클래스는 주기적으로 서블릿 컨테이너가 destroy() 메서드를 호출하여 제거된다.
서블릿 컨테이너는 다음과 같은 XML 설정 파일을 읽어 요청을 처리할 수 있는 서블릿을 호출한다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>controller.MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>myServlet</servlet-name>
<url-pattern>/myServlet</url-pattern>
</servlet-mapping>
</web-app>

톰캣을 구성하는 큰 단위로는 3가지가 있다.
3가지의 구성 요소를 다음과 같이 동작한다.
Connector
클라이언트와의 통신을 담당하는데, HTTP 요청을 받아들이고 처리하는 역할을 한다. 톰캣의 기본 커넥터는 HTTP/1.1 표준에 따라 클라이언트로부터의 요청을 처리한다. 또한, 커넥터는 특정 포트(기본적으로 8080)에서 요청을 기다린다.
Engine
톰캣의 핵심 역할을 담당한다. 커넥터로부터의 요청을 처리하여 클라이언트에 전송할 적절한 커넥터로 응답을 다시 전달한다. 톰캣에는 기본적으로 Catalina 엔진이 포함된다.
Context
톰캣 내의 단일 웹 애플리케이션(*.war 파일)을 의미한다.
Tomcat 3.2 이전 버전에서는, 유저의 요청이 들어올 때마다 서블릿을 실행할 스레드를 하나씩 생성하고 요청이 끝나면 소멸했다. 하지만 이 같은 방식은 OS와 JVM에 대해 많은 부담을 안겨주었고, 동시에 다수의 요청이 들어오면 순간적으로 서버가 다운되었다.
톰캣은 이런 문제들을 해결하기 위해 스레드풀을 활용하기 시작한다.

스레드풀을 자바에서 구현한 구현체가 ThreadPoolExecutor이다. 다음과 같이 스레드 풀의 사이즈를 2로 지정하고 3개의 스레드를 submit하면 2개는 실행되고 나머지 1개는 큐에서 대기한다.
@Test
void testNewFixedThreadPool() {
final var executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
executor.submit(logWithSleep("hello fixed thread pools"));
final int expectedPoolSize = 2;
final int expectedQueueSize = 1;
assertThat(expectedPoolSize).isEqualTo(executor.getPoolSize());
assertThat(expectedQueueSize).isEqualTo(executor.getQueue().size());
}
스프링부트는 톰캣을 내장하고 있기 때문에 다음과 같이 application.yml 파일을 통해 톰캣 설정을 변경해줄 수도 있다.
server:
tomcat:
threads:
max: 200 # 생성할 수 있는 스레드의 총 개수
min-spare: 10 # 활성화 되어 있는 스레드 개수
accept-count: 100 # 작업큐의 사이즈
스레드 요청이 min-spare값을 넘어서면 max값까지 스레드를 계속 생성한다. 그리고 max값도 넘어서면 작업 큐에 스레드를 대기시킨다.
참고
자바 서블릿에 대해 알아보자. 근데 톰캣과 스프링을 살짝 곁들인
서블릿이란?
스프링부트는 어떻게 다중 유저 요청을 처리할까? (Tomcat9.0 Thread Pool)
Tomcat - (2) 구조
WAS의 Thread Pool 동작 원리 부분에서 yml파일 예시의 min-spare에 대한 설명을 더 명확하게 수정하면 좋을 것 같아요. '기본적으로 활성화 되어 있는 스레드 개수' or '항상 활성화 되어 있는 스레드 개수' 라는 표현이 더 명확할 것 같아요.