이번 미션은 Tomcat 구현하기였는데, 미션 마감일에 쫒겨 코드를 치기 바빴어서 한번 흐름을 정리해보려고 한다.
사실 구현한 게 Tomcat 보다는 WAS 에 가깝다고 한다.
제출한 코드는 여기 있다.
package com.techcourse;
import org.apache.catalina.startup.Tomcat;
public class Application {
public static void main(String[] args) {
final var tomcat = new Tomcat();
tomcat.start();
}
}
👩🏻💻 Tomcat
의 start
메서드를 실행시키는 것밖에 없구나
package org.apache.catalina.startup;
import org.apache.catalina.connector.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class Tomcat {
private static final Logger log = LoggerFactory.getLogger(Tomcat.class);
public void start() {
var connector = new Connector();
connector.start();
try {
// make the application wait until we press any key.
System.in.read();
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
log.info("web server stop.");
connector.stop();
}
}
}
👩🏻💻 여기선 또 Connector
의 start()
를 실행시키네
헷갈리니까 일단 Connector
클래스 전체 코드를 두고 시작하는 것이 나을 것 같다.
package org.apache.catalina.connector;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.coyote.http11.Http11Processor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Connector implements Runnable {
private static final Logger log = LoggerFactory.getLogger(Connector.class);
private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_ACCEPT_COUNT = 100;
private static final int DEFAULT_MAX_THREADS = 250;
private final ServerSocket serverSocket;
private final ExecutorService executorService;
private boolean stopped;
public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREADS);
}
public Connector(int port, int acceptCount, int maxThreads) {
this.serverSocket = createServerSocket(port, acceptCount);
this.executorService = Executors.newFixedThreadPool(maxThreads);
this.stopped = false;
}
private ServerSocket createServerSocket(final int port, final int acceptCount) {
try {
final int checkedPort = checkPort(port);
final int checkedAcceptCount = checkAcceptCount(acceptCount);
return new ServerSocket(checkedPort, checkedAcceptCount);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public void start() {
var thread = new Thread(this);
thread.setDaemon(true);
thread.start();
stopped = false;
log.info("Web Application Server started {} port.", serverSocket.getLocalPort());
}
@Override
public void run() {
// 클라이언트가 연결될때까지 대기한다.
while (!stopped) {
connect();
}
}
private void connect() {
try {
process(serverSocket.accept());
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
private void process(final Socket connection) {
if (connection == null) {
return;
}
var processor = new Http11Processor(connection);
executorService.execute(processor);
}
public void stop() {
stopped = true;
try {
serverSocket.close();
shutdown();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
private int checkPort(final int port) {
final var MIN_PORT = 1;
final var MAX_PORT = 65535;
if (port < MIN_PORT || MAX_PORT < port) {
return DEFAULT_PORT;
}
return port;
}
private int checkAcceptCount(final int acceptCount) {
return Math.max(acceptCount, DEFAULT_ACCEPT_COUNT);
}
public void shutdown() {
executorService.shutdown();
}
}
Connector
의 start()
public void start() { //Connector 의 start()
var thread = new Thread(this); //this 는 Connector
thread.setDaemon(true);
thread.start();
stopped = false;
log.info("Web Application Server started {} port.", serverSocket.getLocalPort());
}
👩🏻💻 new Thread(this);
는 뭐지?
👩🏻💻 Connector
가 Runnable
을 구현하고 있구나.
👩🏻💻 new Thread(runnable)
로 Thread
객체를 생성한 것까지 알았다.
그다음 thread.setDaemon(true);
데몬 스레드로 설정하고, thread.start();
를 한다.
데몬 스레드는 주 스레드가 종료되면 함께 종료되며, 주로 백그라운드 작업을 수행하는 데 사용된다.
즉, 메인 스레드는 별도의 작업을 계속할 수 있고 새로운 스레드는 메인 스레드와는 독립적으로 백그라운드 작업을 처리한다.
(참고) 메인 스레드는 JVM이 애플리케이션을 시작할 때 생성되며, 애플리케이션의main
메서드를 실행하는 스레드이다.
thread.start();
를 들어가보자.start0();
까지 들어가보니 네이티브 메서드이다.
이 네이티브 메서드는 자바 표준 라이브러리 코드 외부에서 구현되고, JVM 구현에 따라 처리하는 방법이 다르다.
어쨌든 이 메서드를 통해 실제 OS 스레드를 생성하고, 이 스레드는 Thread
의 run()
메서드를 호출한다고 한다.
해당 메서드의 구현이 궁금하다면 JVM 의 소스 코드를 보면 된다. (OpenJDK GitHub Repo)
다만 C++ 로 작성되어있다 🥹
그런데 Runnable
객체를 생성하고 이를 Thread
에 전달한 경우, Thread
의 run()
메서드는 Runnable
의 run()
메서드를 호출하도록 설정된다. 이 설정은 Thread
클래스의 생성자에서 처리된다.
Thread
의 run()
메서드Thread
클래스의 생성자 (너무 길어서 잘렸다.)생성자가 되게 긴데.. 일단 아래 부분만 봐도 된다.
this.holder = new FieldHolder(g, task, stackSize, priority, parent.isDaemon());
이 스레드의 주요 속성을 저장하는 클래스인 FieldHolder
에 Runnable
이 저장된다.
👩🏻💻 그럼 이제 Connector
의 run()
메서드가 호출되겠구나!
Connector
의 run()
@Override
public void run() {
// 클라이언트가 연결될때까지 대기한다.
while (!stopped) {
connect();
}
}
private void connect() {
try {
process(serverSocket.accept());
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
private void process(final Socket connection) {
if (connection == null) {
return;
}
var processor = new Http11Processor(connection);
executorService.execute(processor);
}
run();
→ connect();
→ process(serverSocket.accept());
👩🏻💻 process
메서드가 Socket
클래스를 인자로 받고 있다. 이게 뭐지?
serverSocket.accept()
로 전달하고 있으니, serverSocket
이 초기화 되는 Connector
클래스의 생성자 부분을 살펴보자.
ServerSocket
과 ExecutorService
private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_ACCEPT_COUNT = 100;
private static final int DEFAULT_MAX_THREADS = 250;
private final ServerSocket serverSocket;
private final ExecutorService executorService;
private boolean stopped;
public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREADS);
}
public Connector(int port, int acceptCount, int maxThreads) {
this.serverSocket = createServerSocket(port, acceptCount);
this.executorService = Executors.newFixedThreadPool(maxThreads);
this.stopped = false;
}
private ServerSocket createServerSocket(final int port, final int acceptCount) {
try {
final int checkedPort = checkPort(port);
final int checkedAcceptCount = checkAcceptCount(acceptCount);
return new ServerSocket(checkedPort, checkedAcceptCount);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
생성자에서 ServerSocket
과 ExecutorService
를 만들고 있다.
ServerSocket
이 뭔지 이해하려면 클라이언트와 서버가 데이터를 어떻게 주고 받는지 부터 이해해야 한다.
- 클라이언트가
Socket
을 생성해서 서버에 연결을 요청한다.
- 웹 브라우저가 서버에 HTTP 요청을 보내기 위해
Socket
을 생성한다.- 웹 브라우저가 직접 소켓을 생성하는 건 아니고, 소켓을 생성해달라는 요청을 운영체제에 보낸다.
- 소켓 생성 요청은
Socket
API 를 호출한다. (Java에Socket
클래스가 있다.)- 운영체제는 소켓을 생성하고, 서버와의 TCP 연결을 설정한다.
- 서버의
ServerSocket
이 클라이언트의 연결 요청을 기다리고, 그 요청을 받으면accept()
로 새로운Socket
을 생성한다.- 이 새로운
Socket
을 통해 클라이언트와 서버가 데이터를 주고받는다.
👩🏻💻 아하 그럼 ServerSocket
이 클라이언트의 연결 요청을 기다리다가, 요청이 오면 accept()
로 새로운 Socket
이 생성되는구나!
👩🏻💻 근데 위에서 Connector
의 run()
메서드가 호출이 되면 while 문의 connect()
가 반복적으로 호출이 되는 것 같은데, 그럼 connect()
의 serverSocket.accept()
도 계속 호출이 되네.
여기서
accept()
는 블로킹 메서드이기 때문에 새로운 연결 요청이 올 때까지 실행을 멈추고 대기한다.
- 반복적으로
connect()
가 호출되면서, 내부적으로accept()
메서드를 호출하게 된다.accept()
는 새로운 클라이언트 연결 요청이 들어올 때까지 대기하는 상태로, 호출은 완료되었지만 클라이언트가 연결을 시도하기 전까지 아무런 동작을 하지 않는다. (블로킹 상태)- 클라이언트가 연결 요청을 보내면, 그때
accept()
가 새로운Socket
을 반환하고, 연결이 수립된다.
출처) ServerSocket (Java Platform SE 8 )
👩🏻💻 그럼 클라이언트에게 연결 요청을 받았을 때 새로운 Socket
과 함께 process
가 실행되는구나!
private void process(final Socket connection) {
if (connection == null) {
return;
}
var processor = new Http11Processor(connection);
executorService.execute(processor);
}
👩🏻💻 그런데 Http11Processor
를 executorService
가 실행시키네.
👩🏻💻 executorService
는 또 뭐지?
아까 생성자에서 ExecutorService
를 만들고 있다고 했다. (참고 3️⃣-4️⃣)
this.executorService = Executors.newFixedThreadPool(maxThreads);
executorService
는 스레드 풀을 관리하는 클래스이다.
스레드 풀은 미리 여러 개의 스레드를 생성해 두고, 필요한 작업이 들어오면 그 작업을 풀에 있는 미리 준비된 스레드에 할당한다.
newFixedThreadPool()
메서드를 사용하여 고정된 개수의 스레드 풀을 만들 수 있다.
클라이언트 요청이 들어오면, 스레드 풀에서 빈 스레드를 할당하여 그 요청을 처리한다.
👩🏻💻 아하 그럼 클라이언트의 요청이 들어올 때마다 Http11Processor
같은 작업을 스레드 풀에서 처리하는구나
👩🏻💻 process
메서드는 클라이언트와의 연결(Socket
)을 받아서 해당 연결을 처리할 Http11Processor
에 넘기는 부분이겠다.
new Thread()
도 있고 newFixedThreadPool()
도 있네
- 연결을 기다리는 스레드: 클라이언트의 요청을 계속 기다리는 스레드.
- 연결이 들어오면 작업을 처리하는 스레드: 새로운 연결이 들어오면 이를 처리하는 스레드.
start()
에서 실행되는 스레드Connector
클래스의 start()
메서드는 애플리케이션에서 단 한 번 호출된다. 이 메서드가 호출되면 새로운 스레드를 생성하여 이 스레드가 클라이언트의 연결 요청을 기다리게 된다.executorService
를 이용해 스레드 풀에서 처리한다.Http11Processor
는 HTTP 요청을 처리하는 클래스이다.
해당 부분부터는 직접 구현했기 때문에, 간략하게 설명만 남겨두려고 한다!
@Override
public void process(Socket connection) {
try (InputStream inputStream = connection.getInputStream();
OutputStream outputStream = connection.getOutputStream()) {
BufferedReader request = new BufferedReader(new InputStreamReader(inputStream));
HttpRequest httpRequest = HttpRequest.from(request);
HttpResponse httpResponse = new HttpResponse();
requestMapping.getController(httpRequest)
.service(httpRequest, httpResponse);
outputStream.write(httpResponse.getBytes());
outputStream.flush();
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
try (InputStream inputStream = connection.getInputStream();
OutputStream outputStream = connection.getOutputStream()) {
InputStream
과 OutputStream
을 사용하여 클라이언트와의 데이터 입출력을 처리한다.connection.getInputStream()
으로 클라이언트의 요청을 읽고, connection.getOutputStream()
으로 응답을 클라이언트에 보낸다.BufferedReader request = new BufferedReader(new InputStreamReader(inputStream));
HttpRequest httpRequest = HttpRequest.from(request);
BufferedReader
를 통해 요청 스트림을 읽어들인다.HttpRequest.from(request)
를 사용해 HTTP 요청 객체를 생성한다.HttpResponse httpResponse = new HttpResponse();
requestMapping.getController(httpRequest).service(httpRequest, httpResponse);
HttpResponse
객체를 생성하여 응답을 준비한다.requestMapping.getController(httpRequest)
를 통해 요청에 대한 적절한 컨트롤러를 찾고, 해당 컨트롤러의 service
메서드를 호출하여 요청을 처리하고 응답을 만든다.outputStream.write(httpResponse.getBytes());
outputStream.flush();
HttpResponse.getBytes()
로 변환된 응답을 OutputStream
을 통해 클라이언트로 전송한다.flush()
를 호출하여 응답이 클라이언트에게 전달되도록 한다.
저 오늘 바뀐거 없나요?