본문 바로가기
Baeldung번역&공부/Java-web

ServerSocket을 이용한 멀티쓰레드 서버 구축(A Simple HTTP Server With Java ServerSocket)

by ms727 2025. 2. 5.

원본 글: https://www.baeldung.com/java-serversocket-simple-http-server

HTTP 서버는 일반적으로 요청 클라이언트에게 여러 리소스를 제공하는 역할을 수행합니다. Tomcat, Netty와 같은 실제 운영에서 사용할법한 웹 서버도 제공하지만, ServerSocket클래스를 이용해 웹 서버를 만들면 HTTP 프로토콜이 실제로 어떻게 동작하는지 더 잘 알 수 있습니다.

해당 클래스는 IP와 Port정보를 받아서 TCP 커넥션을 맺는 서버를 만들 수 있게 합니다.

이 글에서는 ServerSocket클래스를 이용한 간단한 서버를 제작합니다. 그리고 간단한 GET요청을 수행해볼겁니다.
명심해야할건 제작되는 이 서버는 실제 운영에 사용하기에는 적합하지 않습니다.(학습용)

1. Basic of Web Server Using ServerSocket

먼저 서버는 클라이언트의 요청을 수신합니다. 클라이언트는 브라우저가 될 수 있고, 프로그램, API 툴이 될 수도 있습니다. 커넥션이 성공하면 서버는 클라이언트에게 리소스를 제공할겁니다.

ServerSocket 클래스는 지정된 포트에 서버를 생성하는 함수를 제공합니다. accept() 함수를 통해서 정의된 포트에서 들어오는 연결을 수신합니다.

accept() 함수는 커넥션이 완전히 설립될 때까지 차단되며, 이후에는 Socket 인스턴스를 반환합니다. Socket 인스턴스는 서버와 클라이언트간 요청,응답을 진행할 수 있는 하나의 소통 창구(Stream)를 제공합니다.

2. Creating a ServerSocket Instance

Port를 명시적으로 제공하여 ServerSocket 오브젝트를 만듭니다.
그리고, accept() 함수를 통해서 들어오는 커넥션에 대해서 허용할 준비를 합니다.

public class SimpleSocketServer {

    public static void main(String[] args) {
        int port = 8080;
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while(true) {
                Socket clientSocket = serverSocket.accept();
            }
        } catch (Exception e) {
            System.out.println("server create error");
        }
    }
}

이 코드는 while문을 통해서 커넥션을 계속 기다립니다. 그리고 ServerSocket클래스의 accept() 함수를 통해서 연결을 수신하고 수락합니다.
연결이 생성되면, accept() 함수는 Socket 오브젝트를 반환하고 해당 오브젝트는 서버와 클라이언트간 소통을 할 수 있는 네트워크를 만듭니다.

3. Handling Input and Output

일반적으로 서버는 클라이언트에게 요청을 받으면 적절한 응답을 제공합니다. Socket클래스에 있는 getInputStream()과 getOutputStream() 함수를 사용하면 클라이언트에 읽고 쓸 수 있는 스트림을 제공하여 통신을 용이하게 할 수 있습니다.

따라서 위에서 작성한 코드를 조금 더 수정해봅니다.

while(true) {
// ...
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
}
// ...

위 코드에서 clientSocket 객체의 getInputStream() 메서드를 사용하여 클라이언트와 서버 간의 활성 연결에서 입력 스트림을 가져옵니다. 이 스트림은 BufferedReader로 감싸져, 텍스트 데이터를 보다 효율적으로 읽을 수 있도록 합니다.
비슷하게 getOutputStream()도 BufferedWriter로 감싸져 서버 응답을 보다 편하게 클라이언트에게 전달할 수 있게 합니다.

BufferedWriter() 객체에 있는 write()함수를 통해서 서버 응답을 작성해봅니다. HTTP 응답은 헤더와 바디를 가집니다.

먼저, 응답 바디부를 작성합니다.

private String getResponseBody() {
    return """
                <html>
                    <head>
                        <title>Minseok Home</title>
                    </head>
                    <body>
                        <h1>Minseok Home Page</h1>
                        <p>Java Tutorials</p>
                        <ul>
                            <li>
                                <a href="/get-started-with-java-series"> Java </a>
                            </li>
                            <li>
                                <a href="/spring-boot"> Spring </a>
                            </li>
                            <li>
                                <a href="/learn-jpa-hibernate"> Hibernate </a>
                            </li>
                        </ul>
                     </body>
                 </html>
            """;
}

위 코드는 간단한 HTML페이지를 코드를 작성하고. 다음은 이 body에 대한 전체 길이를 헤더에 추가해야하기 때문에 이를 계산하는 코드도 작성합니다.

private int getBodyLength(String body) {
    return body.length();
}

이제 HTTP 헤더와 바디부를 output 스트림에 작성해봅니다.

while(true) {
    // ...
    String clientInputLine;
    while((clientInputLine = in.readLine()) != null) {
        if (clientInputLine.isEmpty()) {
            break;
        }

        out.write("HTTP/1.0 200 OK\r\n");
        out.write("Date: " + new Date() + "\r\n");
        out.write("Server: Custom Server\r\n");
        out.write("Content-Type: text/html\r\n");
        out.write("Content-Length: " + getBodyLength(getResponseBody()) + "\r\n");
        out.write("\r\n");
        out.write(getResponseBody());
    }
}

위 코드에서 우리는 Http 헤더와 바디를 wirte()함수를 통해서 작성하였습니다. 그리고 헤더와 바디를 '\r\n'문자로 개행 및 분리시켜 header부가 끝났음을 알려줍니다.

4. Multithreaded Server

위에서 만든 서버는 싱글 쓰레드로 동작하기에 성능이 뛰어나지 않을 수 있습니다. 서버는 동시에 여러개의 요청을 받을 수 있어야합니다.

별도의 쓰레드에서 모든 요청을 수행할 수 있도록 위의 예제를 리팩터링해야합니다. 먼저 새로운 멑리 쓰레드 서버 관련한 새로운 클래스를 만듭니다.

public class SimpleHttpServerMultiThreaded {

    private final int port;
    private static final int THREAD_POOL_SIZE = 10;

    public SimpleHttpServerMultiThreaded(int port) {
        this.port = port;
    }
}

위 클래스에서는 포트 번호와 쓰레드 풀 크기를 가지는 두개의 필드를 만들었습니다. 포트 번호는 서버 오브젝트가 생성되면 부여됩니다.

그리고 클라이언트와 소통을 담당하는 함수를 하나 만듭니다.

void handleClient(Socket clientSocket) {
       try(BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
           BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
           String clientInputLine;
           while((clientInputLine = in.readLine()) != null) {
               if(clientInputLine.isEmpty()) {
                   break;
               }
           }
           LocalDateTime now = LocalDateTime.now();

           out.write("HTTP/1.0 200 OK\r\n");
           out.write("Date: " + now + "\r\n");
           out.write("Server: Custom Server\r\n");
           out.write("Content-Type: text/html\r\n");
           out.write("Content-Length: " + getBodyLength(getResponseBody()) + "\r\n");
           out.write("\r\n");
           out.write(getResponseBody());
       } catch (IOException e) {
           // ...
       } finally {
           try {
               clientSocket.close();
           } catch (IOException e) {
               // ...
           }
       }
    }

이 함수는 클라이언트와 데이터를 주고받을 수 있게 설계되었습니다. 이전 예제와 크게 다른점은 없습니다.

다음으로, 별도의 쓰레드에서 각 연결을 설정하기 위해start()라는 함수를 만듭니다.

void start() throws IOException {
        try(ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        ServerSocket serverSocket = new ServerSocket(port)) {
            while(true) {
                Socket clientSocket = serverSocket.accept();
                threadPool.execute(() -> handleClient(clientSocket));
            }
        }
    }

위 코드에서 ExcutorService를 통하여 쓰레드 풀을 만듭니다. 다음으로 execute() 함수를 통해 클라이언트 커넥션을 요청마다 각각 제공합니다.
이 덕에 서버는 동시에 들어오는 여러 요청에 대해 대응이 가능해지고 수행능력과 성능을 크게 향상시킬 수 있게 됩니다.

accept() 함수를 통해서 새로운 Socket인스턴스를 클라이언트 요청마다 각각 제공합니다.

5. Testing the Server

main함수에서 서버를 인스턴스화하여 실행해봅니다.

public class SimpleHttpServerMain {

    public static void main(String[] args) {
        int port = 8080;
        SimpleHttpServerMultiThreaded server = new SimpleHttpServerMultiThreaded(port);
        try {
            server.start();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

그리고 localhost:8080으로 접근하여 페이지가 제대로 뜨는지 확인합니다.

 

6. 결론

이 글에서는 ServerSocket클래스를 이용하여 간단한 서버를 만들었습니다. 그리고 싱글쓰레드와 멀티쓰레드를 이용한 서버도 이 클래스를 사용하여 만들었습니다.

예제 코드 : https://github.com/kkminseok/baeldung-test/tree/main/Java-web/src/main/java/com/my