레벨 4로 접어들면서 톰캣 만들기 미션을 하게 되었는데 제공받은 초기 코드부터가 흥미로웠다.
public class Application {
private static final Logger log = LoggerFactory.getLogger(Application.class);
public static void main(String[] args) {
log.info("web server start.");
final var tomcat = new Tomcat();
tomcat.start();
}
}
위 코드만 실행 시켜도 자바 애플리케이션이 꺼지지도 않으면서 localhost:8080으로 오는 요청을 받고 있는 것이 신기해서 소스 코드를 한 번 분석해 보았다.
Application {
final var tomcat = new Tomcat();
tomcat.start();
- Tomcat 클래스 구동 (start())
Tomcat {
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 객체 생성 및 구동
- 콘솔에 입력이 들어올 때까지 대기 (애플리케이션을 중단시키지 않기 위해)
- 예외가 발생하면 Connector 작동 중지
Connector {
public class Connector implements Runnable {
private static final int DEFAULT_PORT = 8080;
private static final int DEFAULT_ACCEPT_COUNT = 100;
private final ServerSocket serverSocket;
private boolean stopped;
public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT);
}
public Connector(final int port, final int acceptCount) {
this.serverSocket = createServerSocket(port, acceptCount);
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);
}
}
- Connector는 Runnable 인터페이스를 구현하고 있다.
- Runnable 인터페이스를 구현하는 클래스는 run() 메서드를 재정의 해야 한다.
- Runnable 구현 클래스가 스레드를 생성하는 데 사용되는 경우 스레드를 시작하면 별도의 스레드에서 run() 메서드가 호출된다.
- ServerSocket과 현재 중단 상태인지를 나타내는 stopped 필드를 가지고 있다.
- ServerSocket은 서버 소켓의 구현 클래스이다. 서버 소켓은 네트워크로부터 요청이 오기를 기다린다.
- 요청이 오면 작동되고, 결과를 반환해 준다.
- 위 코드에서는 기본 port와 acceptCount로 서버 소켓을 생성하고 있다. (기본 생성자를 사용했기 때문)
- 위 코드로 생성된 소켓은 지정된 포트에 바인딩되며 acceptCount만큼의 최대 큐 길이를 가질 수 있다. 큐가 가득 찼을 때는 연결이 거부된다.
start() {
var thread = new Thread(this);
thread.setDaemon(true);
thread.start();
stopped = false;
- Tomcat 클래스에서 connector.start() 메서드를 호출했으니 위 코드가 실행된다.
- Connector 자신으로 스레드를 생성했기에 thread.start() 메서드에 의해 connector.run() 메서드가 호출될 것이다.
- thread.setDemon(true)로 의해 위 스레드는 데몬 스레드가 된다.
- 주 스레드의 작업을 돕는 보조적인 역할을 함
- 일반 스레드와 크게 다른 점은 애플리케이션을 종료할 때 살펴볼 수 있다.
- 애플리케이션이 종료될 때 일반 스레드 전부가 종료되어야 JVM 프로세스가 종료된다.
- 하지만 JVM은 데몬 스레드의 종료를 기다리지 않기 때문에 데몬 스레드를 그냥 셧다운 시킨다. 때문에 종료 시 특별한 처리가 필요한 작업이라면 데몬 스레드에서 실행하면 안 된다.
run() {
@Override
public void run() {
while (!stopped) {
connect();
}
}
private void connect() {
try {
process(serverSocket.accept());
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
- 스레드가 시작되었으니 run() 메서드가 호출되고 stopped 상태가 false이니 while 문이 돌면서 연결을 유지하게 된다.
- ServerSocket의 accept() 메서드를 호출하면 프로그램은 클라이언트와 포트로 연결될 때까지 대기한다.
- 연결이 이루어지면 Socket 객체를 반환한다.
process(final Socket connection) {
if (connection == null) {
return;
}
log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort());
var processor = new Http11Processor(connection);
new Thread(processor).start();
- ServerSocket이 연결을 통해 반환한 Socket connection은 연결을 요청한 클라이언트로부터의 정보가 들어 있다.
- 이 요청을 토대로 서버는 적절한 결과를 리턴해주면 된다.
- HTTP request 형식으로 왔을 정보를 알맞은 결과로 반한하기 위해 Runnable을 구현한 Http11Processor에게 connection을 넘겨 주어 새로운 스레드에서 요청을 처리하도록 넘긴다.
- 그럼 Connector는 다시 while 문으로 돌아가 stopped가 true가 될 때까지 다른 요청을 기다리며 계속 들어오는 새로운 요청을 여러 스레드에게 처리하도록 맡길 것이다.
while (!stopped) {
connect();
}
Http11Processor {
@Override
public void run() {
process(connection);
}
@Override
public void process(final Socket connection) {
try (final var inputStream = connection.getInputStream();
final var outputStream = connection.getOutputStream();
final var requestReader = new BufferedReader(new InputStreamReader(inputStream))
) {
HttpRequest request = new HttpRequest(requestReader);
log.info("REQUEST \r\n{}", request);
final var response = handleHttpRequest(request);
log.info("RESPONSE \r\n{}", response);
outputStream.write(response.getBytes());
outputStream.flush();
} catch (IOException | UncheckedServletException e) {
log.error(e.getMessage(), e);
}
}
- 이제 들어온 요청을 처리하는 로직을 살펴보자
- 스레드로 생성하여 시작하였으니 똑같이 run(), process() 메서드가 차례로 실행될 것이다.
- Socket에서 InputStream과 OutputStream을 꺼낼 수 있는데 InputStream으로부터 요청 메시지를 읽어올 수 있고, OutputStream으로 알맞은 결과를 응답 메시지를 쓸 수 있다.
- BufferedReader를 통해 InputStream을 읽으면서 로직을 처리하여 OutStream.write()를 통해 결과를 반환하면 한 요청에 대한 프로세스가 종료가 된다.
- 위 코드에 있는 HttpRequest와 HttpResponse 그리고 handleHttpRequest() 메서드는 필자가 설계해 본 구조인데 이런 흐름으로 요청을 처리하는 것이 톰캣 만들기 미션을 하면서 이해한 간단한 웹서버 작동 원리다.
'우아한테크코스' 카테고리의 다른 글
Level2 지하철 노선도 미션 피드백 정리 (feat. dao, repository) (0) | 2022.05.15 |
---|---|
Level2 Spring 체스 피드백 정리 (0) | 2022.05.06 |
Level1 체스 미션 피드백 정리 (0) | 2022.04.10 |
수업 따라하기 (Gradle 프로젝트에 Docker로 mysql 접속) (0) | 2022.03.30 |
Level1 블랙잭 미션 피드백 정리 (0) | 2022.03.21 |