- DB 인덱스 설정
- DB에 접근하는 쿼리
- 코드 내 자료구조 시간복잡도
- 사용 알고리즘의 효율성
- 데이터의 양 등등 .....
그러던 중 내가 그동안 간과하고, 의심조차 하지 않고 있던 녀석이 있음을 깨달은 것이다.
결국 내 서버는 WAS, Servlet Container 역할인 Tomcat을 통해 Servlet 생성 & 응답을 생성한다.
이론적으로 웹 서버는 정적인 것 담당, WAS는 동적인 역할 담당을 한다. (DB 접근, 사용자 역할 담당) 이렇게만 알고 있었다. 더 나아가서 서블릿 컨테이너 역할로서, 서블릿의 생명주기를 담당한다 정도!?
그런데 서버 개발자로서 , 그리고 지금 실제 사용자들이 사용하는 SW 를 담당하는 입장으로서, 내 서버 내의 WAS가 뭘 어떻게 하고 있는지 알아야 할 것 아닌가!? 그래서 파헤쳐보기로 했다.
유명한 개발자 필수 상식 질문인 "WWW.XXX.COM" 접속 시 어떤 일이 일어나는가에 대한 질문을 들어봤을 것이다.
해당 부분에 대해 그림을 통해 살펴보자
사진 출처
해당 부분에서 감이 안 잡히는 부분은 아래 연두색 코멘트 부분이다. (내가 작성)
추후 깊게 공부하면서 해당 부분은 네트워크 시간에 학습하는 TCP/IP Socket 통신
을 통해 구현된다는 것을 학습했다.
클라이언트 프로그램 (요청자) 과 서버 프로그램 (응답자) 은 각각 자신이 포트 (포트 번호는 TCP LAYER 계층에 존재) 를 통해 통신해야 한다. 연결을 할때도 포트를 사용하고 데이터를 교환할때도 포트를 사용한다. 자바 프로그램 안에서 포트를 사용하기 위해서는 소켓을 이용해야 한다. 자바안에서 소켓의 종류에는 서버소켓과 클라이언트 소켓이 있다.
위 두 소켓에 대한 내용이 정확히 tomcat 내부에서 등장하고, tomcat이 관리해주고 있는 것이다.
// 패키지 import
package org.apache.catalina.connector;
// IOException 및 UncheckedIOException을 위한 import
import java.io.IOException;
import java.io.UncheckedIOException;
// 네트워크 관련 클래스 import
import java.net.ServerSocket;
import java.net.Socket;
// 동시성 처리를 위한 ExecutorService 및 Executors import
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// Tomcat의 Coyote 컴포넌트에 속하는 클래스로, HTTP 1.1 프로토콜을 처리하는 역할을 담당
// 클라이언트의 HTTP 요청을 처리하고, 해당 요청에 대한 응답을 생성
import org.apache.coyote.http11.Http11Processor;
// 로깅을 위한 import
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// Connector 클래스 선언 및 Runnable 인터페이스 구현
public class Connector implements Runnable {
// 로깅을 위한 Logger 생성
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 ExecutorService executorService;
private final ServerSocket serverSocket;
private boolean stopped;
// 기본 생성자
public Connector() {
this(DEFAULT_PORT, DEFAULT_ACCEPT_COUNT, DEFAULT_MAX_THREADS);
}
// 생성자 오버로딩
public Connector(final int port, final int acceptCount, final int maxThread) {
// 고정 크기 스레드 풀 생성
this.executorService = Executors.newFixedThreadPool(maxThread);
// 서버 소켓 생성 및 포트 번호, 접속 허용 수 확인
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);
// ServerSocket 생성
return new ServerSocket(checkedPort, checkedAcceptCount);
} catch (IOException e) {
// IOException 을 UncheckedIOException 으로 변환하여 예외 처리
throw new UncheckedIOException(e);
}
}
// 서버 시작 메서드
public void start() {
// 새로운 스레드 생성 및 실행
var thread = new Thread(this);
// 백그라운드 스레드로 설정
thread.setDaemon(true);
thread.start();
stopped = false;
}
// Runnable 인터페이스의 run 메서드 구현
@Override
public void run() {
// 서버가 정지되지 않은 동안 계속해서 접속 요청 처리
while (!stopped) {
connect();
}
}
// 클라이언트의 접속 요청 처리 메서드
private void connect() {
try {
// 클라이언트의 접속 요청을 받아들임
process(serverSocket.accept());
} catch (IOException e) {
// IOException 로깅
log.error(e.getMessage(), e);
}
}
// 클라이언트 연결 처리 메서드
private void process(final Socket connection) { // 클라이언트와의 소켓 연결
if (connection == null) { // 매개변수 connection 이 null 인 경우 메서드를 종료하고 반환합니다.
return;
}
// 클라이언트의 호스트와 포트 정보를 로깅
log.info("connect host: {}, port: {}", connection.getInetAddress(), connection.getPort());
// Http11Processor 인스턴스 생성 *클라이언트의 요청을 처리하기 위한 처리기 역할
var processor = new Http11Processor(connection);
// ExecutorService : executorService.submit(processor)를 사용하여 processor 를 처리 작업으로 제출합
executorService.submit(processor);
// => executorService 는 스레드 풀에서 사용 가능한 쓰레드를 가져와서 processor 를 실행하게 됩니다.
}
// 서버 정지 메서드
public void stop() {
stopped = true;
try {
// 서버 소켓 닫기
serverSocket.close();
} catch (IOException e) {
// IOException 로깅
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);
}
}
순서 | 메서드명 | 역할 |
---|---|---|
1 | createServerSocket | 지정된 포트 번호와 접속 허용 수로 ServerSocket을 생성 |
2 | start | Connector를 별도의 스레드에서 실행하고 백그라운드로 설정 |
3 | run | 서버가 정지되지 않은 동안 클라이언트의 접속 요청을 처리 , Runnable 인터페이스 구현 |
4 | connect | 클라이언트의 접속 요청을 받아들이고 process 메서드 호출 |
5 | process | 클라이언트와의 소켓 연결을 처리하고 클라이언트의 요청을 Http11Processor로 전달 |
6 | stop | 서버를 정지시키고 ServerSocket을 닫음 |
클래스 | 역할 |
---|---|
ServerSocket | - 서버 프로그램에서 사용되는 소켓 - 지정한 포트를 통해 연결 요청이 오기를 대기 - 요청이 오면 클라이언트와 연결을 맺고 해당 클라이언트와 통신하는 새 소켓을 생성 |
Socket | - 서버 프로그램으로 연결 요청하는 클라이언트 소켓 - 서버와의 데이터 전송을 담당 |
서버 | 방향 | 클라이언트 |
---|---|---|
클라이언트의 요청을 받기 위한 준비를 한다.(ServerSocket) | ||
클라이언트의 요청을 받아 들인다. (accept) | <- | 서버에 접속 요청을 한다. (Socket) |
클라이언트가 보낸 데이터를 출력 한다. (BufferedReader) | <- | 서버에 메시지를 보낸다. ( BufferedWriter ) |
클라이언트에 메시지를 보낸다. ( BufferedWriter ) | -> | 서버가 보낸 메시지를 출력한다. ( BufferedReader ) |
종료 한다. ( socket.close() ) | 종료 한다. ( socket.close() ) |
// 클라이언트 연결 처리 메서드
private void process(final Socket connection) { // 클라이언트와의 소켓 연결
if (connection == null) { // 매개변수 connection 이 null 인 경우 메서드를 종료하고 반환합니다.
return;
}
// Http11Processor 인스턴스 생성 *클라이언트의 요청을 처리하기 위한 처리기 역할
var processor = new Http11Processor(connection);
// ExecutorService : executorService.submit(processor)를 사용하여 processor 를 처리 작업으로 제출합
executorService.submit(processor);
// => executorService 는 스레드 풀에서 사용 가능한 쓰레드를 가져와서 processor 를 실행하게 됩니다.
}
private final ExecutorService executorService
의 스레드 생성 시 역할Q ) Task는 뭐로 관리가 되나?
톰캣 구현 & 궁금증들을 이슈에 등록해놓은 깃허브 레포지토리