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는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
목차
- 비동기 I/O가 왜 필요한가요?
- 블로킹 vs 논블로킹 비교
- io_context와 run
- 비동기 타이머
- async_read / async_write 완전 예제
- 비동기 TCP 클라이언트
- 비동기 서버 (async_accept)
- 에러 처리 패턴
- 자주 하는 실수 (핸들러 수명, work_guard)
- 모범 사례 (Best Practices)
- 성능 비교: 동기 vs 비동기
- 프로덕션 패턴
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_aborted | cancel() 호출됨 | 타임아웃 등 의도적 취소 |
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_guard | run() 조기 종료 방지 (필요 시) |
실전에서는 타임아웃(타이머와 함께 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]