C++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]

C++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]

이 글의 핵심

C++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]에 대해 정리한 개발 블로그 글입니다. 실전에서 쓰는 HTTP 서버는 라우팅·미들웨어·비동기 I/O가 결합되어 있습니다. 그런데 "Boost.Beast나 Crow를 쓰면 되지 않나?"라고 생각할 수 있습니다. 실제로 프로덕션에서는 그런 라이브러리를 쓰는 것이… 개념과 예제 코드를 단계적으로 다루며, 실무·학습에 참고할 수 있…

들어가며: “Express나 FastAPI 같은 걸 C++로 최소만 구현해 보자”

문제 시나리오: 왜 직접 만드는가

실전에서 쓰는 HTTP 서버는 라우팅·미들웨어·비동기 I/O가 결합되어 있습니다. 그런데 “Boost.Beast나 Crow를 쓰면 되지 않나?”라고 생각할 수 있습니다. 실제로 프로덕션에서는 그런 라이브러리를 쓰는 것이 맞습니다. 하지만 다음 상황에서는 바닥부터 구현이 도움이 됩니다.

시나리오 1: 임베디드·제한된 환경
리소스가 극도로 제한된 장치에서 HTTP API를 제공해야 할 때, Beast 전체를 넣기엔 바이너리 크기가 부담됩니다. 최소 파서·라우터만 구현하면 수십 KB 수준으로 줄일 수 있습니다.

시나리오 2: 커스텀 프로토콜 혼합
HTTP와 WebSocket, 또는 자체 TCP 프로토콜을 같은 포트에서 처리해야 할 때, 내부 구조를 알면 프로토콜 디스패칭을 직접 설계할 수 있습니다.

시나리오 3: 학습·인터뷰 대비
”라우팅이 어떻게 동작하나요?”, “미들웨어 체인이 뭔가요?” 같은 질문에 직접 짜본 경험이 있으면 답이 달라집니다.

시나리오 4: 의존성 최소화
헤더만 있는 경량 라이브러리조차 부담되는 환경에서, Asio만으로 HTTP 서버를 구성하고 싶을 때.

이 글은 그 최소 버전을 C++과 Asio로 바닥부터 짜 보는 딥다이브입니다. “GET /api/hello → JSON 응답”, “요청 전/후에 로깅” 같은 동작을 자신이 설계한 구조로 구현하면서, 라우터·핸들러·미들웨어 체인의 개념을 몸으로 익힐 수 있습니다.

이 글에서 다루는 것:

  • HTTP 파싱: 요청 라인·헤더·바디의 최소 파싱 (한 줄씩 읽기 또는 간단한 상태 머신)
  • 라우팅: method + path → 핸들러 매핑 (map 또는 트리)
  • 미들웨어(요청이 핸들러에 도달하기 전·후에 실행되는 처리 단계. 로깅, 인증, 공통 헤더 등): 요청/응답을 감싸는 체인
  • 비동기 I/O: Asio로 accept → read → 파싱 → 핸들러 실행 → write

선수 지식: Asio 입문, Redis 클론에서 세션·한 줄 읽기 패턴을 해봤으면 좋습니다.

요구 환경: Boost.Asio 또는 standalone Asio. C++14 이상. vcpkg install boost-asio 또는 시스템 패키지로 설치.

개념을 잡는 비유

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


목차

  1. 전체 구조
  2. HTTP 요청 최소 파싱
  3. 라우팅 설계
  4. 미들웨어 체인
  5. 비동기 흐름·핸들러 호출
  6. 완전한 HTTP 프레임워크 예제
  7. 자주 발생하는 에러와 해결법
  8. 성능 튜닝
  9. 프로덕션 패턴
  10. 구현 체크리스트

1. 전체 구조

아키텍처 개요

  • io_context + tcp::acceptor로 연결 수락. 연결당 세션에서 async_read_until 또는 async_read로 요청을 모읍니다.
  • “\r\n\r\n” 까지 읽으면 헤더 끝. Content-Length가 있으면 그만큼 바디를 추가로 읽습니다.
  • 파싱한 요청(method, path, headers, body)라우터에 넘겨 핸들러를 찾고, 미들웨어 체인을 통과시킨 뒤 핸들러를 실행합니다. 핸들러가 응답(상태 코드, 헤더, 바디) 을 만들면 async_write로 클라이언트에 보냅니다.

흐름 다이어그램

flowchart TB
    subgraph Client["클라이언트"]
        C1[HTTP 요청]
    end

    subgraph Server["서버"]
        A[Acceptor] --> S[Session]
        S --> P[HTTP 파서]
        P --> R[라우터]
        R --> M[미들웨어 체인]
        M --> H[핸들러]
        H --> W[async_write]
    end

    C1 --> A
    W --> C2[HTTP 응답]
    C2 --> Client

시퀀스 다이어그램

sequenceDiagram
    participant C as Client
    participant A as Acceptor
    participant S as Session
    participant P as Parser
    participant R as Router
    participant M as Middleware
    participant H as Handler

    C->>A: TCP 연결
    A->>S: 새 세션 생성
    C->>S: HTTP 요청 (헤더+바디)
    S->>P: raw 데이터
    P->>P: 파싱 (method, path, headers, body)
    P->>R: Request 객체
    R->>R: method+path로 핸들러 검색
    R->>M: Request + next
    M->>M: 전처리 (로깅, 인증 등)
    M->>H: Request
    H->>H: 비즈니스 로직
    H-->>M: Response
    M->>M: 후처리 (헤더 추가 등)
    M-->>R: Response
    R-->>S: Response
    S->>C: HTTP 응답 전송

2. HTTP 요청 최소 파싱

요청 라인

  • 첫 줄: METHOD /path HTTP/1.1 형태. 공백으로 split해 method, path를 저장합니다.
  • query string이 있으면 path에서 ? 뒤를 분리해 key=value로 파싱할 수 있습니다.

헤더

  • 한 줄씩 Key: value 형태. 빈 줄(\r\n\r\n)이 나올 때까지 읽어 map<string, string> 에 넣습니다.
  • Content-Length가 있으면 그 값만큼 바디를 추가로 async_read로 읽습니다.

바디

  • 읽은 바디는 string 또는 vector 로 보관해 핸들러에 넘깁니다. JSON 등은 나중에 라이브러리(nlohmann/json 등)로 파싱할 수 있습니다.

HTTP 파싱 상태 머신 (개념)

stateDiagram-v2
    [*] --> RequestLine: 연결 수락
    RequestLine --> Headers: \r\n
    Headers --> Headers: Key: value\r\n
    Headers --> Body: \r\n\r\n (Content-Length 있음)
    Headers --> Complete: \r\n\r\n (Content-Length 없음)
    Body --> Complete: Content-Length 바이트 읽음
    Complete --> [*]: 핸들러 호출

파싱 예제 코드

#include <string>
#include <sstream>
#include <map>
#include <algorithm>
#include <cctype>

struct Request {
    std::string method;
    std::string path;
    std::string query_string;
    std::map<std::string, std::string> headers;
    std::string body;
};

// 요청 라인 파싱: "GET /api/hello?name=world HTTP/1.1"
void parse_request_line(const std::string& line, Request& req) {
    std::istringstream iss(line);
    iss >> req.method >> req.path;
    // query string 분리
    auto qpos = req.path.find('?');
    if (qpos != std::string::npos) {
        req.query_string = req.path.substr(qpos + 1);
        req.path = req.path.substr(0, qpos);
    }
}

// 헤더 파싱: "Content-Type: application/json"
void parse_header(const std::string& line, Request& req) {
    auto colon = line.find(':');
    if (colon != std::string::npos) {
        std::string key = line.substr(0, colon);
        std::string value = line.substr(colon + 1);
        // 앞뒤 공백 제거
        while (!key.empty() && std::isspace(key.back())) key.pop_back();
        while (!value.empty() && std::isspace(value.front())) value.erase(0, 1);
        req.headers[key] = value;
    }
}

3. 라우팅 설계

단순 매핑

  • using Handler = std::function<Response(const Request&)>; 로 핸들러 타입을 정의합니다.
  • map<pair<string, string>, Handler> 또는 method → (path → Handler) 구조로 등록합니다.

라우터 구현 예제

#include <functional>
#include <unordered_map>

struct Response {
    int status = 200;
    std::string content_type = "text/plain";
    std::string body;
};

class Router {
public:
    using Handler = std::function<Response(const Request&)>;

    void get(const std::string& path, Handler h) {
        routes_[{"GET", path}] = std::move(h);
    }

    void post(const std::string& path, Handler h) {
        routes_[{"POST", path}] = std::move(h);
    }

    Response dispatch(const Request& req) {
        auto key = std::make_pair(req.method, req.path);
        auto it = routes_.find(key);
        if (it != routes_.end()) {
            return it->second(req);
        }
        return Response{404, "text/plain", "Not Found"};
    }

private:
    std::unordered_map<std::pair<std::string, std::string>, Handler> routes_;
};

// 사용 예
Router router;
router.get("/api/hello",  {
    return Response{200, "application/json", "{\"msg\":\"hello\"}"};
});

경로 파라미터 (선택)

  • /user/:id 같은 패턴을 지원하려면 path를 토큰으로 쪼개고, 일치하는 패턴을 찾아 :id 부분을 Request에 넣어 주는 식으로 확장할 수 있습니다. 최소 버전에서는 고정 path만 해도 됩니다.
// /user/123 -> params["id"] = "123"
bool match_path(const std::string& pattern, const std::string& path,
                std::map<std::string, std::string>& params) {
    // 간단한 구현: :id 같은 토큰을 정규식 또는 수동 파싱으로 매칭
    // 생략 (고정 path만 사용 시 불필요)
    return pattern == path;
}

4. 미들웨어 체인

개념

  • 미들웨어는 (Request → 다음 미들웨어/핸들러)를 인자로 받아, 전처리(로깅, 인증) 후 다음을 호출하고, 반환된 Response를 후처리(헤더 추가 등)해 반환합니다.
  • 체인: 라우터가 최종 핸들러를 감싸서, “로깅 미들웨어 → 공통 헤더 미들웨어 → 라우트 핸들러” 순으로 호출되게 합니다.

미들웨어 타입 정의

using Next = std::function<Response(const Request&)>;
using Middleware = std::function<Response(const Request&, const Next&)>;

로깅 미들웨어 예시

next 는 “다음 미들웨어 또는 최종 핸들러”를 나타내는 std::function 입니다. logging_mw 는 요청 로그를 남긴 뒤 next(req) 로 다음으로 넘기고, 반환된 Response 로 응답 로그를 남긴 다음 그대로 반환합니다.

Response logging_mw(const Request& req, const Next& next) {
    std::cout << "request: " << req.method << " " << req.path << "\n";
    auto res = next(req);
    std::cout << "response: " << res.status << "\n";
    return res;
}

공통 헤더 미들웨어

Response cors_mw(const Request& req, const Next& next) {
    auto res = next(req);
    // CORS 헤더 추가 (실제로는 Response 구조체에 headers 맵 필요)
    return res;
}

인증 미들웨어 (401 반환 예시)

Response auth_mw(const Request& req, const Next& next) {
    auto it = req.headers.find("Authorization");
    if (it == req.headers.end() || it->second != "Bearer secret-token") {
        return Response{401, "text/plain", "Unauthorized"};
    }
    return next(req);
}

미들웨어 체인 흐름

flowchart LR
    R[Request] --> L[logging_mw]
    L --> A[auth_mw]
    A --> H[route_handler]
    H --> A
    A --> L
    L --> Res[Response]

체인 구성

라우터에서 핸들러를 찾은 뒤, 그 핸들러를 next로 해서 logging_mw(req, next) 처럼 감싸 호출하면 됩니다.

// 체인: logging -> auth -> route_handler
Response handle_request(const Request& req) {
    auto route_handler = [&]() { return router.dispatch(req); };
    auto with_auth = [&](const Request& r) { return auth_mw(r, route_handler); };
    return logging_mw(req, with_auth);
}

5. 비동기 흐름·핸들러 호출

  • 세션에서 “헤더+바디”가 다 모이면 Request 구조체를 채우고, 라우터에 넘겨 Response를 받습니다. 이 부분은 동기로 두어도 됩니다(싱글 스레드에서 빠르게 처리).
  • Response를 HTTP 형식(상태 라인, 헤더, 바디)으로 직렬화한 뒤 async_write로 전송합니다. 전송이 끝나면 같은 세션에서 다음 요청을 다시 읽거나(HTTP/1.1 keep-alive) 연결을 닫습니다.
  • 멀티 스레드로 확장할 때는 라우터·미들웨어·핸들러가 스레드 안전해야 하거나, 연결당 Strand로 직렬화하면 됩니다. 고성능 네트워크 가이드 #3 참고.

Response 직렬화

std::string to_http_response(const Response& res) {
    std::string status_text;
    switch (res.status) {
        case 200: status_text = "OK"; break;
        case 404: status_text = "Not Found"; break;
        case 500: status_text = "Internal Server Error"; break;
        default: status_text = "Unknown"; break;
    }
    std::string out = "HTTP/1.1 " + std::to_string(res.status) + " " + status_text + "\r\n";
    out += "Content-Type: " + res.content_type + "\r\n";
    out += "Content-Length: " + std::to_string(res.body.size()) + "\r\n";
    out += "Connection: keep-alive\r\n";
    out += "\r\n";
    out += res.body;
    return out;
}

6. 완전한 HTTP 프레임워크 예제

최소 동작 서버 (Hello World)

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

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

class HttpSession : public std::enable_shared_from_this<HttpSession> {
public:
    HttpSession(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_, "\r\n\r\n",
            [this, self](boost::system::error_code ec, std::size_t) {
                if (ec) return;
                std::istream is(&buffer_);
                std::string line;
                std::getline(is, line);  // GET / HTTP/1.1
                // 최소 파싱: path만 확인
                std::string path = "/";
                if (line.size() > 4) {
                    auto start = line.find(' ');
                    auto end = line.find(' ', start + 1);
                    if (start != std::string::npos && end != std::string::npos)
                        path = line.substr(start + 1, end - start - 1);
                }
                std::string body = "Hello, World!";
                std::string response = "HTTP/1.1 200 OK\r\n";
                response += "Content-Type: text/plain\r\n";
                response += "Content-Length: " + std::to_string(body.size()) + "\r\n\r\n";
                response += body;
                do_write(response);
            });
    }

    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) {
                    // keep-alive: 다음 요청 대기 (간단히 연결 종료)
                    boost::system::error_code ign;
                    socket_.shutdown(tcp::socket::shutdown_both, ign);
                }
            });
    }

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

void do_accept(tcp::acceptor& acceptor) {
    acceptor.async_accept([&acceptor](boost::system::error_code ec, tcp::socket socket) {
        if (!ec) {
            std::make_shared<HttpSession>(std::move(socket))->start();
        }
        do_accept(acceptor);
    });
}

int main() {
    asio::io_context io;
    tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), 8080));
    do_accept(acceptor);
    std::cout << "HTTP server on http://localhost:8080\n";
    io.run();
}

라우팅·미들웨어 통합 예제

// main에서 Router와 미들웨어를 사용하는 세션
class FullHttpSession : public std::enable_shared_from_this<FullHttpSession> {
public:
    FullHttpSession(tcp::socket socket, Router& router)
        : socket_(std::move(socket)), router_(router) {}

    void start() { do_read(); }

private:
    void do_read() {
        auto self(shared_from_this());
        asio::async_read_until(socket_, buffer_, "\r\n\r\n",
            [this, self](boost::system::error_code ec, std::size_t) {
                if (ec) return;
                Request req = parse_full_request();
                // 미들웨어 체인 + 라우터
                auto route_handler = [this](const Request& r) { return router_.dispatch(r); };
                Response res = logging_mw(req, route_handler);
                std::string http_res = to_http_response(res);
                do_write(http_res);
            });
    }

    Request parse_full_request() {
        Request req;
        std::istream is(&buffer_);
        std::string line;
        std::getline(is, line);
        parse_request_line(line, req);
        while (std::getline(is, line) && line != "\r" && !line.empty()) {
            parse_header(line, req);
        }
        // Content-Length 있으면 바디 추가 읽기 (생략)
        return req;
    }

    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) {
                    boost::system::error_code ign;
                    socket_.shutdown(tcp::socket::shutdown_both, ign);
                }
            });
    }

    tcp::socket socket_;
    asio::streambuf buffer_;
    Router& router_;
};

빌드 및 실행

# vcpkg로 Boost 설치
vcpkg install boost-asio

# 빌드 (예: CMake)
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=[vcpkg]/scripts/buildsystems/vcpkg.cmake
make

# 실행
./http_server
# curl http://localhost:8080/api/hello

JSON API 예제 (nlohmann/json 연동)

#include <nlohmann/json.hpp>
using json = nlohmann::json;

router.post("/api/users",  {
    try {
        auto j = json::parse(req.body);
        std::string name = j["name"];
        // DB 저장 등...
        return Response{201, "application/json",
            json{{"id", 1}, {"name", name}}.dump()};
    } catch (const json::exception& e) {
        return Response{400, "application/json",
            json{{"error", e.what()}}.dump()};
    }
});

router.get("/api/users/:id",  {
    // params에서 id 추출 (경로 파라미터 구현 시)
    return Response{200, "application/json",
        json{{"id", 1}, {"name", "Alice"}}.dump()};
});

테스트 방법

# GET 요청
curl -v http://localhost:8080/api/hello

# POST + JSON 바디
curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Bob"}'

# 헬스 체크
curl http://localhost:8080/health

7. 자주 발생하는 에러와 해결법

문제 1: “Connection reset by peer” 또는 읽기 중 끊김

원인: 클라이언트가 요청을 보내다 중간에 연결을 끊거나, keep-alive 타임아웃.

해결법:

// ✅ 에러 시 조용히 세션 종료
asio::async_read_until(socket_, buffer_, "\r\n\r\n",
    [this, self](boost::system::error_code ec, std::size_t n) {
        if (ec) {
            if (ec == asio::error::eof || ec == asio::error::connection_reset)
                return;  // 정상적인 끊김
            std::cerr << "Read error: " << ec.message() << "\n";
            return;
        }
        // 정상 처리
    });

문제 2: Content-Length 없이 바디가 오는 요청

원인: HTTP/1.1 chunked encoding 또는 Content-Length 누락.

해결법:

// ✅ Content-Length 없으면 바디 읽기 스킵 (최소 구현)
auto it = req.headers.find("Content-Length");
if (it != req.headers.end()) {
    size_t len = std::stoull(it->second);
    if (buffer_.size() < len) {
        // 추가 async_read로 len만큼 읽기
        asio::async_read(socket_, buffer_, asio::transfer_exactly(len - buffer_.size()),
            [this, len, ...](boost::system::error_code ec, std::size_t) { /* ... */ });
        return;
    }
}
// 바디 이미 있음 → 파싱 계속

문제 3: 파싱 시 “\r\n” vs “\n” 혼동

원인: HTTP는 \r\n(CRLF)를 줄 구분자로 사용. std::getline\n만 제거.

해결법:

// ✅ \r 제거
std::string line;
std::getline(is, line);
if (!line.empty() && line.back() == '\r')
    line.pop_back();

문제 4: 핸들러에서 shared_from_this 없이 람다 캡처

원인: 비동기 콜백 실행 시 세션이 이미 소멸됨.

해결법:

// ❌ 잘못된 예
void do_read() {
    asio::async_read_until(socket_, buffer_, "\r\n\r\n",
        [this](boost::system::error_code ec, std::size_t n) {
            // this가 dangling일 수 있음!
        });
}

// ✅ 올바른 예
void do_read() {
    auto self(shared_from_this());
    asio::async_read_until(socket_, buffer_, "\r\n\r\n",
        [this, self](boost::system::error_code ec, std::size_t n) {
            if (!ec) process_request();
        });
}

문제 5: 대용량 요청으로 메모리 폭발

원인: Content-Length가 1GB인 요청을 그대로 읽으려 함.

해결법:

const size_t MAX_BODY_SIZE = 1024 * 1024;  // 1MB 제한

if (len > MAX_BODY_SIZE) {
    return Response{413, "text/plain", "Payload Too Large"};
}

문제 6: 경로 중복 등록

원인: 같은 method+path에 여러 핸들러 등록 시 나중 것이 덮어씀.

해결법:

// ✅ 등록 시 로그 또는 assert
void get(const std::string& path, Handler h) {
    auto key = std::make_pair("GET", path);
    if (routes_.count(key))
        std::cerr << "Warning: route GET " << path << " overwritten\n";
    routes_[key] = std::move(h);
}

문제 7: Keep-Alive 시 버퍼에 이전 요청 잔여 데이터

원인: HTTP/1.1 keep-alive에서 한 연결에 여러 요청이 오면, streambuf에 이전 요청 데이터가 남아 있을 수 있음.

해결법:

// ✅ 처리 완료 후 consume
void on_request_handled() {
    buffer_.consume(buffer_.size());  // 처리한 데이터 제거
    do_read();  // 다음 요청 대기
}

문제 8: 스레드 안전성

원인: 멀티 스레드에서 io.run() 호출 시, 라우터·미들웨어가 동시에 접근되면 데이터 레이스.

해결법:

// ✅ 라우터는 초기화 시점에만 수정, 이후 읽기만 → 스레드 안전
// ✅ 또는 strand로 핸들러 직렬화
asio::io_context::strand strand(io);
asio::post(strand, [&]() {
    Response res = router.dispatch(req);
    do_write(to_http_response(res));
});

8. 성능 튜닝

병목 지점 분석

구간설명최적화
HTTP 파싱문자열 split, map 삽입파싱 결과 캐시, 작은 버퍼 풀
라우팅map 조회해시맵, 경로 트리(라드릭스)
핸들러비즈니스 로직비동기 DB/IO, 스레드 풀
응답 직렬화string 연결ostringstream, 재사용 버퍼

버퍼 재사용

// ✅ streambuf 재사용 (세션 멤버로 유지)
asio::streambuf buffer_;  // 매 요청마다 consume만 호출

// consume: 이미 처리한 데이터 제거
buffer_.consume(buffer_.size());  // 다음 요청 전

멀티 스레드 run()

// ✅ CPU 코어 수만큼 스레드에서 io.run()
std::vector<std::thread> threads;
unsigned n = std::thread::hardware_concurrency();
for (unsigned i = 0; i < n; ++i) {
    threads.emplace_back([&io]() { io.run(); });
}
for (auto& t : threads) t.join();

벤치마크 시나리오 (참고)

  • 동시 연결: 1,000개
  • 요청: GET /api/hello (JSON 20바이트 응답)
  • 환경: localhost, 1 스레드
구현req/s (대략)
최소 파서 + map 라우팅~15,000
Beast 기반~25,000
nginx (참고)~50,000+

결론: 직접 구현한 최소 프레임워크도 수천 RPS는 가능. 프로덕션에서는 Beast 등 검증된 라이브러리 사용 권장.


9. 프로덕션 패턴

연결 제한

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<HttpSession>(std::move(socket))->start();
        do_accept(acceptor);
    });
}

// HttpSession 소멸자에서 connection_count--

요청 타임아웃

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_, "\r\n\r\n", ...);
}

로깅 (구조화)

// spdlog 등 사용
spdlog::info("{} {} {} - {}", req.method, req.path, res.status,
             socket_.remote_endpoint().address().to_string());

Graceful Shutdown

asio::signal_set signals(io, SIGINT, SIGTERM);
signals.async_wait([&](boost::system::error_code, int) {
    acceptor.close();
    io.stop();
});

헬스 체크 엔드포인트

router.get("/health",  {
    return Response{200, "application/json", "{\"status\":\"ok\"}"};
});

대안 라이브러리 비교

라이브러리특징사용 시기
Boost.BeastHTTP/1.1, HTTP/2, WebSocket. Asio 기반.프로덕션, 표준 준수 필요
CrowExpress 스타일, 헤더 전용.빠른 프로토타입, REST API
Drogon비동기, ORM, 최신 C++17.풀스택 C++ 웹앱
직접 구현최소 의존성, 학습용.임베디드, 학습, 커스텀 확장

모니터링 포인트

// 메트릭 수집 (Prometheus 등 연동 시)
void record_request(const std::string& path, int status, std::chrono::microseconds latency) {
    // request_count{path="/api/hello",status="200"}++
    // request_latency_seconds{path="/api/hello"} observe(latency)
}

10. 구현 체크리스트

기본 기능

  • HTTP 요청 라인 파싱 (method, path, query)
  • 헤더 파싱 (map)
  • Content-Length 기반 바디 읽기
  • Response 직렬화 (상태 라인, 헤더, 바디)
  • method + path 라우팅

미들웨어

  • 로깅 미들웨어
  • 공통 헤더 (CORS 등) 미들웨어
  • 인증 미들웨어 (선택)

에러 처리

  • 연결 끊김 처리
  • 404 Not Found
  • 413 Payload Too Large (바디 크기 제한)
  • 500 Internal Server Error (예외 처리)

프로덕션

  • 연결 수 제한
  • 요청 타임아웃
  • Graceful shutdown
  • 구조화 로깅

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

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

  • C++ REST API 서버 완벽 가이드 | Beast 라우팅·JSON·미들웨어 [#31-2]
  • C++ HTTP 기초 완벽 가이드 | 요청/응답 파싱·헤더·청크 인코딩·Beast 실전 [#30-1]
  • C++ Redis 클론 | Modern C++ 인메모리 KV 스토어 [#48-1]

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

C++ HTTP 프레임워크, 웹 서버, Asio HTTP, C++ 라우팅, 미들웨어 체인 등으로 검색하시면 이 글이 도움이 됩니다.


정리

항목내용
구조Acceptor → Session → Parser → Router → Middleware → Handler → write
파싱요청 라인 split, 헤더 map, Content-Length로 바디
라우팅map<pair<method, path>, Handler>
미들웨어(Request, Next) → 전처리 → next() → 후처리 → Response
에러error_code 확인, shared_from_this, 바디 크기 제한
프로덕션연결 제한, 타임아웃, Graceful shutdown

이렇게 라우팅·미들웨어·비동기 I/O가 결합된 최소 HTTP 프레임워크를 만들면, 상용 프레임워크가 어떤 조각으로 이루어져 있는지 이해하는 데 도움이 됩니다. 실전에서는 Boost.Beast 같은 검증된 라이브러리를 사용하고, 이 글의 내용은 설계 이해와 커스텀 확장에 활용하세요.


자주 묻는 질문 (FAQ)

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

A. 라우팅, 미들웨어, 비동기 I/O 처리기를 C++과 Asio로 직접 짜보는 튜토리얼입니다. Express·FastAPI 같은 최소 HTTP 서버를 바닥부터 구현합니다. 실무에서는 Beast/Crow를 쓰되, 임베디드·의존성 최소화·학습 목적으로 이 구조를 참고할 수 있습니다.

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

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

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. Boost.Beast 문서에서 HTTP/WebSocket 구현을 확인할 수 있습니다.

Q. Beast와 직접 구현의 차이는?

A. Beast는 HTTP/1.1, HTTP/2, WebSocket을 표준에 맞게 완전히 구현합니다. 직접 구현은 “최소 동작” 수준이므로 프로덕션에는 Beast 권장. 직접 구현은 학습·제한 환경용입니다.

Q. 왜 이 방법을 써야 하나요? (대안 비교)

A.

접근장점단점

관련 글

  • C++ Redis 클론 | Modern C++ 인메모리 KV 스토어 [#48-1]
  • C++ 게임 엔진 기초 | 게임 루프·ECS·씬 그래프·입력 처리 완전 가이드
  • C++ ECS 패턴 완벽 가이드 | Entity·Component·System·쿼리·컴포넌트 스토리지 실전
  • C++ 커스텀 메모리 할당자(Memory Pool) 제작기 [#48-3]
  • C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]