[2026] C++ Boost.Asio 입문 | io_context·async_read
이 글의 핵심
C++ Boost.Asio 입문: io_context·async_read. 실무에서 겪은 문제·비동기 I/O가 왜 필요한가요?
들어가며: “비동기 I/O가 왜 필요한가요?”
문제 시나리오 1: 블로킹 서버의 한계
채팅 서버를 만든다고 상상해 보세요. 동기(블로킹) 방식으로 구현하면: 다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 동기 서버: 한 연결 처리 중에는 다른 연결을 받을 수 없음
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는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 비동기 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: 스레드가 대기
아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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): 이벤트 기반
다음은 mermaid를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 대기 시간에는 다른 연결 처리.
시각적 비교
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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()이 완료된 연산의 콜백을 실행합니다.
아래 코드는 mermaid를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
flowchart LR A[async_* 등록] --> B[io_context] B --> C[run] C --> D[완료 시 콜백] D --> A
최소 예제: post로 작업 등록
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// run(): 작업이 없을 때까지 블로킹
io.run();
// poll(): 대기 없이 즉시 반환 (한 번만 실행)
io.poll();
// poll_one(): 완료된 작업 하나만 처리
io.poll_one();
4. 비동기 타이머
기본: 1초 후 콜백
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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초 후 실행됨
반복 타이머: 주기적 실행
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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;
}
출력: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Tick 1
Tick 2
Tick 3
Tick 4
Tick 5
타임아웃과 함께 사용 (async_read 취소)
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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) {
// 데이터 처리
}
});
}
완전한 타이머 예제: 빌드 및 실행
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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: 버퍼가 찰 때까지
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 구분자까지 읽기
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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: 전송 완료 보장
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 가변 길이 데이터
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 한 번에 최대 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 클라이언트
흐름도
아래 코드는 mermaid를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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를 걸어 “다음 데이터” 대기 (체이닝) 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 클라이언트 흐름
resolver.async_resolve(...)
-> async_connect(...)
-> async_write(요청)
-> async_read(응답)
-> async_read(다음 응답) // 체이닝
완전한 에코 클라이언트 예제
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 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 체이닝
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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를 호출해 다음 연결 대기
빌드 및 테스트
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 터미널 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 확인
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
acceptor.async_accept([&](boost::system::error_code ec, tcp::socket socket) {
if (ec) {
std::cerr << "Accept error: " << ec.message() << "\n";
return; // 에러 시 재등록하지 않음 -> run() 종료 가능
}
// 정상 처리
});
연결 종료 처리
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 사용 (선택)
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
asio::async_read(socket, buf,
asio::bind_executor(strand_,
{
if (ec) {
// 로깅 후 재등록 또는 종료
}
}));
9. 자주 하는 실수 (핸들러 수명, work_guard)
실수 1: 핸들러에서 dangling reference
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 예: session이 소멸된 뒤 콜백 실행 가능
void do_read() {
asio::async_read(socket_, buf, [this](boost::system::error_code ec, size_t n) {
// this가 이미 소멸됐을 수 있음!
process_data();
});
}
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 예: 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() 즉시 종료
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 문제: async_accept 한 번만 등록 -> 연결 받고 run() 종료
asio::io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
acceptor.async_accept( { /* ....*/ });
io.run(); // 연결 하나 받고 바로 끝!
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 해결 1: 핸들러에서 do_accept 재호출 (이미 예제에 포함)
// ✅ 해결 2: work_guard로 "할 일 있음" 유지
asio::executor_work_guard<asio::io_context::executor_type> work =
asio::make_work_guard(io);
// 이제 io에 작업이 없어도 run()이 반환하지 않음
실수 3: 버퍼 수명
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 예: 스택 버퍼는 콜백 실행 시 이미 소멸
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(); // 즉시 반환 (아무 작업 없음)
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 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"); });
다음은 간단한 cpp 코드 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 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: 람다 캡처로 인한 수명 문제
다음은 간단한 cpp 코드 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 참조 캡처: acceptor가 스코프 밖으로 나가면 dangling
acceptor.async_accept([&acceptor](...) {
do_accept(acceptor); // acceptor 참조가 무효화됐을 수 있음
});
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 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 사용 조건
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Session이 shared_ptr로 관리될 때만 사용 가능
class Session : public std::enable_shared_from_this<Session> {
// 생성 직후 shared_from_this() 호출 시 undefined behavior!
// 반드시 std::make_shared<Session>(...)로 생성된 뒤에만 호출
};
3. 에러 시 리소스 정리 순서
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 조합
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 읽기와 타이머를 함께 등록, 먼저 완료되는 쪽이 나머지 취소
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. 로깅은 핸들러 진입/종료 시
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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. 프로덕션 패턴
연결 제한
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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--
타임아웃 적용
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 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
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// SIGINT/SIGTERM 수신 시 io_context 중지
asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](auto, auto) {
io.stop(); // run() 반환 유도
});
로깅
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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 패턴
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.