[2026] C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post
이 글의 핵심
Asio 이벤트 루프의 모든 것: run/run_one/poll 차이, post/dispatch 작업 큐, work_guard로 서버 유지, strand 동기화, C++20 코루틴, 일반적인 에러와 프로덕션 패턴까지 실전 코드로.
들어가며: “서버가 바로 종료돼요. run()이 끝나지 않게 하려면?”
문제 상황 1: 서버가 즉시 종료됨
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 문제: 서버가 바로 종료됨
boost::asio::io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
// async_accept 한 번만 등록
acceptor.async_accept( {
std::cout << "Connected!\n";
// 여기서 끝
});
io.run(); // 💥 연결 하나 받고 바로 종료!
std::cout << "Server stopped\n"; // 즉시 출력됨
왜 이런 일이 발생할까요?
io_context::run()은 등록된 비동기 작업이 모두 완료되면 반환합니다. 위 코드는 async_accept 하나만 등록했으므로, 연결 하나를 받으면 더 이상 할 일이 없어서 run()이 종료됩니다.
추가 문제 시나리오
시나리오 2: run()이 끝나지 않아요
work_guard를 사용했는데 reset()을 호출하지 않아 서버를 종료할 수 없는 경우. SIGINT 핸들러에서 work_guard.reset()을 호출해야 graceful shutdown이 가능합니다.
시나리오 3: 멀티스레드에서 데이터 레이스
여러 스레드가 같은 io_context::run()을 호출할 때, 공유 변수에 mutex 없이 접근하면 undefined behavior가 발생합니다. strand로 순차 실행을 보장해야 합니다.
시나리오 4: 핸들러 실행 순서 혼란
post와 dispatch의 차이를 모르고 사용하면, 핸들러가 예상과 다른 순서로 실행될 수 있습니다. dispatch는 현재 핸들러 내부에서 즉시 실행되므로 재귀 깊이에 주의해야 합니다.
시나리오 5: run() 호출 후 io_context 재사용
io.run()이 반환된 io_context는 “stopped” 상태입니다. io.restart()를 호출하지 않고 다시 run()을 호출하면 아무 작업도 실행되지 않습니다.
해결책:
- work_guard: “아직 할 일이 있다”고 표시
- 완료 핸들러에서 재등록:
async_accept완료 시 다시async_accept등록 - 멀티스레드 run(): 여러 스레드가 동시에 이벤트 처리 목표:
run()/run_one()/poll()동작 이해post/dispatch로 작업 큐 관리work_guard로 서버 유지- 멀티스레드 이벤트 루프 구현
- 완료 핸들러 체이닝 패턴 요구 환경: Boost.Asio 1.70 이상 이 글을 읽으면:
- 이벤트 루프의 동작 원리를 이해할 수 있습니다.
- 서버가 종료되지 않게 유지할 수 있습니다.
- 멀티스레드로 성능을 향상시킬 수 있습니다.
개념을 잡는 비유
소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 이벤트 루프 동작 원리
- run/run_one/poll 비교
- work_guard로 서버 유지
- post와 dispatch
- 멀티스레드 이벤트 루프
- strand 완전 예제
- C++20 코루틴
- 완료 핸들러 체이닝
- 실전 예시
- 일반적인 에러와 해결법
- 모범 사례
- 프로덕션 패턴
1. 이벤트 루프 동작 원리
이벤트 루프란?
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
Start["io_context run 시작"]
Check{등록된\n작업 있음?}
Wait[I/O 이벤트 대기]
Execute[완료 핸들러 실행]
Done[run 종료]
Start --> Check
Check -->|Yes| Wait
Wait --> Execute
Execute --> Check
Check -->|No| Done
style Wait fill:#ffeb3b
style Execute fill:#4caf50
style Done fill:#f44336
이벤트 루프의 핵심:
- 등록된 비동기 작업을 확인
- I/O 이벤트 발생 대기 (epoll/kqueue/IOCP)
- 이벤트 발생 시 완료 핸들러 실행
- 1번으로 돌아가서 반복
- 더 이상 작업이 없으면 종료
시퀀스 다이어그램: run() 동작
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
sequenceDiagram
participant Main as 메인 스레드
participant IO as io_context
participant Kernel as OS (epoll/kqueue)
Main->>IO: post(핸들러1), post(핸들러2)
Main->>IO: run() 호출
IO->>IO: 작업 큐 확인 (2개)
loop 이벤트 루프
IO->>Kernel: 이벤트 대기 (또는 즉시 실행)
Kernel-->>IO: 준비된 작업
IO->>IO: 핸들러1 실행
IO->>IO: 핸들러2 실행
IO->>IO: 작업 없음?
end
IO-->>Main: run() 반환
내부 동작 이해
#include <boost/asio.hpp>
#include <iostream>
#include <thread>
using boost::asio::ip::tcp;
using boost::system::error_code;
void demonstrate_event_loop() {
boost::asio::io_context io;
std::cout << "1. run() 호출 전\n";
// 비동기 작업 등록
boost::asio::post(io, {
std::cout << "2. 첫 번째 핸들러 실행\n";
});
boost::asio::post(io, {
std::cout << "3. 두 번째 핸들러 실행\n";
});
std::cout << "4. run() 호출\n";
io.run(); // 여기서 2, 3번 핸들러 실행
std::cout << "5. run() 종료 (더 이상 작업 없음)\n";
}
// 출력:
// 1. run() 호출 전
// 4. run() 호출
// 2. 첫 번째 핸들러 실행
// 3. 두 번째 핸들러 실행
// 5. run() 종료 (더 이상 작업 없음)
2. run/run_one/poll 비교
세 가지 실행 방식
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <boost/asio.hpp>
#include <iostream>
void compare_run_methods() {
boost::asio::io_context io;
// 작업 3개 등록
for (int i = 1; i <= 3; ++i) {
boost::asio::post(io, [i]() {
std::cout << "Task " << i << "\n";
});
}
// 1. run(): 모든 작업 실행
{
boost::asio::io_context io1;
for (int i = 1; i <= 3; ++i) {
boost::asio::post(io1, [i]() {
std::cout << "run: Task " << i << "\n";
});
}
io1.run(); // Task 1, 2, 3 모두 실행
std::cout << "run() completed\n";
}
// 2. run_one(): 한 번에 하나씩
{
boost::asio::io_context io2;
for (int i = 1; i <= 3; ++i) {
boost::asio::post(io2, [i]() {
std::cout << "run_one: Task " << i << "\n";
});
}
io2.run_one(); // Task 1만 실행
std::cout << "First run_one() completed\n";
io2.run_one(); // Task 2만 실행
std::cout << "Second run_one() completed\n";
io2.run(); // Task 3 실행
}
// 3. poll(): 대기 없이 준비된 작업만
{
boost::asio::io_context io3;
// 즉시 실행 가능한 작업
boost::asio::post(io3, {
std::cout << "poll: Immediate task\n";
});
// I/O 대기가 필요한 작업 (타이머)
boost::asio::steady_timer timer(io3, std::chrono::seconds(1));
timer.async_wait( {
std::cout << "poll: Timer expired\n";
});
io3.poll(); // Immediate task만 실행 (타이머는 실행 안 됨)
std::cout << "poll() completed (no blocking)\n";
// 타이머 만료까지 대기하려면 run() 필요
io3.run(); // Timer expired 실행
}
}
비교표
| 메서드 | 동작 | 사용 사례 |
|---|---|---|
| run() | 모든 작업 완료까지 블로킹 | 서버 메인 루프 |
| run_one() | 한 작업만 실행 후 반환 | 작업 단위 제어 |
| poll() | 대기 없이 준비된 작업만 | 게임 루프, UI 업데이트 |
| run_for(duration) | 시간 제한 실행 | 타임아웃 필요 시 |
| run_until(time_point) | 특정 시각까지 실행 | 스케줄링 |
3. work_guard로 서버 유지
문제: 서버가 바로 종료됨
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 서버 코드
void broken_server() {
boost::asio::io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
acceptor.async_accept( {
std::cout << "Client connected\n";
// 한 번만 실행되고 끝
});
io.run(); // 연결 하나 받고 종료!
std::cout << "Server stopped\n";
}
해결책 1: work_guard 사용
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <boost/asio.hpp>
#include <iostream>
void server_with_work_guard() {
boost::asio::io_context io;
// work_guard 생성: "아직 할 일이 있다"고 표시
auto work = boost::asio::make_work_guard(io);
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
acceptor.async_accept( {
std::cout << "Client connected\n";
});
std::cout << "Server started on port 8080\n";
// work_guard가 있으므로 run()이 종료되지 않음
std::thread server_thread([&io]() {
io.run();
});
// 5초 후 종료
std::this_thread::sleep_for(std::chrono::seconds(5));
// work_guard 해제 → run() 종료
work.reset();
server_thread.join();
std::cout << "Server stopped\n";
}
해결책 2: 완료 핸들러에서 재등록 (권장)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Server {
boost::asio::io_context& io_;
tcp::acceptor acceptor_;
public:
Server(boost::asio::io_context& io, uint16_t port)
: io_(io), acceptor_(io, tcp::endpoint(tcp::v4(), port)) {
start_accept();
}
private:
void start_accept() {
// ✅ 핵심: 완료 핸들러에서 다시 start_accept() 호출
acceptor_.async_accept(
[this](error_code ec, tcp::socket socket) {
if (!ec) {
std::cout << "Client connected from "
<< socket.remote_endpoint() << "\n";
// 클라이언트 처리 (세션 시작)
handle_client(std::move(socket));
}
// 다음 연결 대기 (재귀적 등록)
start_accept();
}
);
}
void handle_client(tcp::socket socket) {
// 클라이언트와 통신
auto buffer = std::make_shared<std::array<char, 1024>>();
socket.async_read_some(
boost::asio::buffer(*buffer),
[buffer, socket = std::move(socket)](error_code ec, size_t bytes) mutable {
if (!ec) {
std::cout << "Received " << bytes << " bytes\n";
// Echo back
boost::asio::async_write(
socket,
boost::asio::buffer(*buffer, bytes),
{}
);
}
}
);
}
};
void run_server() {
boost::asio::io_context io;
Server server(io, 8080);
std::cout << "Server started on port 8080\n";
io.run(); // 계속 실행됨 (async_accept가 계속 등록되므로)
}
4. post와 dispatch
post: 항상 큐에 넣기
void demonstrate_post() {
boost::asio::io_context io;
std::cout << "Main thread: " << std::this_thread::get_id() << "\n";
// post: 항상 나중에 실행
boost::asio::post(io, {
std::cout << "Handler thread: " << std::this_thread::get_id() << "\n";
std::cout << "This runs later\n";
});
std::cout << "Before run()\n";
io.run();
std::cout << "After run()\n";
}
// 출력:
// Main thread: 123456
// Before run()
// Handler thread: 123456
// This runs later
// After run()
dispatch: 가능하면 즉시 실행
void demonstrate_dispatch() {
boost::asio::io_context io;
// dispatch: run() 실행 중이면 즉시 실행 가능
boost::asio::dispatch(io, {
std::cout << "Dispatch 1\n";
// 핸들러 내부에서 dispatch → 즉시 실행
boost::asio::dispatch(io, {
std::cout << "Dispatch 2 (immediate)\n";
});
// 핸들러 내부에서 post → 큐에 넣음
boost::asio::post(io, {
std::cout << "Post (queued)\n";
});
std::cout << "Dispatch 1 end\n";
});
io.run();
}
// 출력:
// Dispatch 1
// Dispatch 2 (immediate)
// Dispatch 1 end
// Post (queued)
언제 무엇을 사용할까?
| 상황 | 사용 | 이유 |
|---|---|---|
| 다른 스레드에서 작업 등록 | post | 스레드 안전 |
| 핸들러 내부에서 작업 등록 | dispatch | 오버헤드 감소 |
| 순서 보장 필요 | post | 큐 순서 보장 |
| 즉시 실행 가능 | dispatch | 성능 최적화 |
5. 멀티스레드
일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다. 이벤트 루프
단일 스레드 vs 멀티스레드
다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 단일 스레드: 한 번에 하나씩 처리
void single_threaded_server() {
boost::asio::io_context io;
// ....acceptor 설정 ...
io.run(); // 메인 스레드에서만 실행
}
// 멀티스레드: 여러 핸들러 동시 처리
void multi_threaded_server() {
boost::asio::io_context io;
// ....acceptor 설정 ...
// 4개 스레드가 같은 io_context 처리
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back([&io]() {
io.run();
});
}
for (auto& t : threads) {
t.join();
}
}
스레드 풀 구현
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class ThreadPool {
boost::asio::io_context io_;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> work_;
std::vector<std::thread> threads_;
public:
ThreadPool(size_t num_threads)
: work_(boost::asio::make_work_guard(io_)) {
for (size_t i = 0; i < num_threads; ++i) {
threads_.emplace_back([this]() {
io_.run();
});
}
}
~ThreadPool() {
work_.reset(); // work_guard 해제
for (auto& t : threads_) {
t.join();
}
}
// 작업 추가
template<typename F>
void post(F&& f) {
boost::asio::post(io_, std::forward<F>(f));
}
boost::asio::io_context& get_io_context() {
return io_;
}
};
// 사용 예시
void use_thread_pool() {
ThreadPool pool(4); // 4개 스레드
// 작업 추가
for (int i = 0; i < 10; ++i) {
pool.post([i]() {
std::cout << "Task " << i
<< " on thread " << std::this_thread::get_id() << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
});
}
std::this_thread::sleep_for(std::chrono::seconds(2));
}
동기화 주의사항
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Counter {
int count_ = 0;
std::mutex mutex_; // ❌ 멀티스레드에서 필요
public:
void increment() {
std::lock_guard<std::mutex> lock(mutex_);
++count_;
}
int get() {
std::lock_guard<std::mutex> lock(mutex_);
return count_;
}
};
// ✅ strand 사용 (Asio의 동기화 메커니즘)
class StrandCounter {
boost::asio::io_context::strand strand_;
int count_ = 0; // strand로 보호되므로 mutex 불필요
public:
StrandCounter(boost::asio::io_context& io)
: strand_(io) {}
void increment() {
boost::asio::post(strand_, [this]() {
++count_; // strand 내에서 실행 → 순차 보장
});
}
void get(std::function<void(int)> callback) {
boost::asio::post(strand_, [this, callback]() {
callback(count_);
});
}
};
6. strand 완전 예제
strand는 같은 io_context에서 순차 실행을 보장하는 실행 컨텍스트입니다. mutex 없이 공유 자원을 안전하게 접근할 수 있습니다.
strand로 보호된 Echo 세션
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <boost/asio.hpp>
#include <memory>
using boost::asio::ip::tcp;
using boost::system::error_code;
class StrandEchoSession : public std::enable_shared_from_this<StrandEchoSession> {
tcp::socket socket_;
boost::asio::io_context::strand strand_;
std::array<char, 1024> buffer_;
public:
StrandEchoSession(tcp::socket socket)
: socket_(std::move(socket)),
strand_(socket_.get_executor()) {}
void start() {
boost::asio::dispatch(strand_, [self = shared_from_this()]() {
self->do_read();
});
}
private:
void do_read() {
auto self = shared_from_this();
socket_.async_read_some(
boost::asio::buffer(buffer_),
boost::asio::bind_executor(strand_, [this, self](error_code ec, size_t bytes) {
if (!ec) do_write(bytes);
})
);
}
void do_write(size_t bytes) {
auto self = shared_from_this();
boost::asio::async_write(
socket_, boost::asio::buffer(buffer_, bytes),
boost::asio::bind_executor(strand_, [this, self](error_code ec, size_t) {
if (!ec) do_read();
})
);
}
};
strand vs mutex
| 방식 | 장점 | 단점 |
|---|---|---|
| strand | 데드락 없음, Asio 네이티브 | strand 범위 설계 필요 |
| mutex | 기존 코드와 호환 | 데드락 위험, 성능 오버헤드 |
7. C++20 코루틴
Boost.Asio는 C++20 코루틴을 지원합니다. co_await로 콜백 지옥을 피하고 동기 코드처럼 작성할 수 있습니다.
Echo 서버 (코루틴)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#if __cplusplus >= 202002L
namespace asio = boost::asio;
using asio::ip::tcp;
asio::awaitable<void> echo_session(tcp::socket socket) {
try {
char data[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(
asio::buffer(data), asio::use_awaitable);
co_await asio::async_write(
socket, asio::buffer(data, n), asio::use_awaitable);
}
} catch (const std::exception& e) {
std::printf("Echo exception: %s\n", e.what());
}
}
asio::awaitable<void> listen(tcp::acceptor& acceptor) {
for (;;) {
tcp::socket socket = co_await acceptor.async_accept(asio::use_awaitable);
asio::co_spawn(
acceptor.get_executor(),
echo_session(std::move(socket)),
asio::detached);
}
}
void run_coroutine_server() {
asio::io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
asio::co_spawn(io, listen(acceptor), asio::detached);
io.run();
}
#endif // C++20
에러 처리: as_tuple
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
auto [ec, n] = co_await socket.async_read_some(
asio::buffer(data), asio::as_tuple(asio::use_awaitable));
if (ec) {
std::cerr << "Read error: " << ec.message() << "\n";
co_return;
}
8. 완료 핸들러 체이닝
패턴: 완료 시 다음 작업 등록
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class EchoSession : public std::enable_shared_from_this<EchoSession> {
tcp::socket socket_;
std::array<char, 1024> buffer_;
public:
EchoSession(tcp::socket socket)
: socket_(std::move(socket)) {}
void start() {
do_read();
}
private:
void do_read() {
auto self = shared_from_this();
socket_.async_read_some(
boost::asio::buffer(buffer_),
[this, self](error_code ec, size_t bytes) {
if (!ec) {
do_write(bytes); // ✅ 읽기 완료 → 쓰기 시작
}
}
);
}
void do_write(size_t bytes) {
auto self = shared_from_this();
boost::asio::async_write(
socket_,
boost::asio::buffer(buffer_, bytes),
[this, self](error_code ec, size_t) {
if (!ec) {
do_read(); // ✅ 쓰기 완료 → 다시 읽기
}
}
);
}
};
타이머 체이닝
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class PeriodicTimer {
boost::asio::steady_timer timer_;
std::function<void()> callback_;
std::chrono::milliseconds interval_;
public:
PeriodicTimer(
boost::asio::io_context& io,
std::chrono::milliseconds interval,
std::function<void()> callback
) : timer_(io), interval_(interval), callback_(callback) {}
void start() {
schedule_next();
}
private:
void schedule_next() {
timer_.expires_after(interval_);
timer_.async_wait([this](error_code ec) {
if (!ec) {
callback_();
schedule_next(); // ✅ 타이머 완료 → 다시 등록
}
});
}
};
// 사용
void use_periodic_timer() {
boost::asio::io_context io;
PeriodicTimer timer(io, std::chrono::seconds(1), {
std::cout << "Tick: " << std::time(nullptr) << "\n";
});
timer.start();
io.run();
}
9. 실전 예시
예시 1: HTTP 서버 (간단한 버전)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class SimpleHttpServer {
boost::asio::io_context& io_;
tcp::acceptor acceptor_;
public:
SimpleHttpServer(boost::asio::io_context& io, uint16_t port)
: io_(io), acceptor_(io, tcp::endpoint(tcp::v4(), port)) {
start_accept();
}
private:
void start_accept() {
acceptor_.async_accept([this](error_code ec, tcp::socket socket) {
if (!ec) {
handle_request(std::move(socket));
}
start_accept(); // 다음 연결 대기
});
}
void handle_request(tcp::socket socket) {
auto buffer = std::make_shared<boost::asio::streambuf>();
boost::asio::async_read_until(
socket,
*buffer,
"\r\n\r\n",
[this, socket = std::move(socket), buffer](error_code ec, size_t) mutable {
if (!ec) {
std::string response =
"HTTP/1.1 200 OK\r\n"
"Content-Length: 13\r\n"
"\r\n"
"Hello, World!";
boost::asio::async_write(
socket,
boost::asio::buffer(response),
{}
);
}
}
);
}
};
타임아웃 처리 패턴
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 타이머로 읽기 타임아웃 구현 (콜백 방식)
void read_with_timeout(std::shared_ptr<tcp::socket> socket,
boost::asio::mutable_buffer buffer,
std::chrono::seconds timeout,
std::function<void(error_code, size_t)> handler) {
auto timer = std::make_shared<boost::asio::steady_timer>(
socket->get_executor(), timeout);
auto buf = std::make_shared<std::vector<char>>(buffer.size());
timer->async_wait([socket, handler](error_code ec) {
if (!ec) socket->cancel(); // 타임아웃 시 읽기 취소
});
socket->async_read_some(boost::asio::buffer(*buf),
[timer, buf, handler](error_code ec, size_t n) mutable {
timer->cancel(); // 읽기 완료 시 타이머 취소
handler(ec, n);
});
}
연결 풀 (클라이언트 측)
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class ConnectionPool {
boost::asio::io_context& io_;
std::queue<std::shared_ptr<tcp::socket>> pool_;
std::string host_, port_;
size_t max_size_;
public:
void acquire(std::function<void(error_code, std::shared_ptr<tcp::socket>)> cb) {
if (!pool_.empty()) {
auto sock = std::move(pool_.front());
pool_.pop();
cb(error_code{}, std::move(sock));
return;
}
// resolver로 새 연결 생성 후 cb 호출
}
void release(std::shared_ptr<tcp::socket> sock) {
if (pool_.size() < max_size_ && sock->is_open())
pool_.push(std::move(sock));
}
};
10. 일반적인 에러와 해결법
에러 1: shared_from_this() 호출 시 bad_weak_ptr
원인: 객체가 아직 shared_ptr로 관리되지 않은 상태에서 shared_from_this() 호출.
// ❌ 잘못된 코드
new Session(socket)->start(); // shared_ptr 아님 → bad_weak_ptr
해결법:
// ✅ 올바른 코드
auto session = std::make_shared<Session>(std::move(socket));
session->start();
에러 2: run() 후 io_context 재사용
원인: run()이 반환된 io_context는 stopped 상태.
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
io.run(); // 완료 후
boost::asio::post(io, {});
io.run(); // 💥 아무것도 실행 안 됨
해결법:
// ✅ 올바른 코드
io.restart();
io.run();
에러 3: 소켓/버퍼 수명 관리
원인: 비동기 작업 완료 전에 소켓이나 버퍼가 소멸됨. 다음은 간단한 cpp 코드 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 잘못된 코드
std::array<char, 1024> buffer; // 스택
socket.async_read_some(boost::asio::buffer(buffer), {});
// 함수 종료 → buffer 소멸
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 올바른 코드
auto buffer = std::make_shared<std::array<char, 1024>>();
socket.async_read_some(
boost::asio::buffer(*buffer),
[buffer, socket = std::move(socket)](error_code ec, size_t n) mutable {});
에러 4: 멀티스레드에서 공유 변수 접근
원인: 여러 스레드가 io.run() 실행 시, 핸들러가 서로 다른 스레드에서 실행됨.
해결법: strand 사용 또는 std::mutex 사용.
// ✅ strand 사용
boost::asio::io_context::strand strand(io);
boost::asio::post(strand, [&]() { ++counter; });
에러 5: dispatch 재귀 깊이
원인: 핸들러 내부에서 dispatch로 자기 자신 호출 → 스택 오버플로우.
해결법: 재귀가 깊어지면 post 사용 (큐에 넣어 스택 해제 후 실행).
11. 모범 사례
- shared_from_this: 세션 클래스는
enable_shared_from_this상속,make_shared로 생성 - strand: 멀티스레드
run()사용 시 공유 상태는 전용strand로 보호 - 에러 코드: 모든 비동기 핸들러에서
error_code확인 - post vs dispatch: 다른 스레드 →
post, 핸들러 내부 →dispatch(재귀 주의) - work_guard: graceful shutdown에서
reset()호출 시점을 신호와 연동
12. 프로덕션 패턴
Graceful Shutdown
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::atomic<bool> g_running{true};
std::signal(SIGINT, { g_running = false; });
// do_accept 내부에서
if (!g_running) {
work.reset();
return;
}
구현 체크리스트
-
async_accept완료 핸들러에서 재등록 - 세션은
make_shared로 생성 - 멀티스레드 시 공유 자원은
strand또는 mutex로 보호 - 모든 핸들러에서
error_code확인 -
work_guard사용 시 shutdown에서reset()호출 - 소켓/버퍼 수명:
shared_ptr또는 람다 캡처로 유지
성능 비교
| 구성 | 처리량 (req/s) | CPU 사용률 | 메모리 |
|---|---|---|---|
| 단일 스레드 | 10,000 | 100% (1 core) | 낮음 |
| 멀티스레드 (4개) | 35,000 | 90% (4 cores) | 중간 |
| 멀티스레드 (8개) | 40,000 | 80% (8 cores) | 높음 |
| 결론: 스레드 수는 CPU 코어 수와 비슷하게 설정하는 것이 최적입니다. |
정리
| 항목 | 설명 |
|---|---|
| run() | 모든 작업 완료까지 블로킹 |
| work_guard | run()이 종료되지 않게 유지 |
| post | 작업을 큐에 넣어 나중에 실행 |
| dispatch | 가능하면 즉시 실행 |
| strand | 순차 실행 보장, mutex 대체 |
| 멀티스레드 | 여러 스레드가 같은 io_context 처리 |
| 코루틴 | co_await로 콜백 지옥 회피 |
| 체이닝 | 완료 핸들러에서 다음 작업 등록 |
| 핵심 원칙: |
- 서버는
async_accept재등록으로 유지 - 멀티스레드는 CPU 코어 수만큼
- 공유 자원은
strand로 보호 post는 스레드 안전,dispatch는 성능 최적화- 세션은
shared_ptr+enable_shared_from_this로 수명 관리
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 고성능 네트워크 서버, 비동기 I/O 시스템, 이벤트 기반 애플리케이션 등에서 필수입니다. 특히 수천 개의 동시 연결을 처리하는 서버에서 이벤트 루프 패턴은 필수적입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 Boost.Asio 공식 문서를 참고하세요. C++20 코루틴은 Boost.Asio C++20 Coroutines를 참고하면 좋습니다.
Q. run()과 poll()의 차이는?
A. run()은 작업이 완료될 때까지 블로킹하지만, poll()은 즉시 준비된 작업만 처리하고 반환합니다. 게임 루프처럼 매 프레임마다 이벤트를 처리해야 하는 경우 poll()을 사용합니다.
Q. 스레드를 몇 개 만들어야 하나요?
A. 일반적으로 CPU 코어 수만큼 만드는 것이 최적입니다. std::thread::hardware_concurrency()로 확인할 수 있습니다. I/O 대기가 많으면 코어 수보다 약간 더 많이 만들 수 있습니다.
Q. 코루틴 vs 콜백, 어떤 것을 써야 하나요?
A. C++20을 사용할 수 있다면 코루틴이 가독성과 유지보수에 유리합니다. 레거시 환경이거나 팀이 코루틴에 익숙하지 않다면 콜백 + shared_from_this 패턴이 안정적입니다.
한 줄 요약: run·post·work_guard·strand·코루틴으로 고성능 비동기 이벤트 루프를 구현할 수 있습니다.
다음 글: [C++ 실전 가이드 #29-3] 멀티스레드 네트워크 서버: io_context 풀과 strand
이전 글: [C++ 실전 가이드 #29-1] Asio 입문: 비동기 I/O의 시작
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Boost.Asio 입문 | io_context·async_read
- C++ 멀티스레드 네트워크 서버 완벽 가이드 | io_context 풀·strand·data race 방지
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가? 이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.