HTTP 프레임워크 아래에는 결국 Socket이 있습니다. 네트워크 프로그래밍의 기본을 알면 프레임워크가 왜 그렇게 동작하는지 이해할 수 있습니다.

이게 뭔가요?

자바의 네트워크 프로그래밍은 java.net.Socket(클라이언트)과 java.net.ServerSocket(서버)으로 TCP 통신을 구현하고, java.nio.channels로 논블로킹/비동기 통신을 구현하는 것입니다.

왜 필요한가요?

  • HTTP 서버(Tomcat, Netty)의 내부 동작 이해
  • 커스텀 프로토콜 서버 구현
  • 채팅, 게임 서버 같은 실시간 통신
  • 네트워크 관련 장애 디버깅

블로킹 서버 (ServerSocket)

가장 기본적인 TCP 서버입니다.

JAVA
public class SimpleServer {
    public static void main(String[] args) throws Exception {
        try (ServerSocket server = new ServerSocket(8080)) {
            System.out.println("서버 시작: 포트 8080");

            while (true) {
                // accept()는 클라이언트 연결까지 블로킹
                Socket client = server.accept();
                // 클라이언트마다 새 스레드 (비효율적)
                new Thread(() -> handleClient(client)).start();
            }
        }
    }

    static void handleClient(Socket client) {
        try (client;
             var in = new BufferedReader(
                 new InputStreamReader(client.getInputStream()));
             var out = new PrintWriter(
                 client.getOutputStream(), true)) {

            String message;
            while ((message = in.readLine()) != null) {
                out.println("에코: " + message);
            }
        } catch (IOException e) {
            log.error("클라이언트 처리 에러", e);
        }
    }
}

문제점

클라이언트 1000개가 접속하면 스레드 1000개가 필요합니다. 스레드 하나당 약 1MB 스택 메모리를 사용하므로 확장성에 한계가 있습니다.

블로킹 클라이언트 (Socket)

JAVA
try (Socket socket = new Socket("localhost", 8080);
     var out = new PrintWriter(socket.getOutputStream(), true);
     var in = new BufferedReader(
         new InputStreamReader(socket.getInputStream()))) {

    out.println("안녕하세요");
    String response = in.readLine(); // 응답 대기 (블로킹)
    System.out.println("서버 응답: " + response);
}

스레드 풀 서버

스레드를 무한정 만드는 대신 스레드 풀을 사용합니다.

JAVA
ExecutorService pool = Executors.newFixedThreadPool(200);

try (ServerSocket server = new ServerSocket(8080)) {
    while (true) {
        Socket client = server.accept();
        pool.submit(() -> handleClient(client)); // 스레드 풀에 작업 제출
    }
}

200개의 동시 연결만 처리할 수 있습니다. 201번째 클라이언트는 큐에서 대기합니다.

NIO: 논블로킹 서버 (Selector)

하나의 스레드로 여러 연결을 처리합니다.

JAVA
public class NioServer {
    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080));
        serverChannel.configureBlocking(false); // 논블로킹 모드

        // 새 연결 이벤트를 Selector에 등록
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            selector.select(); // 이벤트 발생까지 대기

            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                keys.remove();

                if (key.isAcceptable()) {
                    // 새 클라이언트 연결
                    SocketChannel client = serverChannel.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_READ);
                }

                if (key.isReadable()) {
                    // 데이터 읽기 가능
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int read = client.read(buffer);
                    if (read == -1) {
                        client.close();
                    } else {
                        buffer.flip();
                        client.write(buffer); // 에코
                    }
                }
            }
        }
    }
}

Selector의 핵심

PLAINTEXT
Selector (1개 스레드)
  ├── Channel A (읽기 가능?)
  ├── Channel B (쓰기 가능?)
  ├── Channel C (연결 요청?)
  └── ...

select() → "이벤트가 있는 채널만" 반환
→ 하나의 스레드로 수천 연결 처리 가능

이것이 Netty, Tomcat NIO, Nginx의 기본 동작 원리입니다.

소켓 옵션

JAVA
Socket socket = new Socket();
socket.setSoTimeout(5000);        // 읽기 타임아웃 5초
socket.setKeepAlive(true);        // TCP Keep-Alive
socket.setTcpNoDelay(true);       // Nagle 알고리즘 비활성화 (저지연)
socket.setReceiveBufferSize(8192); // 수신 버퍼 크기
socket.connect(
    new InetSocketAddress("server", 8080),
    3000); // 연결 타임아웃 3초

HttpClient (Java 11+)

저수준 Socket 대신 HTTP 통신에는 HttpClient를 사용합니다.

JAVA
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/users"))
    .header("Accept", "application/json")
    .GET()
    .build();

// 동기
HttpResponse<String> response =
    client.send(request, HttpResponse.BodyHandlers.ofString());

// 비동기
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenApply(HttpResponse::body)
    .thenAccept(System.out::println);

자주 헷갈리는 포인트

  • 블로킹 I/O vs NIO: 블로킹은 코드가 간단하지만 확장성이 낮습니다. NIO는 복잡하지만 하나의 스레드로 수천 연결을 처리합니다. 대부분의 경우 Netty 같은 프레임워크를 쓰는 것이 좋습니다.
  • SO_TIMEOUT: 소켓 타임아웃을 설정하지 않으면 read()가 영원히 블로킹될 수 있습니다. 반드시 설정하세요.
  • TCP_NODELAY: Nagle 알고리즘은 작은 패킷을 모아서 보냅니다. 실시간 통신에서는 비활성화(true)하는 것이 좋습니다.
  • ByteBuffer flip(): write 모드에서 read 모드로 전환할 때 반드시 flip()을 호출해야 합니다. 빠뜨리면 빈 데이터를 읽습니다.

정리

방식스레드 모델동시 연결복잡도
ServerSocket스레드/연결수백낮음
스레드 풀고정 스레드 풀수백낮음
NIO Selector1 스레드 : N 연결수만높음
Netty이벤트 루프수만중간 (프레임워크)

References

댓글 로딩 중...