C++ REST API 서버 만들기 | 라우팅·미들웨어·인증·Swagger 문서화 [#50-2]

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


목차

  1. 라우터 설계
  2. 미들웨어 체인
  3. 요청/응답 처리
  4. 인증 미들웨어
  5. 검증 및 에러 처리
  6. Swagger 문서 생성
  7. 완전한 REST API 예시
  8. 자주 발생하는 에러와 해결법
  9. 베스트 프랙티스
  10. 성능 최적화 팁
  11. 프로덕션 패턴

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_castRequestuser_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 메서드와 상태 코드 준수

작업메서드성공실패
생성POST201400, 409
조회GET200404
수정PUT/PATCH200400, 404
삭제DELETE204404

4. 환경 변수로 설정 분리

JWT 시크릿 등은 절대 하드코딩 금지. std::getenv("JWT_SECRET") 사용.

5. 비동기 I/O와 스레드 모델

io_contextstd::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.2ms0.6ms0.2ms
메모리50MB80MB120MB

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 자동 생성

핵심 원칙:

  1. Express 스타일 API로 생산성 향상
  2. 미들웨어로 횡단 관심사 분리
  3. JSON 자동 파싱으로 편의성 제공
  4. 타입 안전성 유지
  5. 자동 문서화로 유지보수성 확보

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


자주 묻는 질문 (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]