C++ Boost.Asio 입문 | io_context·async_read

C++ Boost.Asio 입문 | io_context·async_read

이 글의 핵심

C++ Boost.Asio 입문에 대한 실전 가이드입니다. 개념부터 실무 활용까지 예제와 함께 상세히 설명합니다.

들어가며: “비동기 I/O가 왜 필요한가요?”

문제 시나리오 1: 블로킹 서버의 한계

채팅 서버를 만든다고 상상해 보세요. 동기(블로킹) 방식으로 구현하면:

// 동기 서버: 한 연결 처리 중에는 다른 연결을 받을 수 없음
void handle_client(tcp::socket socket) {
    std::array<char, 1024> buf;
    while (true) {
        size_t n = socket.read_some(boost::asio::buffer(buf));  // ⏸️ 여기서 블로킹!
        if (n == 0) break;
        // 클라이언트 A가 10초 동안 아무것도 안 보내면?
        // → 다른 클라이언트 B, C는 연결조차 못 받음!
    }
}

int main() {
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
    while (true) {
        auto socket = acceptor.accept(io);  // ⏸️ 여기서도 블로킹
        std::thread(handle_client, std::move(socket)).detach();  // 스레드 폭발
    }
}

주의사항: detach만 하고 생명주기·예외를 관리하지 않으면 크래시·리소스 고갈로 이어질 수 있습니다. 프로덕션에서는 스레드 풀·비동기 모델을 검토하세요.

문제점:

  • 클라이언트가 데이터를 보내지 않으면 스레드가 그대로 대기
  • 연결 1만 개 = 스레드 1만 개 → 메모리·컨텍스트 스위칭 폭발
  • 멀티스레드로 해결하려 해도 스케일 한계에 부딪힘

문제 시나리오 2: 게임 서버·IoT·실시간 데이터

시나리오겪는 문제동기 방식 한계
게임 서버5,000명 동시 접속, 저지연 응답 필요스레드 5,000개 → 메모리 2GB+
IoT 센서 수집10,000개 디바이스가 주기적 전송블로킹 recv로 처리 불가
실시간 시세수백 연결에서 동시 푸시한 연결 지연 시 전체 영향
HTTP API 서버요청당 대기 시간 변동 큼느린 클라이언트가 전체 블로킹

Asio 비동기 I/O의 해결책:

  • 한 스레드가 수천 개 연결을 논블로킹으로 처리
  • I/O 완료 시 콜백으로 알림 → 다음 작업 등록
  • 이벤트 기반 모델로 리소스 효율 극대화

이 글을 읽기 전에: C++ 기본 문법과 소켓의 개념(#28 소켓 기초)을 알고 있으면 이해가 쉽습니다. “블로킹 서버는 한 연결 처리하는 동안 다른 연결을 못 받는다”는 한계를 느꼈다면, 이 글의 비동기·io_context·run()이 그 다음 단계입니다. 더 깊은 run/poll/Strand는 고성능 네트워크 가이드 #1에서 이어서 다룹니다.

요구 환경: Boost.Asio(vcpkg install boost-asio 등) 또는 standalone Asio. C++14 이상. Linux/macOS에서 g++/Clang으로 빌드·실행, Windows에서는 WSL 또는 MSVC + vcpkg 권장.

개념을 잡는 비유

소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.


목차

  1. 비동기 I/O가 왜 필요한가요?
  2. 블로킹 vs 논블로킹 비교
  3. io_context와 run
  4. 비동기 타이머
  5. async_read / async_write 완전 예제
  6. 비동기 TCP 클라이언트
  7. 비동기 서버 (async_accept)
  8. 에러 처리 패턴
  9. 자주 하는 실수 (핸들러 수명, work_guard)
  10. 모범 사례 (Best Practices)
  11. 성능 비교: 동기 vs 비동기
  12. 프로덕션 패턴

1. 비동기 I/O가 왜 필요한가요?

실제 겪는 문제

상황동기(블로킹)비동기(Asio)
10,000 동시 연결스레드 10,000개 필요1~8 스레드로 처리
클라이언트가 30초 대기30초 동안 스레드 점유다른 작업 처리 가능
연결 수 증가메모리·CPU 선형 증가거의 일정한 리소스
네트워크 지연전체 처리량 저하영향 최소화

핵심: 비동기 I/O는 “연산을 시작만 해 두고, 완료되면 콜백으로 알려준다”는 모델입니다. 스레드가 대기하지 않고 다른 작업을 처리할 수 있어, 소수의 스레드로 많은 연결을 다룰 수 있습니다.


2. 블로킹 vs 논블로킹 비교

블로킹 I/O: 스레드가 대기

sequenceDiagram
    participant T as 스레드
    participant S as 소켓
    participant N as 네트워크

    T->>S: read_some() 호출
    S->>N: 데이터 요청
    Note over T: ⏸️ 블로킹 (다른 일 못 함)
    N-->>S: 데이터 도착
    S-->>T: 반환
    T->>T: 다음 작업

특징: read_some()이 데이터가 올 때까지 스레드를 점유. 연결 N개면 스레드 N개 필요.

논블로킹 I/O (Asio): 이벤트 기반

sequenceDiagram
    participant T as 스레드
    participant IO as io_context
    participant S1 as 소켓1
    participant S2 as 소켓2

    T->>IO: async_read(소켓1) 등록
    T->>IO: async_read(소켓2) 등록
    T->>IO: run()
    Note over T,IO: io_context가 완료된 연산만 실행
    IO->>S1: 소켓1 데이터 도착
    IO->>T: 콜백 실행 (소켓1)
    T->>IO: 다음 async_read 등록
    IO->>S2: 소켓2 데이터 도착
    IO->>T: 콜백 실행 (소켓2)

특징: 스레드는 콜백 실행만 담당. I/O 대기 시간에는 다른 연결 처리.

시각적 비교

flowchart TB
    subgraph Blocking["블로킹 (연결 3개)"]
        B1[스레드1: 연결A 대기]
        B2[스레드2: 연결B 대기]
        B3[스레드3: 연결C 대기]
    end

    subgraph Async["비동기 (연결 3개)"]
        A1[스레드1: A 콜백]
        A2[스레드1: B 콜백]
        A3[스레드1: C 콜백]
    end

    Blocking --> |"스레드 3개"| Blocking
    Async --> |"스레드 1개"| Async

3. io_context와 run

기본 개념

io_context는 Asio의 이벤트 루프입니다. async_accept, async_read, async_wait 같은 비동기 연산을 등록해 두면 io.run()이 완료된 연산의 콜백을 실행합니다.

flowchart LR
  A[async_* 등록] --> B[io_context]
  B --> C[run]
  C --> D[완료 시 콜백]
  D --> A

최소 예제: post로 작업 등록

#include <boost/asio.hpp>
#include <iostream>

int main() {
    boost::asio::io_context io;

    // 비동기 작업 등록 (post: 즉시 큐에 넣음)
    boost::asio::post(io,  {
        std::cout << "Hello from io_context!\n";
    });

    io.run();  // 등록된 작업이 완료될 때까지 실행
    return 0;
}

실행:

g++ -std=c++17 -o asio_hello asio_hello.cpp -lboost_system -pthread && ./asio_hello

출력:

Hello from io_context!

포인트:

  • post: 작업을 큐에 넣고 즉시 반환. 나중에 run()에서 실행
  • dispatch: run() 내부에서 호출 시 즉시 실행, 외부에서 호출 시 post와 동일
  • run(): 작업이 없으면 반환. work_guard를 두면 작업이 없어도 run이 끝나지 않게 할 수 있음

post vs dispatch

boost::asio::io_context io;

// post: 항상 큐에 넣고 반환
boost::asio::post(io,  { std::cout << "1\n"; });

// dispatch: run() 내부에서 호출 시 즉시 실행
boost::asio::post(io, [&io]() {
    std::cout << "2\n";
    boost::asio::dispatch(io,  { std::cout << "3 (즉시)\n"; });
    std::cout << "4\n";
});

io.run();
// 출력 순서: 1, 2, 3 (즉시), 4

run vs poll

// run(): 작업이 없을 때까지 블로킹
io.run();

// poll(): 대기 없이 즉시 반환 (한 번만 실행)
io.poll();

// poll_one(): 완료된 작업 하나만 처리
io.poll_one();

4. 비동기 타이머

기본: 1초 후 콜백

// g++ -std=c++17 -o asio_timer asio_timer.cpp -lboost_system -pthread && ./asio_timer
#include <boost/asio.hpp>
#include <iostream>

int main() {
    boost::asio::io_context io;
    boost::asio::steady_timer timer(io, std::chrono::seconds(1));

    timer.async_wait( {
        if (!ec) {
            std::cout << "1초 후 실행됨\n";
        }
    });

    io.run();
    return 0;
}

실행 결과:

1초 후 실행됨

반복 타이머: 주기적 실행

#include <boost/asio.hpp>
#include <iostream>

void schedule_timer(boost::asio::steady_timer& timer, int count) {
    timer.expires_after(std::chrono::seconds(1));
    timer.async_wait([&timer, count](const boost::system::error_code& ec) {
        if (ec) return;
        std::cout << "Tick " << count << "\n";
        if (count < 5) {
            schedule_timer(timer, count + 1);  // 다음 타이머 등록
        }
    });
}

int main() {
    boost::asio::io_context io;
    boost::asio::steady_timer timer(io);
    schedule_timer(timer, 1);
    io.run();
    return 0;
}

출력:

Tick 1
Tick 2
Tick 3
Tick 4
Tick 5

타임아웃과 함께 사용 (async_read 취소)

// async_read에 5초 타임아웃 적용
void read_with_timeout(tcp::socket& socket, asio::steady_timer& timer,
                       asio::mutable_buffer buf) {
    timer.expires_after(std::chrono::seconds(5));
    timer.async_wait([&socket](const boost::system::error_code& ec) {
        if (!ec) {
            socket.cancel();  // 5초 지나면 읽기 취소
        }
    });

    asio::async_read(socket, buf, [&timer](boost::system::error_code ec, size_t n) {
        timer.cancel();  // 읽기 완료 시 타이머 취소
        if (!ec) {
            // 데이터 처리
        }
    });
}

완전한 타이머 예제: 빌드 및 실행

// asio_timer_complete.cpp - 저장 후 아래 명령으로 빌드
// g++ -std=c++17 -o asio_timer_complete asio_timer_complete.cpp -lboost_system -pthread
#include <boost/asio.hpp>
#include <iostream>
#include <chrono>

namespace asio = boost::asio;

int main() {
    asio::io_context io;
    asio::steady_timer timer(io);

    auto start = std::chrono::steady_clock::now();

    auto schedule = [&](int count) {
        timer.expires_after(std::chrono::seconds(1));
        timer.async_wait([&, count](const boost::system::error_code& ec) {
            if (ec) return;
            auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
                std::chrono::steady_clock::now() - start).count();
            std::cout << "[" << elapsed << "s] Tick " << count << "\n";
            if (count < 3) schedule(count + 1);
        });
    };

    schedule(1);
    io.run();
    return 0;
}

5. async_read / async_write 완전 예제

async_read: 버퍼가 찰 때까지

#include <boost/asio.hpp>
#include <array>
#include <iostream>

namespace asio = boost::asio;
using asio::ip::tcp;

void do_read(tcp::socket& socket) {
    auto buf = std::make_shared<std::array<char, 1024>>();

    asio::async_read(socket, asio::buffer(*buf),
        [&socket, buf](boost::system::error_code ec, std::size_t length) {
            if (ec) {
                if (ec != asio::error::eof) {
                    std::cerr << "Read error: " << ec.message() << "\n";
                }
                return;
            }
            std::cout << "Received " << length << " bytes: ";
            std::cout.write(buf->data(), length);
            std::cout << "\n";

            // 다음 읽기 등록 (체이닝)
            do_read(socket);
        });
}

int main() {
    asio::io_context io;
    tcp::socket socket(io);
    tcp::resolver resolver(io);

    resolver.async_resolve("localhost", "8080",
        [&](boost::system::error_code ec, tcp::resolver::results_type results) {
            if (ec) return;
            asio::async_connect(socket, results,
                [&](boost::system::error_code ec, const tcp::endpoint&) {
                    if (ec) return;
                    do_read(socket);
                });
        });

    io.run();
    return 0;
}

주의: async_read버퍼가 가득 찰 때까지 또는 EOF까지 대기. 가변 길이 데이터는 async_read_until 또는 async_read_some 사용.

async_read_until: 구분자까지 읽기

#include <boost/asio.hpp>
#include <boost/asio/read_until.hpp>

void do_read_until(tcp::socket& socket) {
    auto buf = std::make_shared<asio::streambuf>();

    asio::async_read_until(socket, *buf, '\n',
        [&socket, buf](boost::system::error_code ec, std::size_t length) {
            if (ec) return;
            std::istream is(buf.get());
            std::string line;
            std::getline(is, line);
            std::cout << "Line: " << line << "\n";
            do_read_until(socket);
        });
}

async_write: 전송 완료 보장

void do_write(tcp::socket& socket, const std::string& message) {
    auto buf = std::make_shared<std::string>(message);

    asio::async_write(socket, asio::buffer(*buf),
        [buf](boost::system::error_code ec, std::size_t length) {
            if (ec) {
                std::cerr << "Write error: " << ec.message() << "\n";
                return;
            }
            std::cout << "Sent " << length << " bytes\n";
        });
}

async_write vs async_write_some:

  • async_write: 전체 버퍼 전송 완료까지 반복 (부분 전송 시 자동 재시도)
  • async_write_some: 일부만 전송해도 콜백 호출

async_read_some: 가변 길이 데이터

// 한 번에 최대 1024바이트만 읽음 (버퍼 가득 차지 않아도 완료)
void do_read_some(tcp::socket& socket) {
    auto buf = std::make_shared<std::array<char, 1024>>();
    asio::async_read_some(socket, asio::buffer(*buf),
        [&socket, buf](boost::system::error_code ec, std::size_t n) {
            if (ec) return;
            // n바이트 처리 후 다음 읽기 등록
            // handle_data(buf->data(), n);
            do_read_some(socket);
        });
}

6. 비동기 TCP 클라이언트

흐름도

flowchart TD
    A[async_resolve] --> B[async_connect]
    B --> C[async_write 요청]
    C --> D[async_read 응답]
    D --> E{더 읽을 데이터?}
    E -->|예| D
    E -->|아니오| F[종료]
  • async_connect로 연결
  • 연결 완료 핸들러에서 async_read / async_write 호출
  • 읽기 완료 핸들러에서 다시 async_read를 걸어 “다음 데이터” 대기 (체이닝)
// 클라이언트 흐름
resolver.async_resolve(...)
    -> async_connect(...)
        -> async_write(요청)
            -> async_read(응답)
                -> async_read(다음 응답)  // 체이닝

완전한 에코 클라이언트 예제

// asio_echo_client.cpp - 서버에 연결 후 입력을 에코로 받음
// g++ -std=c++17 -o asio_echo_client asio_echo_client.cpp -lboost_system -pthread
#include <boost/asio.hpp>
#include <iostream>
#include <string>

namespace asio = boost::asio;
using asio::ip::tcp;

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " <host> <port>\n";
        return 1;
    }

    asio::io_context io;
    tcp::socket socket(io);
    tcp::resolver resolver(io);

    resolver.async_resolve(argv[1], argv[2],
        [&](boost::system::error_code ec, tcp::resolver::results_type results) {
            if (ec) {
                std::cerr << "Resolve: " << ec.message() << "\n";
                return;
            }
            asio::async_connect(socket, results,
                [&](boost::system::error_code ec, const tcp::endpoint&) {
                    if (ec) {
                        std::cerr << "Connect: " << ec.message() << "\n";
                        return;
                    }
                    std::cout << "Connected. Type messages (Ctrl+D to exit).\n";
                    // 첫 읽기 시작
                    auto buf = std::make_shared<std::array<char, 1024>>();
                    asio::async_read_some(socket, asio::buffer(*buf),
                        [&, buf](boost::system::error_code ec, std::size_t n) {
                            if (!ec && n > 0) {
                                std::cout.write(buf->data(), n);
                            }
                        });
                });
        });

    io.run();
    return 0;
}

7. 비동기 서버 (async_accept)

Session 클래스와 async_read 체이닝

namespace asio = boost::asio;
using asio::ip::tcp;

class Session : public std::enable_shared_from_this<Session> {
public:
    explicit Session(tcp::socket socket) : socket_(std::move(socket)) {}

    void start() {
        do_read();
    }

private:
    void do_read() {
        auto self(shared_from_this());
        asio::async_read_until(socket_, buffer_, '\n',
            [this, self](boost::system::error_code ec, std::size_t length) {
                if (!ec) {
                    std::istream is(&buffer_);
                    std::string line;
                    std::getline(is, line);
                    do_write("Echo: " + line + "\n");
                }
            });
    }

    void do_write(const std::string& msg) {
        auto self(shared_from_this());
        asio::async_write(socket_, asio::buffer(msg),
            [this, self](boost::system::error_code ec, std::size_t) {
                if (!ec) {
                    do_read();  // 다음 읽기
                }
            });
    }

    tcp::socket socket_;
    asio::streambuf buffer_;
};

void do_accept(tcp::acceptor& acceptor, asio::io_context& io) {
    acceptor.async_accept([&acceptor, &io](boost::system::error_code ec,
                                            tcp::socket socket) {
        if (ec) return;
        std::make_shared<Session>(std::move(socket))->start();
        do_accept(acceptor, io);  // 다음 연결 대기
    });
}

int main() {
    asio::io_context io;
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
    do_accept(acceptor, io);
    io.run();
}
  • 수락 핸들러에서 socket을 받고, 그 소켓으로 async_read / async_write 시작
  • 핸들러 끝에서 다시 do_accept를 호출해 다음 연결 대기

빌드 및 테스트

# 터미널 1: 서버 실행
g++ -std=c++17 -o asio_echo_server asio_echo_server.cpp -lboost_system -pthread
./asio_echo_server

# 터미널 2: 클라이언트 (nc로 테스트)
echo "Hello Asio" | nc localhost 8080

예상 출력:

Echo: Hello Asio

8. 에러 처리 패턴

기본: error_code 확인

acceptor.async_accept([&](boost::system::error_code ec, tcp::socket socket) {
    if (ec) {
        std::cerr << "Accept error: " << ec.message() << "\n";
        return;  // 에러 시 재등록하지 않음 -> run() 종료 가능
    }
    // 정상 처리
});

연결 종료 처리

void do_read(tcp::socket& socket) {
    asio::async_read(socket, buf, [&](boost::system::error_code ec, size_t n) {
        if (ec) {
            if (ec == asio::error::eof) {
                // 정상 종료: 상대가 연결 끊음
                return;
            }
            if (ec == asio::error::operation_aborted) {
                // 취소됨 (타임아웃 등)
                return;
            }
            std::cerr << "Error: " << ec.message() << "\n";
            return;
        }
        // 정상 처리
    });
}

주요 에러 코드 정리

에러의미대응
eof상대가 연결 종료정상 처리, 세션 정리
operation_abortedcancel() 호출됨타임아웃 등 의도적 취소
connection_reset상대가 비정상 종료로깅 후 정리
broken_pipe닫힌 소켓에 쓰기쓰기 중단

exception_ptr 사용 (선택)

asio::async_read(socket, buf,
    asio::bind_executor(strand_,
         {
            if (ec) {
                // 로깅 후 재등록 또는 종료
            }
        }));

9. 자주 하는 실수 (핸들러 수명, work_guard)

실수 1: 핸들러에서 dangling reference

// ❌ 잘못된 예: session이 소멸된 뒤 콜백 실행 가능
void do_read() {
    asio::async_read(socket_, buf, [this](boost::system::error_code ec, size_t n) {
        // this가 이미 소멸됐을 수 있음!
        process_data();
    });
}
// ✅ 올바른 예: shared_from_this로 수명 연장
void do_read() {
    auto self(shared_from_this());
    asio::async_read(socket_, buf, [this, self](boost::system::error_code ec, size_t n) {
        if (!ec) process_data();
    });
}

실수 2: work_guard 없이 run() 즉시 종료

// ❌ 문제: async_accept 한 번만 등록 -> 연결 받고 run() 종료
asio::io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
acceptor.async_accept( { /* ... */ });
io.run();  // 연결 하나 받고 바로 끝!
// ✅ 해결 1: 핸들러에서 do_accept 재호출 (이미 예제에 포함)
// ✅ 해결 2: work_guard로 "할 일 있음" 유지
asio::executor_work_guard<asio::io_context::executor_type> work =
    asio::make_work_guard(io);
// 이제 io에 작업이 없어도 run()이 반환하지 않음

실수 3: 버퍼 수명

// ❌ 잘못된 예: 스택 버퍼는 콜백 실행 시 이미 소멸
void do_read() {
    std::array<char, 1024> buf;
    asio::async_read(socket_, asio::buffer(buf), [...]);  // 위험!
}

// ✅ 올바른 예: shared_ptr로 버퍼 수명 연장
void do_read() {
    auto buf = std::make_shared<std::array<char, 1024>>();
    asio::async_read(socket_, asio::buffer(*buf),
        [buf, this](boost::system::error_code ec, size_t n) { /* ... */ });
}

실수 4: io_context 재사용 시 restart() 누락

// ❌ run() 반환 후 같은 io로 다시 run() 호출 시 아무 일도 안 함
io.run();  // 작업 완료로 반환
io.run();  // 즉시 반환 (아무 작업 없음)
// ✅ restart() 후 재실행
io.run();
io.restart();  // run() 상태 초기화
// 새 작업 등록 후
io.run();

실수 5: 멀티스레드에서 strand 미사용

// ❌ 여러 스레드가 같은 소켓에 async_read/async_write 동시 등록 -> 데이터 레이스
std::thread t1([&]() { do_read(socket); });
std::thread t2([&]() { do_write(socket, "x"); });
// ✅ strand로 핸들러 직렬화
asio::io_context::strand strand(io);
asio::async_read(socket, buf, asio::bind_executor(strand, handler));
asio::async_write(socket, buf, asio::bind_executor(strand, handler));

실수 6: 람다 캡처로 인한 수명 문제

// ❌ 참조 캡처: acceptor가 스코프 밖으로 나가면 dangling
acceptor.async_accept([&acceptor](...) {
    do_accept(acceptor);  // acceptor 참조가 무효화됐을 수 있음
});
// ✅ shared_ptr 또는 포인터로 안전하게 전달
auto acc = std::make_shared<tcp::acceptor>(std::move(acceptor));
acc->async_accept([acc](...) {
    do_accept(*acc);
});

10. 모범 사례 (Best Practices)

1. 버퍼 선택 가이드

용도권장이유
고정 길이 프로토콜std::array + shared_ptr수명 관리 용이
가변 길이 (줄 단위)asio::streambuf + read_until구분자까지 읽기
대용량 스트리밍std::vector + shared_ptr동적 확장

2. shared_from_this 사용 조건

// Session이 shared_ptr로 관리될 때만 사용 가능
class Session : public std::enable_shared_from_this<Session> {
    // 생성 직후 shared_from_this() 호출 시 undefined behavior!
    // 반드시 std::make_shared<Session>(...)로 생성된 뒤에만 호출
};

3. 에러 시 리소스 정리 순서

void do_read() {
    asio::async_read(socket_, buf, [this](boost::system::error_code ec, size_t n) {
        if (ec) {
            socket_.close();   // 1. 소켓 먼저 닫기
            cleanup();        // 2. 세션 정리
            return;           // 3. 재등록하지 않음
        }
        process();
        do_read();  // 정상 시에만 다음 읽기
    });
}

4. 타임아웃은 타이머 + cancel 조합

// 읽기와 타이머를 함께 등록, 먼저 완료되는 쪽이 나머지 취소
timer_.expires_after(std::chrono::seconds(30));
timer_.async_wait([this](auto ec) { if (!ec) socket_.cancel(); });
asio::async_read_until(socket_, buffer_, '\n',
    [this](auto ec, auto n) {
        timer_.cancel();  // 읽기 완료 시 타이머 취소
        if (!ec) handle_read(n);
    });

5. 로깅은 핸들러 진입/종료 시

acceptor.async_accept([&](boost::system::error_code ec, tcp::socket socket) {
    if (ec) {
        spdlog::error("Accept failed: {}", ec.message());
        return;
    }
    spdlog::info("Connection from {}", socket.remote_endpoint().address().to_string());
    // ...
});

11. 성능 비교: 동기 vs 비동기

벤치마크 시나리오

  • 동시 연결: 1,000개
  • 각 연결: 100바이트 요청 -> 에코 응답
  • 테스트 환경: 로컬 (localhost)
방식스레드 수메모리 (대략)처리량 (req/s)
동기 (1 스레드)1낮음~500
동기 (스레드/연결)1,000~500MB+~3,000 (컨텍스트 스위칭 비용)
비동기 (1 스레드)1낮음~8,000
비동기 (4 스레드)4낮음~25,000

결론:

  • 연결 수가 많을수록 비동기가 압도적으로 유리
  • 동기 스레드/연결은 스케일 한계에 빠르게 도달
  • 비동기 멀티스레드 run()으로 CPU 코어 활용 극대화 가능

12. 프로덕션 패턴

연결 제한

std::atomic<int> connection_count{0};
const int max_connections = 10000;

void do_accept(tcp::acceptor& acceptor) {
    acceptor.async_accept([&](boost::system::error_code ec, tcp::socket socket) {
        if (ec) return;
        if (connection_count >= max_connections) {
            socket.close();
            do_accept(acceptor);
            return;
        }
        ++connection_count;
        std::make_shared<Session>(std::move(socket))->start();
        do_accept(acceptor);
    });
}

// Session 소멸 시 connection_count--

타임아웃 적용

// async_read에 30초 타임아웃
void Session::do_read() {
    timer_.expires_after(std::chrono::seconds(30));
    timer_.async_wait([this](boost::system::error_code ec) {
        if (!ec) socket_.cancel();
    });
    asio::async_read_until(socket_, buffer_, '\n', ...);
}

Graceful Shutdown

// SIGINT/SIGTERM 수신 시 io_context 중지
asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](auto, auto) {
    io.stop();  // run() 반환 유도
});

로깅

acceptor.async_accept([&](boost::system::error_code ec, tcp::socket socket) {
    if (ec) {
        spdlog::error("Accept failed: {}", ec.message());
        return;
    }
    spdlog::info("New connection from {}", socket.remote_endpoint());
    // ...
});

멀티스레드 run 패턴

asio::io_context io;
std::vector<std::thread> threads;
const int num_threads = 4;

for (int i = 0; i < num_threads; ++i) {
    threads.emplace_back([&io]() { io.run(); });
}
for (auto& t : threads) t.join();

HTTP/WebSocket

실제 프로토콜 구현에는 Boost.Beast처럼 Asio 기반 라이브러리를 사용하는 것을 권장합니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Boost.Asio io_context 이벤트 루프 | 동작 원리 정리 [#1]
  • C++ Asio 데드락 디버깅 | 비동기 콜백 실전 [#49-3]
  • C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지

이 글에서 다루는 키워드 (관련 검색어)

C++ Asio, Boost.Asio, 비동기 I/O, io_context, async_read, async_write 등으로 검색하시면 이 글이 도움이 됩니다.


정리

항목내용
io_context이벤트 루프, run()으로 실행
비동기async_* + 완료 시 콜백에서 다음 async 체이닝
서버async_accept -> (새 소켓) async_read/write
에러error_code 확인 후 정리·재등록
핸들러 수명shared_from_this, shared_ptr 버퍼
work_guardrun() 조기 종료 방지 (필요 시)

실전에서는 타임아웃(타이머와 함께 async 연산 취소), 연결 제한, 로깅을 두고, HTTP/WebSocket 등 프로토콜 구현에는 Boost.Beast처럼 Asio 기반 라이브러리를 쓰는 것을 권장합니다.

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Boost.Asio(및 standalone Asio)의 io_context, async_accept, async_read, async_write 기본 사용법과 비동기 흐름을 다룹니다. 고성능 네트워크 서버, 채팅, 게임 서버, 실시간 데이터 처리 등에서 활용됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 이벤트 루프(#29-2)에서 run, work_guard, 멀티스레드를 더 자세히 다룹니다.

한 줄 요약: io_context와 비동기 연산으로 논블로킹 네트워크 코드를 작성할 수 있습니다. 다음으로 이벤트 루프(#29-2)를 읽어보면 좋습니다.

다음 글: [C++ 실전 가이드 #29-2] 비동기 I/O와 이벤트 루프: Asio의 run과 완료 핸들러

이전 글: [C++ 실전 가이드 #28-3] 네트워크 에러 처리와 타임아웃: 연결 실패와 재시도


관련 글

  • C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post
  • C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
  • C++ 소켓 프로그래밍 완벽 가이드 | TCP/UDP·소켓 옵션·논블로킹·에러 처리 [#28-1]
  • C++ 네트워크 에러 완벽 가이드 | errno·타임아웃·재시도·서킷브레이커 [#28-3]
  • C++ HTTP 기초 완벽 가이드 | 요청/응답 파싱·헤더·청크 인코딩·Beast 실전 [#30-1]