정리/WAS

순수 Java로 WAS 구현 (1) - WAS의 심장, 소켓(Socket) 프로그래밍

baby-t 2026. 5. 12. 13:47

1. 들어가며: 왜 소켓 통신부터 알아야 할까?

스프링 부트로 웹 개발을 하다 보면 내장된 Tomcat(톰캣)이 알아서 HTTP 요청을 처리해 줍니다. 하지만 '문제를 구조적으로 이해하는 개발자'가 되기 위해, 톰캣이라는 거대한 마법 상자를 열어 그 내부를 직접 순수 Java로 구현해 보고자 합니다.

웹 서버(WAS)의 본질은 결국 '클라이언트의 연결을 기다렸다가, 요청을 받고, 응답을 돌려주는 무한 반복 프로그램'입니다. 그리고 이 네트워크 통신의 가장 밑바탕이 되는 기술이 바로 소켓(Socket)입니다.

 

2. 소켓(Socket)이란?

소켓은 프로세스 간 네트워크 통신에 사용되는 양쪽 끝단(End-point)을 의미합니다. 자바에서는 TCP 기반의 신뢰성 있는 연결을 위해 java.net 패키지를 사용합니다.

  • 서버 소켓 (ServerSocket): 특정 포트(ex. 8080)를 열고 클라이언트의 연결을 기다립니다.
  • 클라이언트 소켓 (Socket): 서버의 IP 주소와 포트 번호를 찾아가 연결을 시도합니다.

연결이 성립되면 서버 소켓은 클라이언트와 통신할 새로운 전용 소켓을 하나 할당해주고, 둘은 독립적인 통로를 통해 양방향으로 데이터를 주고받게 됩니다.

 

3. 서버와 클라이언트의 구현 흐름

가장 기본적인 형태의 서버와 클라이언트 통신 코드는 다음과 같습니다. (자원 누수를 막기 위해 Java 7부터 지원하는 try-with-resources 구문을 적용했습니다.)

💻 서버 소켓 구현


try (ServerSocket serverSocket = new ServerSocket(8080)) {
    System.out.println("클라이언트의 접속을 기다립니다...");
    
    // 1. 클라이언트 접속 대기
    Socket socket = serverSocket.accept(); 
    System.out.println("클라이언트 연결 성공!");

    // 2. 데이터 송수신을 위한 Stream 생성
    InputStream in = socket.getInputStream();
    OutputStream out = socket.getOutputStream();

    // 3. 데이터 수신 (클라이언트 -> 서버)
    byte[] inputData = new byte[100];
    int length = in.read(inputData);
    System.out.println("수신한 메시지: " + new String(inputData, 0, length));

    // 4. 데이터 송신 (서버 -> 클라이언트)
    String outputMessage = "서버가 메시지를 잘 받았습니다!";
    out.write(outputMessage.getBytes());
    out.flush();
    
} catch (IOException e) {
    e.printStackTrace();
}

💻 클라이언트 소켓 구현


// 1. 서버의 IP와 포트로 접속 시도
try (Socket socket = new Socket("127.0.0.1", 8080)) {
    
    InputStream in = socket.getInputStream();
    OutputStream out = socket.getOutputStream();

    // 2. 데이터 송신 (클라이언트 -> 서버)
    String outputMessage = "안녕하세요, 서버님!";
    out.write(outputMessage.getBytes());
    out.flush(); // 버퍼에 남은 데이터를 강제로 밀어냄

    // 3. 데이터 수신 (서버 -> 클라이언트)
    byte[] inputData = new byte[100];
    int length = in.read(inputData);
    System.out.println("서버의 응답: " + new String(inputData, 0, length));
    
} catch (IOException e) {
    e.printStackTrace();
}

 

3.1. 소켓 프로그래밍과 예외 처리(Exception Handling)의 필수성

네트워크 통신은 언제 끊길지 모르는 아주 불안정한 환경입니다. 클라이언트의 PC가 갑자기 종료되거나, 지정한 포트가 이미 다른 프로그램에서 사용 중인 등 수많은 변수가 존재합니다.

따라서 자바에서 소켓과 스트림을 다룰 때는 IOException과 같은 예외 처리가 문법적으로 강제(Checked Exception)됩니다. 위 코드에서 try-catch 블록과 try-with-resources 구문을 사용한 이유도 이 때문입니다. 예외가 발생하더라도 서버 애플리케이션 전체가 다운되지 않도록 방어하고, 통신에 사용한 자원(Socket, Stream)을 안전하게 운영체제에 반납(Close)하는 것이 서버 아키텍처의 기본입니다.

 

4. 데이터의 흐름, 스트림(Stream)과 버퍼(Buffer)의 최적화

네트워크를 통해 넘어오는 데이터의 기본 단위는 바이트(byte)입니다. 하지만 우리가 실제로 다루는 것은 '문자(String)'이기 때문에 지속적인 변환이 필요합니다. 이를 효율적으로 처리하기 위해 자바의 I/O 클래스들을 조합하여 사용합니다.

💡 스트림의 3단계 진화 (Byte -> Character -> Buffer)

1. InputStream: 바이트 단위로 읽어옵니다. (ex. 01010101)
2. InputStreamReader: 바이트를 우리가 읽을 수 있는 문자로 변환합니다. (charset 지정 가능)
3. BufferedReader: 문자를 하나씩 전달하지 않고, '버퍼'라는 바구니에 모았다가 한 번에 전달하여 I/O 성능을 극대화합니다.

이러한 최적화를 적용하면 코드가 훨씬 깔끔해지고 성능이 향상됩니다.


// 수신 최적화: 버퍼를 활용해 줄 단위(Line)로 문자를 읽음
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String message = in.readLine();

 

4.1. 헷갈리기 쉬운 출력 최적화: BufferedWriter vs PrintWriter

스트림을 통해 문자를 밖으로 내보낼(출력) 때 버퍼를 활용하는 두 가지 대표적인 클래스가 있습니다. 비슷해 보이지만 사용 편의성과 줄바꿈(\n) 처리에 큰 차이가 있습니다.

📝 BufferedWriter: 원리 충실형
  • write("문자열") 메서드를 사용합니다.
  • 내가 입력한 문자열만 정확히 버퍼에 싣습니다. 만약 수신자가 readLine()(한 줄 읽기)으로 대기 중이라면, 반드시 write("문자열\n")처럼 줄바꿈 기호를 직접 추가해 주어야 멈춤(Blocking) 현상이 발생하지 않습니다.
  • 버퍼에 담긴 데이터를 네트워크로 밀어내려면 flush()를 명시적으로 호출해야 합니다.

🚀 PrintWriter: 개발자 편의형
  • println("문자열"), printf() 등 익숙하고 다양한 포맷팅 메서드를 지원합니다.
  • println()을 사용하면 내가 직접 \n을 치지 않아도 내부적으로 문자열 뒤에 줄바꿈 기호를 붙여서 버퍼에 싣습니다.
  • 객체 생성 시 autoFlush 옵션을 true로 주면, println()을 호출할 때마다 알아서 버퍼를 비워주므로 flush()를 매번 적을 필요가 없어 코드가 간결해집니다.

 

5. 다음 편 예고: 현재 코드의 치명적인 한계점

지금까지 작성한 코드는 훌륭하게 동작하지만, 이 코드로 만든 웹 서버는 치명적인 문제가 있습니다.

바로 serverSocket.accept()in.read() 메서드가 블로킹(Blocking) 방식으로 동작한다는 점입니다. 첫 번째 클라이언트가 연결을 맺고 데이터를 보내지 않은 채 가만히 있는다면, 서버는 read() 상태에서 멈춰버립니다. 그동안 두 번째, 세 번째 클라이언트가 접속을 시도해도 서버는 응답하지 못하고 먹통이 됩니다.

수만 명의 사용자가 동시에 접속하는 웹 서버(WAS)는 이 문제를 어떻게 해결할까요? 다음 편에서는 스레드(Thread)를 활용해 동시성 문제를 해결하는 방법에 대해 알아보겠습니다.