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 각각에 맞는 헤더·본문 형식이 혼란
해결책:
- 완전한 REST API 클라이언트: CRUD 메서드, JSON 직렬화/역직렬화, 통일된 응답 타입
- 인증 처리: Bearer 토큰, API 키, Basic 인증을 헤더에 자동 주입
- 에러 분류: 연결 실패, 타임아웃, HTTP 4xx/5xx, JSON 파싱 에러 구분
- 재시도·회로 차단기: 일시적 실패에만 재시도, 연속 실패 시 요청 중단
목표:
- CRUD 작업 완전 예제 (GET, POST, PUT, DELETE)
- 인증 (Bearer, API 키, Basic)
- 에러 처리 (연결 실패, HTTP 에러, JSON 파싱)
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스와 프로덕션 패턴
이 글을 읽으면:
- HTTP 클라이언트와 JSON을 결합해 REST API를 호출할 수 있습니다.
- 인증 헤더를 추가하고, 에러를 분류해 처리할 수 있습니다.
- 프로덕션 수준의 재시도·로깅·모니터링 패턴을 적용할 수 있습니다.
목차
- 문제 시나리오와 해결 방향
- REST API 클라이언트 아키텍처
- 기본 REST API 클라이언트 구현
- CRUD 작업 완전 예제
- 인증 처리
- 에러 처리
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
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 유형 | 연결 | 읽기 |
|---|---|---|
| 내부 API | 2~5초 | 5~10초 |
| 외부 API | 5~10초 | 10~30초 |
| 느린 API | 10~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
정리
| 항목 | 내용 |
|---|---|
| CRUD | GET, 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] 간단한 작업 큐 구현
참고 자료
- cpp-series-21-1 HTTP 클라이언트
- cpp-series-27-2 JSON nlohmann
- RFC 7230 - HTTP/1.1
- nlohmann/json
관련 글
- C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴
- C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
- C++ 작업 큐 완벽 가이드 | 스레드 풀·워크 스틸링·성능 벤치마크 [#21-2]
- C++ 디자인 패턴 | Observer·Strategy
- C++ RAII 완벽 가이드 |