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는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
목차
- 전체 구조
- HTTP 요청 최소 파싱
- 라우팅 설계
- 미들웨어 체인
- 비동기 흐름·핸들러 호출
- 완전한 HTTP 프레임워크 예제
- 자주 발생하는 에러와 해결법
- 성능 튜닝
- 프로덕션 패턴
- 구현 체크리스트
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.Beast | HTTP/1.1, HTTP/2, WebSocket. Asio 기반. | 프로덕션, 표준 준수 필요 |
| Crow | Express 스타일, 헤더 전용. | 빠른 프로토타입, 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]