본문 바로가기

C++/Socket 통신

멀티스레드 기반 소켓 프로그래밍 : 서버 성능 극대화를 위한 필수 개념 - c++

SMALL

포스팅 계기

 저번 포스팅 2025.02.25 - [C++ 개발이야기/Socket 통신] - C++에서 TCP/UDP 소켓 프로그래밍 입문에 이어 조금 심화과정에 관해서 이야기를 논하고 싶어 해당 글을 쓰게 되었다. 네트워크 프로그래밍에서 서버는 다수의 클라이언트 요청을 동시에 처리해야 하는 경우가 많다. 단일 스레드로 요청을 처리하면 하나의 클라이언트가 긴 시간을 소요할 경우 다른 클라이언트의 요청이 지연될 수 있다. 이를 해결하기 위해 멀티스레드 기반 소켓 프로그래밍이 필요하다. 이번 글에서는 멀티스레드를 활용한 소켓 프로그래밍 개념과 구현 방법을 설명하겠습니다.


멀티스레드 기반 소켓 프로그래밍에 대한 개념

  1.  멀티스레드의 필요성
    • 병렬 처리: 여러 스레드가 동시에 작업을 처리할 수 있어, 여러 클라이언트가 요청을 보낼 때 응답 시간을 단축가능
    • 자원 공유: 동일한 프로세스 내에서 스레드끼리는 메모리를 공유하기 때문에, 데이터 교환 용이
    • 효율적 스케줄링: 운영체제에서 스레드 단위로 스케줄링을 관리하므로, 여러 클라이언트를 처리할 때 프로세스보다 가벼운 비용으로 작업을 병렬 처리 가능
  2. 소켓 프로그래밍과의 결합
    • 소켓(Socket): 네트워크 통신을 위한 양쪽 끝단을 말하며, 서버와 클라이언트 간 데이터를 주고받는 통로
    • 멀티스레드 + 소켓: 서버가 여러 클라이언트 연결을 동시에 처리해야 하는 상황에서, 각 클라이언트 연결마다 스레드를 하나씩 할당하면 독립적으로 통신을 관리가능
  3. 구조도

아래 다이어그램처럼, 서버는 여러 클라이언트 연결을 받으면 각 연결을 처리하기 위해 새로운 스레드를 생성한다. 각 스레드는 독립적으로 클라이언트와 통신하며, 서버 메인 스레드는 전체 흐름을 제어한다.

출처 : ChatGPT


작성 코드

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 9000
#define BUF_SIZE 1024

std::mutex cout_mutex;  // 동기화 출력용 뮤텍스

void client_handler(int client_sock) {
    char buffer[BUF_SIZE];

    while (true) {
        int str_len = read(client_sock, buffer, sizeof(buffer) - 1);
        if (str_len <= 0) break;  // 연결 종료 시 루프 탈출

        buffer[str_len] = '\0';

        {
            std::lock_guard<std::mutex> lock(cout_mutex);
            std::cout << "Client Msg: " << buffer << std::endl;
        }

        write(client_sock, buffer, str_len);  // Echo back
    }

    close(client_sock);
}

int main() {
    int server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock == -1) {
        perror("socket() error");
        return 1;
    }

    sockaddr_in server_addr{}, client_addr{};
    socklen_t client_addr_size = sizeof(client_addr);

    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(PORT);

    if (bind(server_sock, (sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind() error");
        close(server_sock);
        return 1;
    }

    if (listen(server_sock, 5) == -1) {
        perror("listen() error");
        close(server_sock);
        return 1;
    }

    std::cout << "Server is running on port " << PORT << "..." << std::endl;

    std::vector<std::thread> thread_pool;

    while (true) {
        int client_sock = accept(server_sock, (sockaddr*)&client_addr, &client_addr_size);
        if (client_sock == -1) {
            perror("accept() error");
            continue;
        }

        {
            std::lock_guard<std::mutex> lock(cout_mutex);
            std::cout << "Client connected..." << std::endl;
        }

        thread_pool.emplace_back(client_handler, client_sock);
        thread_pool.back().detach();  // 스레드 분리
    }

    close(server_sock);
    return 0;
}

구현시 주의

 

  • 동기화 문제: 멀티스레드 환경에서는 공유 자원을 동시에 접근할 때 데이터 무결성이 깨질 수 있다. 전역 변수나 공유 데이터에 접근할 때는 뮤텍스(Mutex) 또는 세마포어(Semaphore) 등을 이용해 동기화가 필요하다.
  • 스레드 개수 제한: 너무 많은 스레드를 생성하면 오히려 문맥 교환(Context Switching) 비용이 커져 성능이 떨어질 수 있다. 적절한 스레드 풀(Thread Pool)을 구성하거나, OS가 관리 가능한 범위 내에서 스레드를 유지해야 한다.
  • 에러 처리: 네트워크 통신 중 연결 끊김이나 데이터 손실 같은 에러가 발생할 수 있으므로, accept, read, write 등 시스템 콜 반환 값을 꼼꼼히 체크해야 하는 것은 필수다.
  • 자원 누수(메모리/소켓): 스레드가 종료되거나 클라이언트가 연결을 종료했을 때, 소켓을 제대로 닫아주는 것이 중요하다.

 


글을 끝 마치면서

 오늘은 이렇게 멀티스레드 기반 소켓 프로그래밍에 대해 간단히 살펴봤다. 여러 클라이언트가 동시에 접속하는 상황에서 멀티스레드 방식은 성능과 유연성 측면에서 큰 이점을 준다.

 다음 포스팅에서는 비동기 소켓 프로그래밍에 대해 알아볼 예정이다. 멀티스레드와는 또 다른 방식으로 동시성을 해결하는 방법이니, 관심 있으신 분들은 기다려주면 고마울 것 같다. 오늘 포스팅 또한 읽어주신 분들에게 감사하다. 오늘 이야기가 조금이나마 도움이 되었다면 정말 좋을 것 같다.