본문으로 건너뛰기
Previous
Next
C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post

C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post

C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post

이 글의 핵심

Asio 이벤트 루프의 모든 것: run/run_one/poll 차이, post/dispatch 작업 큐, work_guard로 서버 유지, strand 동기화, C++20 코루틴, 일반적인 에러와 프로덕션 패턴까지 실전 코드로.

들어가며: “서버가 바로 종료돼요. run()이 끝나지 않게 하려면?”

문제 상황 1: 서버가 즉시 종료됨

// ❌ 문제: 서버가 바로 종료됨
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: 핸들러 실행 순서 혼란

postdispatch의 차이를 모르고 사용하면, 핸들러가 예상과 다른 순서로 실행될 수 있습니다. dispatch는 현재 핸들러 내부에서 즉시 실행되므로 재귀 깊이에 주의해야 합니다. 시나리오 5: run() 호출 후 io_context 재사용

io.run()이 반환된 io_context는 “stopped” 상태입니다. io.restart()를 호출하지 않고 다시 run()을 호출하면 아무 작업도 실행되지 않습니다. 해결책:

  1. work_guard: “아직 할 일이 있다”고 표시
  2. 완료 핸들러에서 재등록: async_accept 완료 시 다시 async_accept 등록
  3. 멀티스레드 run(): 여러 스레드가 동시에 이벤트 처리 목표:
  • run() / run_one() / poll() 동작 이해
  • post / dispatch로 작업 큐 관리
  • work_guard로 서버 유지
  • 멀티스레드 이벤트 루프 구현
  • 완료 핸들러 체이닝 패턴 요구 환경: Boost.Asio 1.70 이상 이 글을 읽으면:
  • 이벤트 루프의 동작 원리를 이해할 수 있습니다.
  • 서버가 종료되지 않게 유지할 수 있습니다.
  • 멀티스레드로 성능을 향상시킬 수 있습니다.

개념을 잡는 비유

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

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

1. 이벤트 루프 동작 원리

이벤트 루프란?

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

이벤트 루프의 핵심:

  1. 등록된 비동기 작업을 확인
  2. I/O 이벤트 발생 대기 (epoll/kqueue/IOCP)
  3. 이벤트 발생 시 완료 핸들러 실행
  4. 1번으로 돌아가서 반복
  5. 더 이상 작업이 없으면 종료

시퀀스 다이어그램: run() 동작

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 비교

세 가지 실행 방식

#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로 서버 유지

문제: 서버가 바로 종료됨

// ❌ 잘못된 서버 코드
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 사용

#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: 완료 핸들러에서 재등록 (권장)

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 멀티스레드

// 단일 스레드: 한 번에 하나씩 처리
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();
    }
}

스레드 풀 구현

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));
}

동기화 주의사항

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 세션

#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 서버 (코루틴)

#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

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. 완료 핸들러 체이닝

패턴: 완료 시 다음 작업 등록

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();  // ✅ 쓰기 완료 → 다시 읽기
                }
            }
        );
    }
};

타이머 체이닝

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 서버 (간단한 버전)

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),
                         {}
                    );
                }
            }
        );
    }
};

타임아웃 처리 패턴

// 타이머로 읽기 타임아웃 구현 (콜백 방식)
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);
        });
}

연결 풀 (클라이언트 측)

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 상태.

// ❌ 잘못된 코드
io.run();  // 완료 후
boost::asio::post(io, {});
io.run();  // 💥 아무것도 실행 안 됨

해결법:

// ✅ 올바른 코드
io.restart();
io.run();

에러 3: 소켓/버퍼 수명 관리

원인: 비동기 작업 완료 전에 소켓이나 버퍼가 소멸됨.

// ❌ 잘못된 코드
std::array<char, 1024> buffer;  // 스택
socket.async_read_some(boost::asio::buffer(buffer), {});
// 함수 종료 → buffer 소멸

해결법:

// ✅ 올바른 코드
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. 모범 사례

  1. shared_from_this: 세션 클래스는 enable_shared_from_this 상속, make_shared로 생성
  2. strand: 멀티스레드 run() 사용 시 공유 상태는 전용 strand로 보호
  3. 에러 코드: 모든 비동기 핸들러에서 error_code 확인
  4. post vs dispatch: 다른 스레드 → post, 핸들러 내부 → dispatch (재귀 주의)
  5. work_guard: graceful shutdown에서 reset() 호출 시점을 신호와 연동

12. 프로덕션 패턴

Graceful Shutdown

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,000100% (1 core)낮음
멀티스레드 (4개)35,00090% (4 cores)중간
멀티스레드 (8개)40,00080% (8 cores)높음
결론: 스레드 수는 CPU 코어 수와 비슷하게 설정하는 것이 최적입니다.

정리

항목설명
run()모든 작업 완료까지 블로킹
work_guardrun()이 종료되지 않게 유지
post작업을 큐에 넣어 나중에 실행
dispatch가능하면 즉시 실행
strand순차 실행 보장, mutex 대체
멀티스레드여러 스레드가 같은 io_context 처리
코루틴co_await로 콜백 지옥 회피
체이닝완료 핸들러에서 다음 작업 등록
핵심 원칙:
  1. 서버는 async_accept 재등록으로 유지
  2. 멀티스레드는 CPU 코어 수만큼
  3. 공유 자원은 strand로 보호
  4. post는 스레드 안전, dispatch는 성능 최적화
  5. 세션은 shared_ptr + enable_shared_from_this로 수명 관리

자주 묻는 질문 (FAQ)

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

A. 고성능 네트워크 서버, 비동기 I/O 시스템, 이벤트 기반 애플리케이션 등에서 필수입니다. 특히 수천 개의 동시 연결을 처리하는 서버에서 이벤트 루프 패턴은 필수적입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

A. cppreferenceBoost.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++, Asio, 이벤트루프, 비동기, run, post, work_guard, strand, 코루틴, 멀티스레드 등으로 검색하시면 이 글이 도움이 됩니다.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 비동기 I/O 이벤트 루프 완벽 가이드 | Asio run·post」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.