본문으로 건너뛰기
Previous
Next
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는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.

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

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++ 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++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  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 순서를 권장합니다.