C++ REST API 서버 만들기 | 라우팅·미들웨어·인증·Swagger 문서화 [#50-2]
이 글의 핵심
C++ REST API 서버 만들기에 대한 실전 가이드입니다. 라우팅·미들웨어·인증·Swagger 문서화 [#50-2] 등을 예제와 함께 상세히 설명합니다.
들어가며: “Express.js처럼 쉬운 C++ REST API 서버를 만들고 싶어요”
REST API 서버의 핵심
Node.js Express나 Python Flask처럼 간결한 라우팅, 미들웨어 체인, 자동 문서화를 C++로 구현하면 고성능과 생산성을 동시에 얻을 수 있습니다.
목표:
- Express 스타일 라우팅 (GET/POST/PUT/DELETE)
- 미들웨어 체인 (로깅, 인증, CORS 등)
- JSON 요청/응답 자동 파싱
- Swagger/OpenAPI 문서 자동 생성
- 요청 검증 및 에러 핸들링
요구 환경: C++17 이상, Boost.Beast, nlohmann/json
이 글을 읽으면:
- Express 스타일 REST API 서버를 만들 수 있습니다.
- 미들웨어 패턴을 구현할 수 있습니다.
- JWT 인증을 통합할 수 있습니다.
- API 문서를 자동 생성할 수 있습니다.
문제 시나리오: C++ REST API 서버가 필요한 상황
시나리오 1: 고성능 백엔드 API
Node.js나 Python은 편하지만, 초당 수만 요청을 처리해야 하는 게임 서버, 금융 거래 API, 실시간 데이터 피드에서는 지연 시간이 병목입니다. C++로 REST API를 직접 구현하면 마이크로초 단위의 응답 지연을 달성할 수 있습니다.
시나리오 2: 기존 C++ 시스템에 HTTP API 추가
이미 C++로 작성된 게임 엔진, 트레이딩 시스템, IoT 게이트웨이가 있는데 웹/모바일 클라이언트가 연동해야 합니다. 별도 Node.js 서버를 두면 프로세스 간 통신 오버헤드가 생깁니다. C++ 프로세스 내에서 직접 REST API를 제공하면 통합이 단순해집니다.
시나리오 3: 리소스 제약 환경
임베디드·엣지 서버에서 메모리 제한이 엄격합니다. Node.js 런타임은 수십 MB 이상을 차지합니다. C++로 직접 구현한 경량 REST 서버는 메가바이트 단위로 동작하며, 의존성을 최소화할 수 있습니다.
시나리오 4: 라우팅·미들웨어 패턴 학습
Express의 app.get(), app.use(), next() 같은 패턴이 어떻게 동작하는지 이해하려면 직접 구현해 보는 것이 가장 효과적입니다. C++로 구현하면 메모리와 런타임 동작을 완전히 제어할 수 있습니다.
시나리오 5: Swagger 문서 자동 생성
API가 많아질수록 수동 문서가 뒤쳐집니다. 코드와 동기화된 OpenAPI 문서를 자동 생성하면, 프론트엔드·모바일 팀과 협업이 수월해집니다.
시나리오 6: “브라우저에서 CORS 에러가 발생해요”
프론트엔드(React, Vue)에서 http://localhost:3000으로 개발 중인데, API 서버가 http://localhost:8080이면 브라우저가 cross-origin 요청을 차단합니다. Access to fetch has been blocked by CORS policy 에러가 발생합니다. 해결: CORS 미들웨어에서 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers를 설정하고, OPTIONS preflight 요청을 204로 즉시 응답합니다.
시나리오 7: “POST 요청 body가 비어 있어요”
req.json_body()를 호출했는데 빈 객체 {}가 반환됩니다. 원인: Content-Type: application/json 헤더 없이 전송했거나, body 파싱 전에 이미 응답을 보냈을 수 있습니다. 해결: Content-Type 검사 후 파싱하고, 파싱 실패 시 400 Bad Request를 반환합니다.
시나리오 8: “인증 없이 보호된 API에 접근해도 200이 나와요”
/users 엔드포인트에 인증 미들웨어를 등록했는데, Authorization 헤더 없이 요청해도 200 OK가 반환됩니다. 원인: 미들웨어 등록 순서 오류 또는 라우트별 미들웨어가 전역 미들웨어보다 나중에 적용되지 않음. 해결: 인증 미들웨어를 해당 라우트에 명시적으로 바인딩하고, 인증 실패 시 next()를 호출하지 않고 즉시 return합니다.
개념을 잡는 비유
소켓과 비동기 I/O는 우편함 주소와 배달 경로로 이해하면 편합니다. 주소(IP·포트)만 맞으면 데이터가 들어오고, Asio는 한 우체국에서 여러 배달부(스레드·핸들러)가 일을 나누는 구조로 보시면 됩니다.
목차
- 라우터 설계
- 미들웨어 체인
- 요청/응답 처리
- 인증 미들웨어
- 검증 및 에러 처리
- Swagger 문서 생성
- 완전한 REST API 예시
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 성능 최적화 팁
- 프로덕션 패턴
1. 라우터 설계
아키텍처 다이어그램
flowchart TB
subgraph Client["클라이언트"]
C1[HTTP 요청]
end
subgraph Server["서버"]
subgraph Middleware["미들웨어 체인"]
M1[로깅]
M2[CORS]
M3[인증]
M4[에러 핸들러]
end
subgraph Router["라우터"]
R1["GET /users"]
R2["POST /users"]
R3["GET /users/:id"]
end
M1 --> M2 --> M3 --> M4 --> Router
end
C1 --> Client
Client --> Server
Express 스타일 API
// 사용 예시
RestServer server;
server.get("/users", {
res.json({{"users", get_all_users()}});
});
server.post("/users", {
auto user = req.body<User>();
auto id = create_user(user);
res.status(201).json({{"id", id}});
});
server.get("/users/:id", {
auto id = req.params("id");
auto user = get_user(id);
if (!user) {
res.status(404).json({{"error", "User not found"}});
return;
}
res.json(*user);
});
server.listen(8080);
Router 구현
class Router {
public:
using Handler = std::function<void(const Request&, Response&)>;
using Middleware = std::function<void(const Request&, Response&, std::function<void()>)>;
private:
struct Route {
std::string method;
std::regex path_regex;
std::vector<std::string> param_names;
Handler handler;
std::vector<Middleware> middlewares;
};
std::vector<Route> routes_;
std::vector<Middleware> global_middlewares_;
public:
void get(const std::string& path, Handler handler) {
add_route("GET", path, handler);
}
void post(const std::string& path, Handler handler) {
add_route("POST", path, handler);
}
void put(const std::string& path, Handler handler) {
add_route("PUT", path, handler);
}
void del(const std::string& path, Handler handler) {
add_route("DELETE", path, handler);
}
void use(Middleware middleware) {
global_middlewares_.push_back(middleware);
}
private:
void add_route(const std::string& method, const std::string& path, Handler handler) {
Route route;
route.method = method;
route.handler = handler;
// 경로 파라미터 파싱: /users/:id -> /users/([^/]+)
std::string regex_path = path;
std::regex param_regex(R"(:([a-zA-Z_][a-zA-Z0-9_]*))");
std::smatch match;
while (std::regex_search(regex_path, match, param_regex)) {
route.param_names.push_back(match[1]);
regex_path = match.prefix().str() + "([^/]+)" + match.suffix().str();
}
route.path_regex = std::regex("^" + regex_path + "$");
routes_.push_back(route);
}
public:
std::optional<Route> match(const std::string& method, const std::string& path) {
for (auto& route : routes_) {
if (route.method != method) continue;
std::smatch match;
if (std::regex_match(path, match, route.path_regex)) {
// 파라미터 추출
for (size_t i = 0; i < route.param_names.size(); ++i) {
// match[i+1]을 route에 저장
}
return route;
}
}
return std::nullopt;
}
};
2. 미들웨어 체인
미들웨어 패턴
sequenceDiagram
participant C as 클라이언트
participant M1 as 로깅
participant M2 as CORS
participant M3 as 인증
participant H as 핸들러
C->>M1: 요청
M1->>M2: next()
M2->>M3: next()
M3->>H: next()
H-->>M3: 응답
M3-->>M2: 반환
M2-->>M1: 반환
M1-->>C: 응답
class MiddlewareChain {
std::vector<Router::Middleware> middlewares_;
Router::Handler final_handler_;
public:
void add(Router::Middleware middleware) {
middlewares_.push_back(middleware);
}
void execute(const Request& req, Response& res) {
execute_at(0, req, res);
}
private:
void execute_at(size_t index, const Request& req, Response& res) {
if (index >= middlewares_.size()) {
final_handler_(req, res);
return;
}
middlewares_[index](req, res, [this, index, &req, &res]() {
execute_at(index + 1, req, res);
});
}
};
로깅 미들웨어
auto logging_middleware = {
auto start = std::chrono::steady_clock::now();
std::cout << req.method() << " " << req.path() << std::endl;
next(); // 다음 미들웨어 실행
auto end = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << " -> " << res.status_code()
<< " (" << duration.count() << "ms)" << std::endl;
};
server.use(logging_middleware);
CORS 미들웨어
auto cors_middleware = {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method() == "OPTIONS") {
res.status(204).send();
return;
}
next();
};
server.use(cors_middleware);
3. 요청/응답 처리
Request 클래스
class Request {
beast::http::request<beast::http::string_body> req_;
std::unordered_map<std::string, std::string> params_;
std::unordered_map<std::string, std::string> query_;
json body_json_;
public:
Request(beast::http::request<beast::http::string_body> req)
: req_(std::move(req)) {
parse_query();
parse_body();
}
std::string method() const {
return std::string(req_.method_string());
}
std::string path() const {
auto target = req_.target();
auto pos = target.find('?');
return std::string(target.substr(0, pos));
}
std::string param(const std::string& name) const {
auto it = params_.find(name);
return it != params_.end() ? it->second : "";
}
std::string query(const std::string& name) const {
auto it = query_.find(name);
return it != query_.end() ? it->second : "";
}
template<typename T>
T body() const {
return body_json_.get<T>();
}
const json& json_body() const {
return body_json_;
}
std::string header(const std::string& name) const {
return std::string(req_[name]);
}
private:
void parse_query() {
auto target = req_.target();
auto pos = target.find('?');
if (pos == std::string_view::npos) return;
auto query_string = target.substr(pos + 1);
// query_string 파싱: key1=value1&key2=value2
// ... 구현 생략
}
void parse_body() {
if (req_.body().empty()) return;
try {
body_json_ = json::parse(req_.body());
} catch (const json::exception&) {
// JSON 파싱 실패
}
}
};
Response 클래스
class Response {
beast::http::response<beast::http::string_body> res_;
bool sent_ = false;
public:
Response& status(unsigned code) {
res_.result(code);
return *this;
}
Response& set_header(const std::string& name, const std::string& value) {
res_.set(name, value);
return *this;
}
void json(const ::json& data) {
res_.set(beast::http::field::content_type, "application/json");
res_.body() = data.dump();
res_.prepare_payload();
sent_ = true;
}
void send(const std::string& body = "") {
res_.body() = body;
res_.prepare_payload();
sent_ = true;
}
void send_file(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
status(404).send("File not found");
return;
}
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
// MIME 타입 설정
auto ext = std::filesystem::path(path).extension().string();
set_header("Content-Type", get_mime_type(ext));
send(content);
}
unsigned status_code() const {
return res_.result_int();
}
const auto& native() const { return res_; }
};
4. 인증 미들웨어
JWT 인증
class JWTAuth {
std::string secret_;
public:
JWTAuth(const std::string& secret) : secret_(secret) {}
std::string generate(const std::string& user_id) {
return jwt::create()
.set_issuer("my-api")
.set_type("JWT")
.set_payload_claim("user_id", jwt::claim(user_id))
.set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24))
.sign(jwt::algorithm::hs256{secret_});
}
std::optional<std::string> verify(const std::string& token) {
try {
auto decoded = jwt::decode(token);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::hs256{secret_})
.with_issuer("my-api");
verifier.verify(decoded);
return decoded.get_payload_claim("user_id").as_string();
} catch (const std::exception&) {
return std::nullopt;
}
}
};
// 인증 미들웨어
auto auth_middleware(JWTAuth& jwt_auth) {
return [&jwt_auth](const Request& req, Response& res, auto next) {
auto auth_header = req.header("Authorization");
if (auth_header.empty() || !auth_header.starts_with("Bearer ")) {
res.status(401).json({{"error", "Unauthorized"}});
return;
}
auto token = auth_header.substr(7); // "Bearer " 제거
auto user_id = jwt_auth.verify(token);
if (!user_id) {
res.status(401).json({{"error", "Invalid token"}});
return;
}
// req에 user_id 저장 (const_cast 필요)
const_cast<Request&>(req).set_user_id(*user_id);
next();
};
}
// 사용 예시
server.get("/profile", auth_middleware(jwt_auth),
{
auto user_id = req.user_id();
auto profile = get_user_profile(user_id);
res.json(profile);
});
5. 검증 및 에러 처리
요청 검증
class Validator {
public:
struct Rule {
std::string field;
std::function<bool(const json&)> check;
std::string message;
};
std::vector<Rule> rules_;
Validator& required(const std::string& field) {
rules_.push_back({
field,
[field](const json& data) { return data.contains(field); },
field + " is required"
});
return *this;
}
Validator& string(const std::string& field) {
rules_.push_back({
field,
[field](const json& data) {
return data.contains(field) && data[field].is_string();
},
field + " must be a string"
});
return *this;
}
Validator& min_length(const std::string& field, size_t len) {
rules_.push_back({
field,
[field, len](const json& data) {
return data.contains(field) &&
data[field].is_string() &&
data[field].get<std::string>().length() >= len;
},
field + " must be at least " + std::to_string(len) + " characters"
});
return *this;
}
std::optional<std::vector<std::string>> validate(const json& data) {
std::vector<std::string> errors;
for (const auto& rule : rules_) {
if (!rule.check(data)) {
errors.push_back(rule.message);
}
}
return errors.empty() ? std::nullopt : std::make_optional(errors);
}
};
// 사용 예시
server.post("/users", {
Validator validator;
validator.required("email")
.string("email")
.required("password")
.min_length("password", 8);
auto errors = validator.validate(req.json_body());
if (errors) {
res.status(400).json({{"errors", *errors}});
return;
}
// 검증 통과
auto user = create_user(req.body<User>());
res.status(201).json(user);
});
전역 에러 핸들러
class ErrorHandler {
public:
static void handle(const std::exception& e, Response& res) {
if (auto* ve = dynamic_cast<const ValidationError*>(&e)) {
res.status(400).json({
{"error", "Validation failed"},
{"details", ve->errors()}
});
}
else if (auto* ne = dynamic_cast<const NotFoundException*>(&e)) {
res.status(404).json({
{"error", ne->what()}
});
}
else if (auto* ae = dynamic_cast<const AuthError*>(&e)) {
res.status(401).json({
{"error", ae->what()}
});
}
else {
res.status(500).json({
{"error", "Internal server error"},
{"message", e.what()}
});
}
}
};
// 에러 핸들링 미들웨어
auto error_handler_middleware = {
try {
next();
} catch (const std::exception& e) {
ErrorHandler::handle(e, res);
}
};
server.use(error_handler_middleware);
6. Swagger 문서 생성
API 문서 정의
struct APIDoc {
std::string path;
std::string method;
std::string summary;
std::string description;
json parameters;
json request_body;
json responses;
};
class SwaggerGenerator {
std::vector<APIDoc> docs_;
public:
void add_doc(const APIDoc& doc) {
docs_.push_back(doc);
}
json generate() {
json swagger = {
{"openapi", "3.0.0"},
{"info", {
{"title", "My API"},
{"version", "1.0.0"}
}},
{"paths", json::object()}
};
for (const auto& doc : docs_) {
if (!swagger["paths"].contains(doc.path)) {
swagger["paths"][doc.path] = json::object();
}
swagger["paths"][doc.path][doc.method] = {
{"summary", doc.summary},
{"description", doc.description},
{"parameters", doc.parameters},
{"requestBody", doc.request_body},
{"responses", doc.responses}
};
}
return swagger;
}
};
// 사용 예시
SwaggerGenerator swagger;
swagger.add_doc({
"/users",
"post",
"Create user",
"Create a new user account",
json::array(),
{
{"content", {
{"application/json", {
{"schema", {
{"type", "object"},
{"properties", {
{"email", {{"type", "string"}}},
{"password", {{"type", "string"}}}
}},
{"required", {"email", "password"}}
}}
}}
}}
},
{
{"201", {
{"description", "User created"},
{"content", {
{"application/json", {
{"schema", {
{"type", "object"},
{"properties", {
{"id", {{"type", "string"}}},
{"email", {{"type", "string"}}}
}}
}}
}}
}}
}},
{"400", {{"description", "Validation error"}}}
}
});
// Swagger UI 엔드포인트
server.get("/api-docs", [&swagger](const Request& req, Response& res) {
res.json(swagger.generate());
});
7. 완전한 REST API 예시
요청 흐름 (미들웨어 → 라우트)
sequenceDiagram
participant C as 클라이언트
participant L as 로깅
participant CO as CORS
participant A as 인증
participant H as 핸들러
C->>L: GET /users
L->>CO: next()
CO->>A: next()
A->>A: JWT 검증
alt 인증 성공
A->>H: next()
H-->>C: 200 JSON
else 인증 실패
A-->>C: 401 Unauthorized
end
사용자 관리 API 전체 예시 (라우팅·미들웨어·CORS·JSON·인증)
// main.cpp - 사용자 CRUD API 완전 예시
int main() {
RestServer server;
JWTAuth jwt_auth("my-secret-key-change-in-production");
SwaggerGenerator swagger;
server.use(logging_middleware);
server.use(cors_middleware);
server.use(error_handler_middleware);
// 공개: 로그인, 회원가입
server.post("/auth/login", [&jwt_auth](const Request& req, Response& res) {
auto body = req.json_body();
auto user = authenticate(body["email"], body["password"]);
if (!user) { res.status(401).json({{"error", "Invalid credentials"}}); return; }
res.json({{"token", jwt_auth.generate(user->id)}, {"user", *user}});
});
server.post("/auth/register", {
Validator v; v.required("email").required("password").min_length("password", 8);
auto err = v.validate(req.json_body());
if (err) { res.status(400).json({{"errors", *err}}); return; }
auto u = create_user(req.json_body());
res.status(201).json({{"id", u.id}, {"email", u.email}});
});
// 인증 필요: CRUD
server.get("/users", auth_middleware(jwt_auth), {
res.json({{"users", get_users_paginated(req.query("page"), req.query("limit"))}});
});
server.get("/users/:id", auth_middleware(jwt_auth), {
auto u = get_user(req.param("id"));
u ? res.json(*u) : res.status(404).json({{"error", "Not found"}});
});
server.put("/users/:id", auth_middleware(jwt_auth), {
if (req.user_id() != req.param("id")) { res.status(403).send(); return; }
res.json(update_user(req.param("id"), req.json_body()));
});
server.del("/users/:id", auth_middleware(jwt_auth), {
if (req.user_id() != req.param("id")) { res.status(403).send(); return; }
delete_user(req.param("id")) ? res.status(204).send() : res.status(404).send();
});
server.get("/api-docs", [&swagger](auto&, auto& res) { res.json(swagger.generate()); });
server.listen(8080);
return 0;
}
cURL로 전체 API 테스트
# 1. 회원가입
curl -X POST http://localhost:8080/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"secret1234","name":"홍길동"}'
# 2. 로그인
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"secret1234"}'
# 응답: {"token":"eyJ...", "user":{"id":"...","email":"..."}}
# 3. 사용자 목록 (인증 필요)
curl -X GET http://localhost:8080/users?page=1&limit=10 \
-H "Authorization: Bearer eyJ..."
# 4. 사용자 상세
curl -X GET http://localhost:8080/users/123 \
-H "Authorization: Bearer eyJ..."
# 5. 사용자 수정
curl -X PUT http://localhost:8080/users/123 \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"name":"홍길동2","email":"[email protected]"}'
# 6. 사용자 삭제
curl -X DELETE http://localhost:8080/users/123 \
-H "Authorization: Bearer eyJ..."
8. 자주 발생하는 에러와 해결법
문제 1: “Connection reset” / 요청 본문 과대
원인: 요청 본문이 너무 크거나 클라이언트 조기 종료. 해결: Beast parser.body_limit(1024*1024) 설정 후 초과 시 res.status(413).json({{"error","Request entity too large"}}) 반환.
문제 2: JSON 파싱 실패 (400)
원인: Content-Type이 application/json이 아닌데 파싱 시도, 또는 잘못된 JSON. 해결: Content-Type 검사 후 json::parse() 예외 처리 시 BadRequestException throw.
문제 3: JWT 토큰 만료 (401)
원인: exp 클레임 만료. 해결: POST /auth/refresh 엔드포인트로 리프레시 토큰 교환 후 새 액세스 토큰 발급.
문제 4: CORS preflight 실패
원인: OPTIONS 요청 미처리 또는 Access-Control-Allow-* 헤더 누락. 해결:
if (req.method() == "OPTIONS") {
res.set_header("Access-Control-Allow-Origin", "*");
res.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.status(204).send();
return; // next() 호출하지 않음!
}
문제 5: next() 호출 누락
원인: 미들웨어에서 인증 성공 시 next()를 호출하지 않음. 해결: 인증 실패 시 res.status(401).json(...) 후 return, 성공 시 반드시 next() 호출.
// ✅ 올바른 패턴
if (!user_id) { res.status(401).json({{"error","Unauthorized"}}); return; }
const_cast<Request&>(req).set_user_id(*user_id);
next(); // 필수!
문제 6: 경로 파라미터 매칭 실패
원인: :id → ([^/]+) 변환 시 특수문자 이스케이프 누락. 해결: std::regex_replace로 :param을 ([^/]+)로 변환 후 ^...$로 전체 매칭.
문제 7: 동시 요청 시 데이터 레이스
원인: const_cast로 Request에 user_id 저장 시 요청 겹침. 해결: 요청별 RequestContext 구조체로 독립 저장.
문제 8: 404 핸들러 누락
원인: 매칭 라우트 없을 때 미처리. 해결: router.match()가 nullopt일 때 res.status(404).json({{"error","Not found"}}) 반환.
9. 베스트 프랙티스
1. 미들웨어 등록 순서
미들웨어는 등록 순서대로 실행됩니다. 로깅 → CORS → 에러 핸들러를 먼저 등록하고, 인증은 라우트별로 적용합니다.
server.use(logging_middleware);
server.use(cors_middleware);
server.use(error_handler_middleware);
server.get("/users", auth_middleware(jwt_auth), handler);
2. JSON 응답 형식 통일
성공/실패 모두 data 또는 error 키로 통일하면 클라이언트 파싱이 단순해집니다. request_id를 포함하면 디버깅이 용이합니다.
3. HTTP 메서드와 상태 코드 준수
| 작업 | 메서드 | 성공 | 실패 |
|---|---|---|---|
| 생성 | POST | 201 | 400, 409 |
| 조회 | GET | 200 | 404 |
| 수정 | PUT/PATCH | 200 | 400, 404 |
| 삭제 | DELETE | 204 | 404 |
4. 환경 변수로 설정 분리
JWT 시크릿 등은 절대 하드코딩 금지. std::getenv("JWT_SECRET") 사용.
5. 비동기 I/O와 스레드 모델
io_context를 std::thread::hardware_concurrency()개 스레드에서 run()하는 패턴 사용.
6. API 버저닝
URL 경로에 버전 포함: /v1/users, /v2/users.
10. 성능 최적화 팁
1. JSON 직렬화 최적화
// JSON dump 시 불필요한 공백 제거 (프로덕션)
void json_response(const json& data) {
res_.set(beast::http::field::content_type, "application/json");
res_.body() = data.dump(-1); // -1: 들여쓰기 없음, 최소 크기
res_.prepare_payload();
}
2. 연결 풀링 (Keep-Alive)
// HTTP Keep-Alive로 연결 재사용
void prepare_response() {
res_.set(beast::http::field::connection, "keep-alive");
res_.set(beast::http::field::keep_alive, "timeout=60");
}
3. 응답 버퍼 사전 할당
// 큰 응답 시 버퍼 미리 예약
void json(const json& data) {
std::string body = data.dump(-1);
res_.body().reserve(body.size() + 256); // 헤더 여유
res_.body() = std::move(body);
res_.prepare_payload();
}
4. 정규표현식 캐싱
// 라우트 매칭 시 정규표현식은 컴파일 비용이 큼
// 경로를 컴파일 타임에 생성해 두기
class Router {
std::unordered_map<std::string, std::regex> path_cache_;
std::regex get_or_compile(const std::string& path) {
auto it = path_cache_.find(path);
if (it != path_cache_.end()) return it->second;
auto compiled = compile_path_to_regex(path);
path_cache_[path] = compiled;
return compiled;
}
};
5. 비동기 I/O 활용
// 동기 I/O 대신 Boost.Beast async 사용
void handle_request() {
http::async_read(socket_, buffer_, req_,
[this](beast::error_code ec, std::size_t) {
if (!ec) {
process_request();
}
});
}
6. 성능 벤치마크 참고
| 구분 | 동기 (단일 스레드) | 비동기 (단일 스레드) | 비동기 (4스레드) |
|---|---|---|---|
| 1K 요청/초 | ~800 | ~1,500 | ~5,000 |
| 평균 지연 | 1.2ms | 0.6ms | 0.2ms |
| 메모리 | 50MB | 80MB | 120MB |
11. 프로덕션 패턴
1. 헬스 체크 엔드포인트
// Kubernetes / 로드밸런서용
server.get("/health", {
res.json({{"status", "ok"}, {"version", "1.0.0"}, {"timestamp", std::time(nullptr)}});
});
server.get("/health/ready", {
if (!check_database_connection()) res.status(503).json({{"status", "unhealthy"}});
else res.json({{"status", "ready"}});
});
2. 요청 ID 추적
auto request_id_middleware = {
auto req_id = req.header("X-Request-ID").empty() ? generate_uuid() : req.header("X-Request-ID");
res.set_header("X-Request-ID", req_id);
const_cast<Request&>(req).set_request_id(req_id);
next();
};
3. 속도 제한 (Rate Limiting)
// 토큰 버킷: client_id당 분당 100회
class RateLimiter {
std::unordered_map<std::string, std::pair<int, std::chrono::steady_clock::time_point>> limits_;
int max_ = 100;
std::chrono::seconds window_{60};
public:
bool allow(const std::string& client_id) {
auto now = std::chrono::steady_clock::now();
auto& [count, start] = limits_[client_id];
if (now - start > window_) { count = 0; start = now; }
return count++ < max_;
}
};
// 429 Too Many Requests 반환
4. 구조화된 로깅
// JSON 형식 로그 (ELK/CloudWatch 수집용)
void log_request(const Request& req, const Response& res, auto duration) {
std::cerr << json{{"method", req.method()}, {"path", req.path()},
{"status", res.status_code()}, {"duration_ms", duration.count()},
{"request_id", req.request_id()}}.dump() << std::endl;
}
5. Graceful Shutdown
std::atomic<bool> running{true};
std::signal(SIGTERM, { running = false; });
while (running) server.poll();
server.stop();
6. 프로덕션 체크리스트
- [ ] JWT 시크릿을 환경 변수로 분리 (절대 하드코딩 금지)
- [ ] CORS Allow-Origin을 "*" 대신 허용 도메인으로 제한
- [ ] HTTPS 전용 (HTTP 리다이렉트)
- [ ] 요청 본문 크기 제한 (1MB~10MB)
- [ ] Rate limiting 적용
- [ ] 헬스 체크 엔드포인트 노출
- [ ] 구조화된 로깅 (JSON)
- [ ] Graceful shutdown 구현
- [ ] Swagger UI는 개발 환경에서만 노출
정리
| 기능 | 구현 방법 |
|---|---|
| 라우팅 | 정규표현식 + 파라미터 추출 |
| 미들웨어 | 함수 체인 + next() 콜백 |
| 인증 | JWT + Bearer 토큰 |
| 검증 | Rule 기반 Validator |
| 문서화 | Swagger/OpenAPI 자동 생성 |
핵심 원칙:
- Express 스타일 API로 생산성 향상
- 미들웨어로 횡단 관심사 분리
- JSON 자동 파싱으로 편의성 제공
- 타입 안전성 유지
- 자동 문서화로 유지보수성 확보
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 백엔드 API 서버, 마이크로서비스, 모바일 앱 백엔드, IoT 게이트웨이 등 HTTP 기반 서비스 개발에 활용합니다. 고성능이 필요한 금융, 게임, 실시간 시스템에서 특히 유용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
한 줄 요약: Express 스타일 라우팅과 미들웨어로 생산적인 C++ REST API 서버를 만들 수 있습니다.
다음 글: [C++ 실전 가이드 #50-3] 게임 엔진 기초: 렌더링·물리·스크립팅
이전 글: [C++ 실전 가이드 #50-1] 채팅 서버 완성하기
관련 글
- C++ 초경량 HTTP 웹 프레임워크 바닥부터 만들기 [#48-2]
- C++ REST API 서버 완벽 가이드 | Beast 라우팅·JSON·미들웨어 [#31-2]