C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]

C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]

이 글의 핵심

REST API 연동 시 JSON 파싱·인증·타임아웃·재시도 문제를 해결합니다. C++에서 CRUD 작업, Bearer 토큰·API 키 인증, 에러 처리, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴까지 실전 코드로 구현합니다.

들어가며: “REST API 호출은 되는데 응답 처리가 막막해요”

실제 겪는 문제 시나리오

// ❌ 문제: HTTP 클라이언트로 요청은 보냈는데...
// - 결제 API 응답 {"status": "ok", "transaction_id": "tx_123"} 를 어떻게 파싱하지?
// - 401 Unauthorized 응답 시 토큰 갱신 로직은 어디에 넣어야 하지?
// - 서버가 503을 반환하면 재시도할지, 폴백할지 어떻게 결정하지?
// - JSON 본문이 잘못된 형식이면 크래시가 나요

실제 프로덕션에서 겪는 문제들:

  • JSON 파싱 실패: {"data": null} 응답에서 j["data"][0] 접근 시 예외 발생
  • 인증 만료: Bearer 토큰이 만료되면 401 응답, 수동 갱신 없이 재시도만 하면 무한 루프
  • 에러 처리 불일치: HTTP 4xx는 클라이언트 에러, 5xx는 서버 에러인데 구분 없이 처리
  • 타임아웃·재시도 부재: 느린 API에서 무한 대기, 일시적 장애 시 즉시 실패
  • CRUD 패턴 불일치: GET/POST/PUT/DELETE 각각에 맞는 헤더·본문 형식이 혼란

해결책:

  1. 완전한 REST API 클라이언트: CRUD 메서드, JSON 직렬화/역직렬화, 통일된 응답 타입
  2. 인증 처리: Bearer 토큰, API 키, Basic 인증을 헤더에 자동 주입
  3. 에러 분류: 연결 실패, 타임아웃, HTTP 4xx/5xx, JSON 파싱 에러 구분
  4. 재시도·회로 차단기: 일시적 실패에만 재시도, 연속 실패 시 요청 중단

목표:

  • CRUD 작업 완전 예제 (GET, POST, PUT, DELETE)
  • 인증 (Bearer, API 키, Basic)
  • 에러 처리 (연결 실패, HTTP 에러, JSON 파싱)
  • 자주 발생하는 에러와 해결법
  • 베스트 프랙티스프로덕션 패턴

이 글을 읽으면:

  • HTTP 클라이언트와 JSON을 결합해 REST API를 호출할 수 있습니다.
  • 인증 헤더를 추가하고, 에러를 분류해 처리할 수 있습니다.
  • 프로덕션 수준의 재시도·로깅·모니터링 패턴을 적용할 수 있습니다.

목차

  1. 문제 시나리오와 해결 방향
  2. REST API 클라이언트 아키텍처
  3. 기본 REST API 클라이언트 구현
  4. CRUD 작업 완전 예제
  5. 인증 처리
  6. 에러 처리
  7. 자주 발생하는 에러와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴

1. 문제 시나리오와 해결 방향

1.1 전형적인 실패 패턴

sequenceDiagram
  participant App as 애플리케이션
  participant Client as REST API 클라이언트
  participant Server as 외부 API 서버

  App->>Client: GET /api/users
  Client->>Server: HTTP 요청 (인증 없음?)
  Server-->>Client: 401 Unauthorized
  Note over Client: 토큰 없음, 재시도해도 401
  Client-->>App: 실패 (원인 불명)

  App->>Client: POST /api/orders
  Client->>Server: JSON 본문
  Server-->>Client: 200 OK {"data": {...}}
  Client->>Client: j["data"] 접근 → 예외
  Note over Client: data가 null이거나 형식 다름

문제 요약:

  • 인증 헤더 누락 → 401 반복
  • JSON 응답 형식 가정 → 파싱 예외
  • 에러 원인 불명 → 디버깅 어려움

1.2 해결 아키텍처

flowchart TB
  subgraph 개선된 흐름
    A[요청] --> B[인증 헤더 주입]
    B --> C[HTTP 요청 전송]
    C --> D[응답 수신]
    D --> E{상태 코드}
    E -->|2xx| F[JSON 파싱]
    E -->|3xx| G[리다이렉트 처리]
    E -->|4xx| H[클라이언트 에러]
    E -->|5xx| I[재시도 가능?]
    F --> J[결과 반환]
    H --> K[에러 분류 반환]
    I -->|Yes| L[재시도]
    I -->|No| K
  end

1.3 추가 문제 시나리오

시나리오 1: 결제 API 연동
결제 서버에 POST /payments로 요청을 보냈는데, 응답이 {"error": "invalid_card"} 형태로 오거나, 성공 시 {"transaction_id": "tx_123"} 형태로 옵니다. 에러와 성공 응답 구조가 달라서 단일 파싱 로직으로 처리하기 어렵습니다.

시나리오 2: 마이크로서비스 간 호출
Order 서비스가 Inventory 서비스에 재고 확인 요청을 보냅니다. Inventory가 503을 반환하면 “일시적 장애”로 재시도해야 하는데, 400 Bad Request면 재시도하면 안 됩니다. 상태 코드별 분기 처리가 필요합니다.

시나리오 3: OAuth 토큰 갱신
액세스 토큰이 만료되면 401이 옵니다. 리프레시 토큰으로 /auth/refresh를 호출해 새 액세스 토큰을 받아야 합니다. 이 과정을 클라이언트가 자동으로 처리하지 않으면, 매번 사용자가 재로그인해야 합니다.

시나리오 4: 대량 데이터 페이지네이션
GET /users?page=1&limit=100으로 사용자 목록을 가져옵니다. 응답이 {"data": [...], "total": 5000, "page": 1} 형태인데, data가 빈 배열일 수 있고, total이 없을 수도 있습니다. null·빈 배열·누락 필드 처리가 필요합니다.

시나리오 5: 429 Rate Limit 초과
외부 API가 분당 100회 제한을 두고 있습니다. Retry-After 헤더 없이 429를 받으면 언제 재시도해야 할지 모릅니다. 무작위 재시도는 오히려 제한을 더 악화시킬 수 있습니다.

시나리오 6: 연결 타임아웃·연결 거부
서버가 다운되었거나 네트워크가 불안정할 때 connect()가 무한 대기합니다. 타임아웃 없이 블로킹되면 전체 애플리케이션이 멈춥니다.

시나리오 7: SSL/TLS 인증서 검증 실패
HTTPS API 호출 시 자체 서명 인증서나 만료된 인증서로 연결이 거부됩니다. 개발 환경과 프로덕션 환경의 인증서 설정이 달라 혼란이 발생합니다.


2. REST API 클라이언트 아키텍처

2.1 구성 요소

구성 요소역할
HTTP 클라이언트TCP 연결, 요청 전송, 응답 수신
JSON 직렬화요청 본문 생성, 응답 파싱
인증 모듈토큰·API 키·Basic 헤더 주입
에러 처리연결 실패, HTTP 에러, JSON 파싱 에러 분류

2.2 HTTP 응답 래퍼

#include <string>
#include <map>

struct ApiResponse {
    int status_code;
    std::string status_message;
    std::map<std::string, std::string> headers;
    std::string body;  // JSON 문자열

    bool is_success() const {
        return status_code >= 200 && status_code < 300;
    }

    bool is_client_error() const {
        return status_code >= 400 && status_code < 500;
    }

    bool is_server_error() const {
        return status_code >= 500;
    }

    bool is_retryable() const {
        return status_code == 429 || status_code >= 500;
    }
};

2.3 아키텍처 다이어그램

flowchart LR
  subgraph 클라이언트
    A[RestApiClient] --> B[HTTP Layer]
    A --> C[Auth Layer]
    A --> D[Error Handler]
    B --> E[connectToHost]
    B --> F[send/recv]
    C --> G[Bearer/API Key/Basic]
  end
  B --> H[외부 API 서버]

3. 기본 REST API 클라이언트 구현

3.1 의존성

HTTP 클라이언트는 #21-1 HTTP 클라이언트connectToHost, buildRequest, readAll, parseResponse 등을 사용합니다. JSON은 nlohmann/json을 사용합니다.

# vcpkg로 nlohmann-json 설치
vcpkg install nlohmann-json
#include <nlohmann/json.hpp>
using json = nlohmann::json;

3.2 기본 REST 클라이언트 클래스

#include <string>
#include <map>

class RestApiClient {
public:
    RestApiClient(const std::string& base_url, int timeout_sec = 10)
        : base_url_(base_url), timeout_sec_(timeout_sec) {}

    ApiResponse get(const std::string& path,
                    const std::map<std::string, std::string>& extra_headers = {}) {
        return request("GET", path, "", extra_headers);
    }

    ApiResponse post(const std::string& path, const std::string& json_body,
                     const std::map<std::string, std::string>& extra_headers = {}) {
        return request("POST", path, json_body, extra_headers);
    }

    ApiResponse put(const std::string& path, const std::string& json_body,
                    const std::map<std::string, std::string>& extra_headers = {}) {
        return request("PUT", path, json_body, extra_headers);
    }

    ApiResponse del(const std::string& path,
                    const std::map<std::string, std::string>& extra_headers = {}) {
        return request("DELETE", path, "", extra_headers);
    }

private:
    std::string base_url_;
    int timeout_sec_;

    ApiResponse request(const std::string& method, const std::string& path,
                        const std::string& body,
                        const std::map<std::string, std::string>& extra_headers) {
        ApiResponse res;
        // parse base_url_ → host, port, base_path
        // connectToHost, buildRequest, send, readAll, parseResponse
        // ApiResponse로 변환하여 반환
        return res;
    }
};

3.3 URL 파싱

#include <regex>
#include <tuple>

std::tuple<std::string, uint16_t, std::string> parseUrl(const std::string& url) {
    // http://example.com:8080/api/v1 → ("example.com", 8080, "/api/v1")
    std::regex re(R"(^(?:https?://)?([^:/]+)(?::(\d+))?(/.*)?$)");
    std::smatch m;
    if (!std::regex_match(url, m, re)) {
        return {"", 80, ""};
    }
    std::string host = m[1].str();
    uint16_t port = m[2].matched ? static_cast<uint16_t>(std::stoi(m[2].str())) : 80;
    std::string path = m[3].matched ? m[3].str() : "/";
    return {host, port, path};
}

3.4 JSON 기본 헤더

std::map<std::string, std::string> default_json_headers() {
    return {
        {"Content-Type", "application/json"},
        {"Accept", "application/json"}
    };
}

4. CRUD 작업 완전 예제

4.1 사용자 API 예제 (JSON Placeholder 스타일)

{
  "id": 1,
  "name": "Leanne Graham",
  "email": "[email protected]",
  "company": {
    "name": "Acme Corp"
  }
}

4.2 GET (조회)

#include <nlohmann/json.hpp>
#include <iostream>

void example_get_user(RestApiClient& client) {
    ApiResponse res = client.get("/users/1");

    if (!res.is_success()) {
        std::cerr << "GET failed: " << res.status_code << " " << res.status_message << "\n";
        return;
    }

    try {
        json j = json::parse(res.body);
        std::string name = j["name"].get<std::string>();
        std::string email = j["email"].get<std::string>();
        std::cout << "User: " << name << " (" << email << ")\n";

        if (j.contains("company") && j["company"].contains("name")) {
            std::cout << "Company: " << j["company"]["name"].get<std::string>() << "\n";
        }
    } catch (const json::exception& e) {
        std::cerr << "JSON parse error: " << e.what() << "\n";
    }
}

4.3 POST (생성)

void example_create_user(RestApiClient& client) {
    json body;
    body["name"] = "John Doe";
    body["email"] = "[email protected]";
    body["status"] = "active";

    std::map<std::string, std::string> headers;
    headers["Content-Type"] = "application/json";

    ApiResponse res = client.post("/users", body.dump(), headers);

    if (!res.is_success()) {
        std::cerr << "POST failed: " << res.status_code << "\n";
        std::cerr << "Response: " << res.body << "\n";
        return;
    }

    try {
        json j = json::parse(res.body);
        int id = j["id"].get<int>();
        std::cout << "Created user with id: " << id << "\n";
    } catch (const json::exception& e) {
        std::cerr << "JSON parse error: " << e.what() << "\n";
    }
}

4.4 PUT (수정)

void example_update_user(RestApiClient& client, int user_id) {
    json body;
    body["name"] = "Jane Doe";
    body["email"] = "[email protected]";

    std::map<std::string, std::string> headers;
    headers["Content-Type"] = "application/json";

    std::string path = "/users/" + std::to_string(user_id);
    ApiResponse res = client.put(path, body.dump(), headers);

    if (!res.is_success()) {
        std::cerr << "PUT failed: " << res.status_code << "\n";
        return;
    }

    try {
        json j = json::parse(res.body);
        std::cout << "Updated: " << j["name"].get<std::string>() << "\n";
    } catch (const json::exception& e) {
        std::cerr << "JSON parse error: " << e.what() << "\n";
    }
}

4.5 DELETE (삭제)

void example_delete_user(RestApiClient& client, int user_id) {
    std::string path = "/users/" + std::to_string(user_id);
    ApiResponse res = client.del(path);

    if (res.is_success()) {
        std::cout << "User " << user_id << " deleted\n";
    } else if (res.status_code == 404) {
        std::cerr << "User not found\n";
    } else {
        std::cerr << "DELETE failed: " << res.status_code << "\n";
    }
}

4.6 CRUD 흐름 시퀀스

sequenceDiagram
  participant App as 애플리케이션
  participant Client as RestApiClient
  participant Server as API 서버

  App->>Client: get("/users/1")
  Client->>Server: GET /users/1 HTTP/1.1
  Server-->>Client: 200 OK {"id":1,"name":"..."}
  Client-->>App: ApiResponse

  App->>Client: post("/users", json_body)
  Client->>Server: POST /users HTTP/1.1 + JSON
  Server-->>Client: 201 Created {"id":2}
  Client-->>App: ApiResponse

  App->>Client: put("/users/2", json_body)
  Client->>Server: PUT /users/2 HTTP/1.1 + JSON
  Server-->>Client: 200 OK
  Client-->>App: ApiResponse

  App->>Client: del("/users/2")
  Client->>Server: DELETE /users/2 HTTP/1.1
  Server-->>Client: 200 OK
  Client-->>App: ApiResponse

4.7 쿼리 파라미터

std::string build_query(const std::map<std::string, std::string>& params) {
    if (params.empty()) return "";
    std::string q = "?";
    bool first = true;
    for (const auto& [k, v] : params) {
        if (!first) q += "&";
        q += k + "=" + v;  // 실제로는 URL 인코딩 필요
        first = false;
    }
    return q;
}

// GET /users?page=1&limit=10
void example_list_users(RestApiClient& client) {
    std::map<std::string, std::string> params{{"page", "1"}, {"limit", "10"}};
    std::string path = "/users" + build_query(params);
    ApiResponse res = client.get(path);
    // ...
}

5. 인증 처리

5.1 Bearer 토큰

class AuthenticatedRestClient : public RestApiClient {
public:
    AuthenticatedRestClient(const std::string& base_url,
                            const std::string& bearer_token,
                            int timeout_sec = 10)
        : RestApiClient(base_url, timeout_sec), bearer_token_(bearer_token) {}

    ApiResponse get(const std::string& path,
                    const std::map<std::string, std::string>& extra_headers = {}) {
        auto headers = extra_headers;
        headers["Authorization"] = "Bearer " + bearer_token_;
        return RestApiClient::get(path, headers);
    }
    // post, put, del도 동일하게 Authorization 헤더 추가

private:
    std::string bearer_token_;
};

5.2 API 키 (헤더)

void add_api_key_header(std::map<std::string, std::string>& headers,
                        const std::string& api_key,
                        const std::string& header_name = "X-API-Key") {
    headers[header_name] = api_key;
}

// 사용 예
std::map<std::string, std::string> headers;
add_api_key_header(headers, "sk_live_abc123");
ApiResponse res = client.get("/api/data", headers);

5.3 Basic 인증

#include <cstdint>

std::string base64_encode(const std::string& input) {
    static const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    std::string result;
    int val = 0, valb = -6;
    for (unsigned char c : input) {
        val = (val << 8) + c;
        valb += 8;
        while (valb >= 0) {
            result.push_back(chars[(val >> valb) & 0x3F]);
            valb -= 6;
        }
    }
    if (valb > -6) result.push_back(chars[((val << 8) >> (valb + 8)) & 0x3F]);
    while (result.size() % 4) result.push_back('=');
    return result;
}

void add_basic_auth(std::map<std::string, std::string>& headers,
                    const std::string& username, const std::string& password) {
    std::string cred = username + ":" + password;
    headers["Authorization"] = "Basic " + base64_encode(cred);
}

5.4 토큰 갱신 (401 처리)

class TokenRefreshClient {
public:
    bool get_with_refresh(const std::string& path, ApiResponse& out) {
        out = client_.get(path, auth_headers());

        if (out.status_code == 401 && !refresh_token_.empty()) {
            if (refresh_token()) {
                out = client_.get(path, auth_headers());
            }
        }
        return out.is_success();
    }

private:
    std::map<std::string, std::string> auth_headers() {
        std::map<std::string, std::string> h;
        h["Authorization"] = "Bearer " + access_token_;
        return h;
    }

    bool refresh_token() {
        json body;
        body["refresh_token"] = refresh_token_;
        ApiResponse res = client_.post("/auth/refresh", body.dump());
        if (!res.is_success()) return false;
        try {
            json j = json::parse(res.body);
            access_token_ = j["access_token"].get<std::string>();
            return true;
        } catch (...) {
            return false;
        }
    }

    RestApiClient client_;
    std::string access_token_;
    std::string refresh_token_;
};

6. 에러 처리

6.1 에러 타입

enum class ApiErrorType {
    None,
    ConnectionFailed,
    Timeout,
    HttpError,
    JsonParseError,
    InvalidResponse
};

struct ApiError {
    ApiErrorType type;
    int status_code;
    std::string message;
    std::string raw_body;
};

6.2 Result 타입

#include <variant>

template<typename T>
using ApiResult = std::variant<T, ApiError>;

ApiResult<json> get_json_safe(RestApiClient& client, const std::string& path) {
    ApiResponse res = client.get(path);

    if (!res.is_success()) {
        return ApiError{
            ApiErrorType::HttpError,
            res.status_code,
            res.status_message,
            res.body
        };
    }

    try {
        return json::parse(res.body);
    } catch (const json::exception& e) {
        return ApiError{
            ApiErrorType::JsonParseError,
            0,
            e.what(),
            res.body
        };
    }
}

// 사용 예
auto result = get_json_safe(client, "/users/1");
if (std::holds_alternative<json>(result)) {
    json j = std::get<json>(result);
    std::cout << j["name"] << "\n";
} else {
    ApiError err = std::get<ApiError>(result);
    std::cerr << "Error: " << static_cast<int>(err.type)
              << " " << err.message << "\n";
}

6.3 에러 처리 흐름

flowchart TB
  subgraph 에러 처리
    A[요청] --> B{연결}
    B -->|실패| C[ConnectionFailed]
    B -->|성공| D[HTTP 응답]
    D --> E{상태 코드}
    E -->|2xx| F[JSON 파싱]
    E -->|4xx/5xx| G[HttpError]
    F --> H{파싱}
    H -->|성공| I[결과 반환]
    H -->|실패| J[JsonParseError]
    G --> K[에러 반환]
    C --> K
    J --> K
  end

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

7.1 “JSON parse error: parse error at line 1”

원인: 응답 본문이 JSON이 아님. HTML 에러 페이지, 빈 문자열, 잘못된 인코딩

해결:

// ❌ 잘못된 예
json j = json::parse(res.body);  // body가 HTML이면 예외

// ✅ 올바른 예
if (res.body.empty()) return;
auto it = res.headers.find("content-type");
if (it != res.headers.end() && it->second.find("application/json") == std::string::npos) {
    return;  // JSON이 아님
}
try {
    json j = json::parse(res.body);
} catch (const json::exception& e) {
    std::cerr << "Parse error: " << e.what() << "\nRaw: " << res.body << "\n";
}

7.2 “key ‘data’ not found” (type_error)

원인: API가 {"data": null} 또는 {"error": "..."} 응답

해결:

// ❌ 잘못된 예
std::string name = j["data"]["name"].get<std::string>();

// ✅ 올바른 예
if (j.contains("data") && !j["data"].is_null()) {
    if (j["data"].contains("name")) {
        std::string name = j["data"]["name"].get<std::string>();
    }
}

7.3 401 Unauthorized 반복

원인: 토큰 만료, Authorization 헤더 누락

해결:

// ❌ 잘못된 예: 무한 재시도
while (!res.is_success()) {
    res = client.get(path);
}

// ✅ 올바른 예: 401 시 토큰 갱신 후 1회 재시도
if (res.status_code == 401) {
    if (refresh_token()) {
        res = client.get(path, auth_headers());
    }
}

7.4 Content-Type 누락

원인: POST/PUT 시 JSON 본문에 Content-Type 없음

해결:

// ❌ 잘못된 예
client.post("/users", body.dump());

// ✅ 올바른 예
std::map<std::string, std::string> headers;
headers["Content-Type"] = "application/json";
client.post("/users", body.dump(), headers);

7.5 타임아웃 후 소켓 누수

해결: RAII로 소켓 관리

class SecureSocket {
public:
    ~SecureSocket() { if (fd_ >= 0) close(fd_); }
private:
    int fd_ = -1;
};

7.6 인코딩 문제 (한글 등)

해결: Content-Type: application/json; charset=utf-8 확인, UTF-8 해석

7.7 429 Too Many Requests (Rate Limit)

원인: API 호출 빈도 초과. Retry-After 헤더로 대기 시간 제공 가능

해결: res.headers["retry-after"] 확인 후 해당 초만큼 대기 후 1회 재시도. 없으면 60초 기본값 사용.

7.8 Connection refused / Connection timeout

원인: 서버 다운, 방화벽, 잘못된 호스트/포트

해결: connectToHost 실패 시 ApiError{ApiErrorType::ConnectionFailed, ...} 반환. 타임아웃 설정 필수.

7.9 SSL/TLS 인증서 검증 실패

원인: 자체 서명·만료 인증서, 호스트명 불일치

해결: 프로덕션에서는 반드시 검증 활성화. 개발 시에만 DEV_MODE 환경 변수로 완화.


8. 베스트 프랙티스

8.1 요청 전 준비

headers["Content-Type"] = "application/json";
headers["Accept"] = "application/json";
headers["User-Agent"] = "MyApp/1.0";

8.2 안전한 JSON 접근

template<typename T>
std::optional<T> safe_get(const json& j, const std::string& key) {
    if (!j.contains(key)) return std::nullopt;
    try {
        return j[key].get<T>();
    } catch (...) {
        return std::nullopt;
    }
}

8.3 타임아웃 권장값

API 유형연결읽기
내부 API2~5초5~10초
외부 API5~10초10~30초
느린 API10~15초30~60초

8.4 요청 ID·멱등성·Rate Limit

  • X-Request-ID: 디버깅·분산 추적
  • Idempotency-Key: POST/PUT 재시도 시 중복 방지 (결제 API)
  • X-RateLimit-Remaining: 낮으면 요청 속도 조절

9. 프로덕션 패턴

9.1 재시도 (지수 백오프 + 지터)

#include <thread>
#include <chrono>

ApiResponse get_with_retry(RestApiClient& client, const std::string& path,
                           int max_retries = 3) {
    ApiResponse res = client.get(path);
    for (int i = 0; i < max_retries && res.is_retryable(); ++i) {
        int base_ms = 1000 * (1 << i);  // 1s, 2s, 4s
        int jitter = std::rand() % (base_ms / 4 + 1);  // 동시 재시도 폭주 방지
        int delay_ms = base_ms + jitter;
        // 429 시: res.headers["retry-after"] 존중 (초 단위)
        if (res.status_code == 429) {
            auto it = res.headers.find("retry-after");
            if (it != res.headers.end()) {
                try { delay_ms = std::stoi(it->second) * 1000; } catch (...) {}
            }
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
        res = client.get(path);
    }
    return res;
}

9.2 회로 차단기

class CircuitBreaker {
public:
    bool allow_request() {
        if (state_ == State::Open) {
            if (std::chrono::steady_clock::now() > next_try_) {
                state_ = State::HalfOpen;
            } else {
                return false;
            }
        }
        return true;
    }
    void record_success() { failures_ = 0; state_ = State::Closed; }
    void record_failure() {
        ++failures_;
        if (failures_ >= threshold_) {
            state_ = State::Open;
            next_try_ = std::chrono::steady_clock::now() + timeout_;
        }
    }
private:
    enum class State { Closed, Open, HalfOpen };
    State state_ = State::Closed;
    int failures_ = 0;
    int threshold_ = 5;
    std::chrono::seconds timeout_{30};
    std::chrono::steady_clock::time_point next_try_;
};

9.2.1 회로 차단기와 REST 클라이언트 통합

// ✅ 회로 차단기로 보호된 요청
ApiResult<json> get_with_circuit_breaker(RestApiClient& client,
                                         CircuitBreaker& cb,
                                         const std::string& path) {
    if (!cb.allow_request())
        return ApiError{ApiErrorType::HttpError, 0, "Circuit open", ""};
    ApiResponse res = client.get(path);
    if (res.is_success()) {
        cb.record_success();
        try { return json::parse(res.body); }
        catch (const json::exception& e) {
            return ApiError{ApiErrorType::JsonParseError, 0, e.what(), res.body};
        }
    }
    cb.record_failure();
    return ApiError{ApiErrorType::HttpError, res.status_code, res.status_message, res.body};
}

9.3 로깅과 메트릭

ApiResponse get_with_logging(RestApiClient& client, const std::string& path) {
    auto start = std::chrono::steady_clock::now();
    ApiResponse res = client.get(path);
    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::steady_clock::now() - start).count();
    // 로그: path, status_code, elapsed_ms
    return res;
}

9.4 프로덕션 체크리스트

  • Content-Type: application/json
  • 인증 헤더 자동 주입
  • 401 시 토큰 갱신 후 1회 재시도
  • JSON 안전 접근 (contains, value)
  • 타임아웃 적용
  • 429, 5xx에만 재시도
  • 에러 로깅

9.5 완전한 통합 예제 (CRUD + 인증 + 에러 + 재시도 + 회로 차단기)

HTTP 클라이언트(#21-1)와 이 글의 패턴을 결합한 프로덕션 수준 전체 흐름 예제입니다.

// ProductionRestClient: 인증 + 회로 차단기 + 재시도 + 에러 처리 통합
#include <chrono>
#include <thread>
#include <variant>
#include <nlohmann/json.hpp>
#include "rest_api_client.h"
using json = nlohmann::json;

class ProductionRestClient {
public:
    ProductionRestClient(const std::string& base_url,
                         const std::string& bearer_token, int timeout_sec = 10)
        : client_(base_url, timeout_sec), bearer_token_(bearer_token) {}

    ApiResult<json> get(const std::string& path) {
        if (!cb_.allow_request()) return ApiError{ApiErrorType::HttpError, 0, "Circuit open", ""};
        auto headers = auth_headers();
        ApiResponse res = get_with_retry(path, headers);
        return process_response(res);
    }

    ApiResult<json> post(const std::string& path, const json& body) {
        if (!cb_.allow_request()) return ApiError{ApiErrorType::HttpError, 0, "Circuit open", ""};
        auto headers = auth_headers();
        headers["Content-Type"] = "application/json";
        ApiResponse res = client_.post(path, body.dump(), headers);
        return process_response(res);
    }

    ApiResult<json> put(const std::string& path, const json& body) {
        if (!cb_.allow_request()) return ApiError{ApiErrorType::HttpError, 0, "Circuit open", ""};
        auto headers = auth_headers();
        headers["Content-Type"] = "application/json";
        return process_response(client_.put(path, body.dump(), headers));
    }

    ApiResult<json> del(const std::string& path) {
        if (!cb_.allow_request()) return ApiError{ApiErrorType::HttpError, 0, "Circuit open", ""};
        return process_response(client_.del(path, auth_headers()));
    }

private:
    RestApiClient client_;
    std::string bearer_token_;
    CircuitBreaker cb_;

    std::map<std::string, std::string> auth_headers() {
        return {{"Authorization", "Bearer " + bearer_token_},
                {"Accept", "application/json"}, {"User-Agent", "MyApp/1.0"}};
    }

    ApiResponse get_with_retry(const std::string& path,
                               const std::map<std::string, std::string>& headers) {
        ApiResponse res = client_.get(path, headers);
        for (int i = 0; i < 3 && res.is_retryable(); ++i) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1000 * (1 << i)));
            res = client_.get(path, headers);
        }
        return res;
    }

    ApiResult<json> process_response(const ApiResponse& res) {
        if (res.is_success()) {
            cb_.record_success();
            try { return json::parse(res.body); }
            catch (const json::exception& e) {
                return ApiError{ApiErrorType::JsonParseError, 0, e.what(), res.body};
            }
        }
        cb_.record_failure();
        return ApiError{ApiErrorType::HttpError, res.status_code, res.status_message, res.body};
    }
};

// 사용 예: CRUD + ApiResult 패턴
int main() {
    ProductionRestClient client("https://api.example.com", "your-token");
    auto result = client.get("/users/1");
    if (auto* j = std::get_if<json>(&result))
        std::cout << "User: " << (*j)["name"] << "\n";
    else
        std::cerr << "Error: " << std::get<ApiError>(result).message << "\n";

    json body{{"name", "New User"}, {"email", "[email protected]"}};
    result = client.post("/users", body);
    if (auto* j = std::get_if<json>(&result))
        std::cout << "Created id: " << (*j)["id"] << "\n";
    return 0;
}

9.6 간단한 CRUD 테스트 (JSON Placeholder)

// 인증 없이 CRUD 테스트
RestApiClient client("https://jsonplaceholder.typicode.com", 10);
std::map<std::string, std::string> headers{{"Content-Type", "application/json"}};

ApiResponse res = client.get("/users/1");
if (res.is_success()) {
    auto j = nlohmann::json::parse(res.body);
    std::cout << "User: " << j["name"] << "\n";
}
nlohmann::json body{{"title", "Test"}, {"body", "Content"}, {"userId", 1}};
res = client.post("/posts", body.dump(), headers);
if (res.is_success()) {
    auto j = nlohmann::json::parse(res.body);
    std::cout << "Created post id: " << j["id"] << "\n";
}
# 빌드: g++ -std=c++17 -I/path/to/nlohmann main.cpp rest_api_client.cpp -o rest_client

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

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

  • C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
  • C++ 작업 큐 완벽 가이드 | 스레드 풀·워크 스틸링·성능 벤치마크 [#21-2]
  • C++ JSON 처리 | nlohmann/json으로 파싱과 생성하기 [#27-2]

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

C++ REST API, HTTP 클라이언트, JSON 파싱, Bearer 토큰, CRUD, API 인증, nlohmann/json


정리

항목내용
CRUDGET, POST, PUT, DELETE
인증Bearer, API 키, Basic
에러연결 실패, HTTP 4xx/5xx, JSON 파싱
재시도429, 5xx에만 지수 백오프

자주 묻는 질문 (FAQ)

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

A. 외부 API 연동, 마이크로서비스 통신, 결제·인증 서버 호출, 데이터 동기화 등 REST API를 호출하는 모든 C++ 프로젝트에서 활용합니다.

Q. HTTPS는 어떻게 하나요?

A. #21-1 HTTP 클라이언트에서 설명한 대로 OpenSSL 또는 Boost.Beast, libcurl로 TLS를 처리합니다. REST API는 대부분 HTTPS이므로 TLS 지원이 필수입니다.

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

A. HTTP 클라이언트(#21-1)JSON 처리(#27-2)를 먼저 읽으면 좋습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. RFC 7230-7235 (HTTP/1.1), REST API 설계 원칙, nlohmann/json 공식 문서를 참고하세요.

한 줄 요약: HTTP 클라이언트와 JSON을 결합해 REST API를 호출하고, 인증·에러 처리·재시도로 프로덕션 수준으로 끌어올릴 수 있습니다.

다음 글: [C++ 실전 가이드 #22-1] Concepts 기초

이전 글: [C++ 실전 가이드 #21-2] 간단한 작업 큐 구현

참고 자료


관련 글

  • C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴
  • C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
  • C++ 작업 큐 완벽 가이드 | 스레드 풀·워크 스틸링·성능 벤치마크 [#21-2]
  • C++ 디자인 패턴 | Observer·Strategy
  • C++ RAII 완벽 가이드 |