- WAS란
- 네트워크 연결 (with Socket) 구현
- Servlet Container & Spring Container 구현
- Servlet 구현
- MVC 구현
네트워크 연결 (with Socket) 구현
Web Application Server, 서버를 구현한다는 것은 무엇일까?
'클라이언트/서버'는 컴퓨터 간의 관계를 역할로 구분하는 개념이다.
서버는 서비스를 제공하는 컴퓨터이고, 클라이언트는 서비스를 사용하는 컴퓨터가 된다
<자바의 정석>
서버는 클라이언트와 소통하여 서비스를 제공한다.
만약 서버가 하는 일을 순서대로 나열한다면 클라이언트의 요청을 받고 클라이언트에게 응답을 되돌려주는 기능이 맨 앞과 맨 끝에 위치할 것이다.
Socket
0. 소켓 프로그래밍
서버가 클라이언트와 소통하려면 어떻게 해야 할까?
바로 Socket이란 걸 사용한다.
소켓은 프로세스 간 통신에 사용되는 양쪽 끝단(EndPoint)이다.
자바는 소켓을 쉽게 할 수 있도록 TCP, UDP 소켓 클래스를 제공한다.
이 프로젝트에서는 TCP 소켓 클래스를 사용하여 소켓 프로그래밍을 한다.
자바가 제공하는 TCP 소켓 클래스 & 입출력스트림
ServerSocket | 클라이언트와의 연결을 담당 |
Socket | 연결 성공 시 데이터 통신 담당 |
Input/OutputStream | 데이터가 통신하는 통로 |
서비스의 대략적인 흐름은 이렇다.
클라이언트와 연결 -> 데이터 받기 -> 작업 수행 -> 데이터 전달 -> 연결 해제
이번 포스트에서는 작업 수행을 제외한 나머지 단계를 네트워크 연결이라는 하나의 카테고리로 묶어 구현하고자 한다.
1. ServerSocket
public class WebServer {
private static final int DEFAULT_PORT = 8080;
public void start() {
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
}
}
}
a. WebServer라는 클래스를 만들고 start()라는 메서드를 만든다.
이 웹 애플리케이션을 구동하려면 WebServer 인스턴스를 생성하여 start() 메서드를 호출해야 한다.
b. ServerSocket 인스턴스를 생성하여 포트를 넘겨준다. 포트는 임의로 8080이라 정한다.
넘겨받은 포트에 결합(binding)되어 만들어진 서버소켓은 네트워크 연결 요청을 기다린다.
서버소켓은 바인딩할 포트, 연결 최대 개수, 바인딩할 주소를 설정할 수 있다.
public ServerSocket(int port, int backlog, InetAddress bindAddr)
c. try-catch-resource문을 사용하여 자원 반환 까먹는 걸 방지한다.
AutoCloseable을 구현한 객체만 사용 가능하다.
2. Socket
public void start() {
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
while (true) {
logger.debug("waiting connect.. ");
Socket connection = serverSocket.accept();
logger.debug("accept!");
}
} catch ...
}
a. 서버소켓은 accept() 메서드를 호출하여 연결 요청이 올 때까지 기다린다.
참고로 서버소켓은 기다리면서 block 상태가 된다.
예를 들어 이런 코드가 있다면
Socket connection = serverSocket.accept();
doSomething();
연결 요청이 오기 전까지 doSomething() 메서드는 실행되지 않는다.
b. 연결 요청이 성공하면 소켓을 반환한다.
3. 멀티쓰레드
쓰레드 사용 이유
지금은 학습용으로 만들고 있지만.. 진짜 서비스라고 가정을 해보자.
- 비슷한 시간에 서비스에 접속한 7명의 사용자가 있다. 즉, 클라이언트의 연결 요청이 7개가 거의 동시에 들어왔다.
- 근데 3번째 사용자가 서비스를 이용할 때 어떠한 이유로 작업 시간이 길어졌다.
- 4번째 사용자부터는 한참을 기다리고 나서야 서비스를 이용하게 된다.
int num = 1;
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
while (true) {
logger.debug("waiting connect.. ");
Socket connection = serverSocket.accept();
System.out.println(num + "번째 사용자");
if (num == 3) {
for (int i=0; i<100000; i++) {
System.out.println(i);
}
}
num++;
logger.debug("accept!");
}
}
실행 결과
for문에서 출력하는 작업은 단순하지만 실제 서비스 작업은 비교도 못할 만큼의 시간이 걸린다.
시간이 걸려 작업이 끝나기라도 하면 다행이다. 에러가 발생하면 기약 없이 기다려야 한다.
4번째 사용자부터는 이 서비스를 다시는 사용하지 않을지도 모른다.
그러므로 4번째 이후의 사용자가 접속해도 서비스를 빠르게 사용할 수 있게 만들어야 한다.
요청을 순서대로 처리하지 않고 동시에 처리를 하면 사용자들은 더 이상 기다릴 필요가 없다.
이러한 이유로 동시 처리를 하기 위해 필요한 것이 바로 멀티쓰레드이다.
4. Thread Pool
자바는 멀티쓰레드를 쉽게 사용할 수 있도록 지원한다.
쓰레드를 사용하려면 Thread 클래스를 상속받거나 Runnable 인터페이스를 구현하는 방법이 있다.
Thread를 상속하면 다른 클래스를 상속받을 수 없고.. 더 많은 기능들이 있는데.. 지금은 그런 기능들까진 필요 없고.. 등의 이유로 Runnable을 구현하기로 한다.
여기서 의문이 생긴다.
그럼 쓰레드로 뭘 해야 하는데? 쓰레드로 뭘 만들어야 사용자가 동시에 이용할 수 있는데?
한 명의 사용자는 하나의 연결 요청으로 시작하여 서비스를 이용한 다음에 사라진다.
서버의 입장에서는 위의 과정이 서버가 처리해야 할 하나의 작업이다.
그래서 작업마다 쓰레드를 만들면 된다.
2명의 사용자가 오면 쓰레드 두 개를, 7명의 사용자가 오면 쓰레드 일곱 개를 만든다면 동시에 작업을 처리할 수 있다.
그럼 백만 명의 사용자가 오면 백만 개의 쓰레드를 생성해야 할까?
문장만 봐도 큰일이 날 것 같다는 감이 온다.. CPU, 메모리 같은 자원의 임계치가 넘는다면 서버가 죽을 수도 있다.
쓰레드는 마법이 아니므로 효율적으로 사용해야 한다.
쓰레드 풀이라는 게 쓰레드를 효율적으로 사용하게 도와준다.
쓰레드 풀
- 쓰레드 풀은 디자인 패턴 중 하나이다
- 쓰레드를 풀에 보관하여 관리
- 쓰레드가 필요하면 풀에 가서 이미 있는 쓰레드 꺼내서 사용
- 이미 누가 다 쓰고 있으면 기다리거나 거절당함
- 쓰레드를 다 쓰고 나면 풀에 반납
- 생성 가능한 쓰레드의 최대치를 관리한다. 톰캣은 기본 설정이 200개
- 쓰레드가 이미 생성되어 있어 쓰레드 생성 비용 절감, 생성을 바로 안 해도 되니 응답 시간 빠름
5. ExecutorService
역시 자바가 또 쓰레드 풀을 쉽게 사용하도록 지원해 준다.
public class WebServer {
private static final Logger logger = LoggerFactory.getLogger(WebServer.class);
private static final int DEFAULT_PORT = 8080;
private static final int NUM_THREADS = 5;
public void start() {
ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
try (ServerSocket serverSocket = new ServerSocket(DEFAULT_PORT)) {
while (true) {
logger.debug("waiting connect.. ");
Socket connection = serverSocket.accept();
logger.debug("accept!");
Runnable r = new RequestHandler(connection);
executor.submit(r);
}
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
}
}
}
a. NUM_THREADS: 최대 쓰레드 개수는 5개로 설정해두고 테스트에서 7개의 연결 요청을 할 예정이다
b. Executors.newFixedThreadPool() 메서드에 최대 쓰레드 개수를 넘겨주고 ExecutorService를 구현한 인스턴스를 돌려받는다
c. 클라이언트와의 연결이 성공하면 RequestHandler 인스턴스를 생성하고 연결 정보(소켓)을 넘겨준다
RequestHandler는 Runnable 인터페이스를 구현한 클래스이다
서버의 입장에서는 RequestHandler를 하나의 작업 단위로 본다
RequestHandler 클래스 = 쓰레드 = 작업 단위
d. executor.submit() 메서드에 쓰레드(r)를 넘겨주며 쓰레드 풀에 작업 요청을 한다
테스트
public class WebServerTest {
@Test
public void webServer_start(){
WebServer ws = new WebServer();
ws.start();
}
}
테스트 실행 결과
쓰레드 풀 최대 개수: 5
연결 요청(사용자) 개수: 7
결과: 쓰레드 풀 최대 크기만큼 먼저 처리되고 (쓰레드 반환 후) 나머지 2개 작업 처리
[pool-1-thread-1]
[pool-1-thread-2]
[pool-1-thread-3]
[pool-1-thread-4]
[pool-1-thread-5]
[pool-1-thread-1]
[pool-1-thread-2]
'Project' 카테고리의 다른 글
Spring Boot에 Redis를 적용하여 인기 메뉴 순위 구현 (0) | 2023.08.06 |
---|---|
3. JAVA로 아주 간단한 WAS와 Spring MVC Framework 만들기 (0) | 2023.01.10 |
1. JAVA로 아주 간단한 WAS와 Spring MVC Framework 만들기 (0) | 2023.01.04 |
커피 주문 서비스를 객체 지향으로 설계해보기 with Java (0) | 2022.10.19 |
4. AWS + Spring Boot + React 프로젝트 근데 이제 배포 자동화를 곁들인 (0) | 2022.03.27 |