본문으로 건너뛰기
Previous
Next
C++ Redis 완전 실전 가이드 | hiredis·redis-plus-plus

C++ Redis 완전 실전 가이드 | hiredis·redis-plus-plus

C++ Redis 완전 실전 가이드 | hiredis·redis-plus-plus

이 글의 핵심

C++ Redis 연동 종합: hiredis·redis-plus-plus 설치부터 Pub/Sub 실시간 알림, 파이프라인 대량 처리, Lua 원자적 스크립팅, 트랜잭션까지. 실무 문제 시나리오, 완전한 예제 코드, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴 900줄 분량.

들어가며: “Redis C++로 뭘 어디서부터 해야 할지 모르겠어요”

핵심 질문

"hiredis랑 redis-plus-plus 중 뭘 써야 해요?"
"Pub/Sub·파이프라인·Lua·트랜잭션을 언제 각각 쓰나요?"
"실무에서 자주 겪는 에러와 해결법이 뭔가요?"

이 글은 Redis C++ 연동의 종합 실전 가이드입니다. hiredis·redis-plus-plus 선택 기준부터, Pub/Sub·파이프라인·Lua 스크립팅·트랜잭션까지 완전한 예제와 함께 다룹니다. 실무 문제 시나리오, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴까지 900줄 분량으로 정리합니다. 이 글을 읽으면:

  • hiredis vs redis-plus-plus 선택 기준을 알 수 있습니다.
  • Pub/Sub·파이프라인·Lua·트랜잭션을 완전한 코드로 구현할 수 있습니다.
  • Connection timeout, 메모리 누수, CROSSSLOT 등 흔한 에러를 해결할 수 있습니다.
  • 프로덕션 환경에 맞는 패턴을 적용할 수 있습니다. 요구 환경: C++17 이상, Redis 6.x 이상, hiredis 또는 redis-plus-plus

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

문제 시나리오: 언제 무엇을 쓰나?

시나리오 1: 캐시 미스 시 DB 폭주 (Cache Stampede)

상황: 인기 상품 캐시 TTL 만료 시점에 수천 요청이 동시에 DB 조회
문제: 동일 쿼리가 N번 실행되어 DB 부하 급증
결과: 분산 락(SET NX)으로 한 요청만 DB 조회, 나머지는 대기 후 캐시 히트

시나리오 2: 실시간 주문 알림

상황: 주문 완료 시 연결된 모든 클라이언트에 즉시 푸시
문제: 폴링은 지연·부하, WebSocket만으로는 다중 서버 간 메시지 공유 불가
결과: Redis Pub/Sub으로 채널에 발행 → 여러 서버가 구독·클라이언트에 전달

시나리오 3: 10,000건 캐시 워밍업이 10초 걸림

상황: 서버 기동 시 상품 10,000건을 Redis에 SET
문제: 명령당 1ms RTT라면 10초 지연
결과: 파이프라인으로 200건씩 묶어 전송 → RTT 50회로 50ms 수준

시나리오 4: 재고 차감 시 음수 발생

상황: GET → 감소 → SET으로 재고 차감 시 동시 요청에서 경쟁 조건
문제: 두 요청이 동시에 GET(100) → 각각 SET(99), SET(98) → 최종 98 (1건만 차감했는데 2건 차감됨)
결과: Lua 스크립트로 GET→감소→SET을 원자적으로 실행

시나리오 5: 주문 생성 시 재고+주문+포인트를 한 번에

상황: 재고 차감, 주문 기록, 포인트 적립을 원자적으로 처리해야 함
문제: 중간 실패 시 일부만 반영되어 데이터 불일치
결과: MULTI/EXEC 트랜잭션 또는 Lua로 원자적 실행

시나리오별 기술 선택

시나리오Redis 기능C++ 구현
Cache StampedeSET NX EX, DELhiredis/redis-plus-plus
실시간 알림Pub/Sub별도 연결, SUBSCRIBE/PUBLISH
대량 SET/GETPipelineredisAppendCommand / redis.pipeline()
재고 차감Lua EVALEVAL 스크립트
원자적 다중 작업MULTI/EXEC 또는 Luatransaction() / redisCommand
flowchart TB
    subgraph 문제[실무 문제]
        P1[캐시 폭주] --> S1[분산 락]
        P2[실시간 알림] --> S2[Pub/Sub]
        P3[대량 처리] --> S3[파이프라인]
        P4[경쟁 조건] --> S4[Lua]
        P5[원자적 작업] --> S5[트랜잭션]
    end

1. hiredis vs redis-plus-plus 선택

비교표

항목hiredisredis-plus-plus
언어CC++11/14/17
의존성없음hiredis
연결 풀직접 구현내장
API 스타일redisCommand, redisReplySTL 스타일 (optional, vector)
에러 처리수동 (ctx->err, reply->type)예외 기반
Pub/Sub수동 (별도 연결, redisGetReply)subscriber() API
파이프라인redisAppendCommand + redisGetReplypipeline().set().exec()
LuaredisCommand(“EVAL”, …)redis.eval()
트랜잭션MULTI/EXEC 수동transaction()
용량작음 (~100KB)상대적으로 큼

선택 가이드

  • hiredis: 레거시 C 연동, 최소 의존성, 임베디드, 직접 제어 필요
  • redis-plus-plus: 신규 C++ 프로젝트, Modern C++, 풍부한 API, 연결 풀·예외 처리 내장

2. hiredis 완전 예제

환경 설정

# Ubuntu/Debian
sudo apt-get install libhiredis-dev
# macOS
brew install hiredis
# vcpkg
vcpkg install hiredis

기본 연결 (RAII)

// hiredis_basic.cpp
// 컴파일: g++ -std=c++17 -o hiredis_basic hiredis_basic.cpp -lhiredis
#include <hiredis/hiredis.h>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
struct RedisConnection {
    redisContext* ctx = nullptr;
    RedisConnection(const char* host, int port, int timeout_sec = 5) {
        struct timeval tv = {timeout_sec, 0};
        ctx = redisConnectWithTimeout(host, port, tv);
        if (ctx == nullptr) {
            throw std::runtime_error("Redis 연결 할당 실패");
        }
        if (ctx->err) {
            std::string err = ctx->errstr;
            redisFree(ctx);
            ctx = nullptr;
            throw std::runtime_error("Redis 연결 실패: " + err);
        }
    }
    ~RedisConnection() {
        if (ctx) redisFree(ctx);
    }
    RedisConnection(const RedisConnection&) = delete;
    RedisConnection& operator=(const RedisConnection&) = delete;
};
int main() {
    try {
        RedisConnection conn("127.0.0.1", 6379);
        redisReply* reply = (redisReply*)redisCommand(conn.ctx, "SET user:1 %s", "홍길동");
        if (reply->type == REDIS_REPLY_ERROR) {
            std::cerr << "SET 에러: " << reply->str << "\n";
            freeReplyObject(reply);
            return 1;
        }
        freeReplyObject(reply);
        reply = (redisReply*)redisCommand(conn.ctx, "GET user:1");
        if (reply->type == REDIS_REPLY_STRING) {
            std::cout << "user:1 = " << reply->str << "\n";
        } else if (reply->type == REDIS_REPLY_NIL) {
            std::cout << "user:1 = (없음)\n";
        }
        freeReplyObject(reply);
    } catch (const std::exception& e) {
        std::cerr << "에러: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

RAII 래퍼 (GET/SET/SETNX)

// redis_wrapper.hpp
#pragma once
#include <hiredis/hiredis.h>
#include <optional>
#include <stdexcept>
#include <string>
class RedisClient {
public:
    RedisClient(const std::string& host, int port = 6379, int timeout_sec = 5) {
        struct timeval tv = {timeout_sec, 0};
        ctx_ = redisConnectWithTimeout(host.c_str(), port, tv);
        if (!ctx_) throw std::runtime_error("Redis 연결 할당 실패");
        if (ctx_->err) {
            std::string err = ctx_->errstr;
            redisFree(ctx_);
            ctx_ = nullptr;
            throw std::runtime_error("Redis 연결 실패: " + err);
        }
    }
    ~RedisClient() {
        if (ctx_) redisFree(ctx_);
    }
    RedisClient(const RedisClient&) = delete;
    RedisClient& operator=(const RedisClient&) = delete;
    std::optional<std::string> get(const std::string& key) {
        redisReply* reply = (redisReply*)redisCommand(ctx_, "GET %s", key.c_str());
        if (!reply) return std::nullopt;
        std::optional<std::string> result;
        if (reply->type == REDIS_REPLY_STRING) {
            result = std::string(reply->str, reply->len);
        }
        freeReplyObject(reply);
        return result;
    }
    bool set(const std::string& key, const std::string& value, int ttl_seconds = 0) {
        redisReply* reply;
        if (ttl_seconds > 0) {
            reply = (redisReply*)redisCommand(ctx_, "SET %s %b EX %d",
                                              key.c_str(), value.data(), value.size(), ttl_seconds);
        } else {
            reply = (redisReply*)redisCommand(ctx_, "SET %s %b",
                                              key.c_str(), value.data(), value.size());
        }
        if (!reply) return false;
        bool ok = (reply->type == REDIS_REPLY_STATUS && std::string(reply->str) == "OK");
        freeReplyObject(reply);
        return ok;
    }
    bool setNX(const std::string& key, const std::string& value, int ttl_seconds) {
        redisReply* reply = (redisReply*)redisCommand(ctx_, "SET %s %b NX EX %d",
                                                      key.c_str(), value.data(), value.size(), ttl_seconds);
        if (!reply) return false;
        bool ok = (reply->type == REDIS_REPLY_STATUS && std::string(reply->str) == "OK");
        freeReplyObject(reply);
        return ok;
    }
    bool del(const std::string& key) {
        redisReply* reply = (redisReply*)redisCommand(ctx_, "DEL %s", key.c_str());
        if (!reply) return false;
        bool ok = (reply->type == REDIS_REPLY_INTEGER && reply->integer > 0);
        freeReplyObject(reply);
        return ok;
    }
private:
    redisContext* ctx_ = nullptr;
};

주의: %b는 바이너리 안전. %s는 null 종료 문자열에만 사용.

3. redis-plus-plus 완전 예제

설치

vcpkg install redis-plus-plus

기본 사용 (연결 풀, Hash, Sorted Set)

// redispp_basic.cpp
// vcpkg install redis-plus-plus
#include <sw/redis++/redis++.h>
#include <iostream>
#include <string>
using namespace sw::redis;
int main() {
    try {
        auto redis = Redis("tcp://127.0.0.1:6379");
        redis.set("key", "value");
        auto val = redis.get("key");
        if (val) std::cout << "key = " << *val << "\n";
        redis.set("session:abc", "user_data", std::chrono::seconds(3600));
        redis.hset("user:1001", "name", "김철수");
        redis.hset("user:1001", "email", "[email protected]");
        auto name = redis.hget("user:1001", "name");
        redis.zadd("leaderboard", "player1", 1500.0);
        redis.zadd("leaderboard", "player2", 2300.0);
        std::vector<std::pair<std::string, double>> top3;
        redis.zrevrangebyscore("leaderboard", UnboundedInterval<double>{},
                              std::back_inserter(top3), {.offset = 0, .count = 3});
        for (const auto& [member, score] : top3) {
            std::cout << member << ": " << score << "\n";
        }
    } catch (const Error& e) {
        std::cerr << "Redis 에러: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

4. Pub/Sub 실시간 메시징

hiredis Pub/Sub

SUBSCRIBE는 블로킹이므로 별도 연결·스레드에서 구독합니다.

// pubsub_hiredis.cpp
// 컴파일: g++ -std=c++17 -o pubsub pubsub_hiredis.cpp -lhiredis -lpthread
#include <hiredis/hiredis.h>
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
struct RedisConnection {
    redisContext* ctx = nullptr;
    RedisConnection(const char* host, int port) {
        ctx = redisConnect(host, port);
        if (!ctx || ctx->err) throw std::runtime_error("연결 실패");
    }
    ~RedisConnection() { if (ctx) redisFree(ctx); }
};
void publisher(const std::string& channel) {
    RedisConnection conn("127.0.0.1", 6379);
    for (int i = 0; i < 5; ++i) {
        redisReply* r = (redisReply*)redisCommand(conn.ctx, "PUBLISH %s %s",
            channel.c_str(), ("메시지 " + std::to_string(i)).c_str());
        if (r) {
            std::cout << "[Publish] 수신자 수: " << r->integer << "\n";
            freeReplyObject(r);
        }
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}
void subscriber(const std::string& channel) {
    RedisConnection conn("127.0.0.1", 6379);
    redisReply* r = (redisReply*)redisCommand(conn.ctx, "SUBSCRIBE %s", channel.c_str());
    freeReplyObject(r);
    while (true) {
        if (redisGetReply(conn.ctx, (void**)&r) != REDIS_OK) break;
        if (r->type == REDIS_REPLY_ARRAY && r->elements >= 3) {
            std::string type = r->element[0]->str;
            std::string msg = r->element[2]->str;
            std::cout << "[Sub] " << type << ": " << msg << "\n";
        }
        freeReplyObject(r);
    }
}
int main() {
    std::string channel = "channel:orders";
    std::thread sub(subscriber, channel);
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::thread pub(publisher, channel);
    pub.join();
    sub.join();
    return 0;
}

redis-plus-plus Pub/Sub

// pubsub_redispp.cpp
#include <sw/redis++/redis++.h>
#include <iostream>
#include <thread>
#include <chrono>
using namespace sw::redis;
void run_publisher(const std::string& channel) {
    auto redis = Redis("tcp://127.0.0.1:6379");
    for (int i = 0; i < 5; ++i) {
        auto count = redis.publish(channel, "주문 #" + std::to_string(i + 100));
        std::cout << "[Publish] 수신자: " << count << "\n";
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}
void run_subscriber(const std::string& channel) {
    auto sub = Redis("tcp://127.0.0.1:6379").subscriber();
    sub.on_message([channel](std::string ch, std::string msg) {
        std::cout << "[Sub] " << ch << ": " << msg << "\n";
    });
    sub.subscribe(channel);
    sub.consume();
}
int main() {
    std::string channel = "channel:orders";
    std::thread sub(run_subscriber, channel);
    std::this_thread::sleep_for(std::chrono::milliseconds(500));
    std::thread pub(run_publisher, channel);
    pub.join();
    sub.join();
    return 0;
}

주의: SUBSCRIBE한 연결에서는 GET, SET 등 일반 명령 사용 불가. 발행·구독은 독립된 연결로 처리.

5. 파이프라인 대량 처리

hiredis 파이프라인

// pipeline_hiredis.cpp
#include <hiredis/hiredis.h>
#include <iostream>
#include <chrono>
#include <string>
struct RedisConnection {
    redisContext* ctx = nullptr;
    RedisConnection(const char* host, int port) {
        ctx = redisConnect(host, port);
        if (!ctx || ctx->err) throw std::runtime_error("연결 실패");
    }
    ~RedisConnection() { if (ctx) redisFree(ctx); }
};
void pipeline_batch_set(redisContext* ctx, int count) {
    for (int i = 0; i < count; ++i) {
        std::string key = "key:" + std::to_string(i);
        std::string val = "value:" + std::to_string(i);
        redisAppendCommand(ctx, "SET %s %s", key.c_str(), val.c_str());
    }
    redisReply* reply = nullptr;
    for (int i = 0; i < count; ++i) {
        if (redisGetReply(ctx, (void**)&reply) != REDIS_OK) break;
        freeReplyObject(reply);
    }
}
void pipeline_batch_get(redisContext* ctx, int count) {
    for (int i = 0; i < count; ++i) {
        std::string key = "key:" + std::to_string(i);
        redisAppendCommand(ctx, "GET %s", key.c_str());
    }
    redisReply* reply = nullptr;
    for (int i = 0; i < count; ++i) {
        if (redisGetReply(ctx, (void**)&reply) != REDIS_OK) break;
        freeReplyObject(reply);
    }
}
int main() {
    RedisConnection conn("127.0.0.1", 6379);
    auto start = std::chrono::high_resolution_clock::now();
    pipeline_batch_set(conn.ctx, 1000);
    pipeline_batch_get(conn.ctx, 1000);
    auto end = std::chrono::high_resolution_clock::now();
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "파이프라인 2000건: " << ms << " ms\n";
    return 0;
}

redis-plus-plus 파이프라인

// pipeline_redispp.cpp
#include <sw/redis++/redis++.h>
#include <iostream>
#include <chrono>
using namespace sw::redis;
int main() {
    auto redis = Redis("tcp://127.0.0.1:6379");
    auto pipe = redis.pipeline();
    for (int i = 0; i < 1000; ++i) {
        pipe.set("key:" + std::to_string(i), "value:" + std::to_string(i));
    }
    auto replies = pipe.exec();
    std::cout << "파이프라인 SET 완료: " << replies.size() << " 건\n";
    auto pipe2 = redis.pipeline();
    for (int i = 0; i < 1000; ++i) {
        pipe2.get("key:" + std::to_string(i));
    }
    auto get_replies = pipe2.exec();
    std::cout << "파이프라인 GET 완료: " << get_replies.size() << " 건\n";
    return 0;
}

6. Lua 스크립팅

재고 차감 Lua 스크립트

-- decrement_stock.lua
-- KEYS[1]: 재고 키
-- ARGV[1]: 차감 수량
local current = redis.call('GET', KEYS[1])
if not current then
    return -1
end
local stock = tonumber(current)
local amount = tonumber(ARGV[1])
if stock < amount then
    return -2
end
redis.call('SET', KEYS[1], stock - amount)
return stock - amount

hiredis EVAL

// lua_hiredis.cpp
#include <hiredis/hiredis.h>
#include <iostream>
const char* DECREMENT_STOCK = R"(
local current = redis.call('GET', KEYS[1])
if not current then return -1 end
local stock = tonumber(current)
local amount = tonumber(ARGV[1])
if stock < amount then return -2 end
redis.call('SET', KEYS[1], stock - amount)
return stock - amount
)";
int main() {
    redisContext* ctx = redisConnect("127.0.0.1", 6379);
    if (!ctx || ctx->err) return 1;
    redisReply* r = (redisReply*)redisCommand(ctx, "SET stock 100");
    freeReplyObject(r);
    r = (redisReply*)redisCommand(ctx, "EVAL %s 1 stock 5", DECREMENT_STOCK);
    if (r && r->type == REDIS_REPLY_INTEGER) {
        std::cout << "차감 후 재고: " << r->integer << "\n";
    }
    freeReplyObject(r);
    redisFree(ctx);
    return 0;
}

redis-plus-plus EVAL

// lua_redispp.cpp
#include <sw/redis++/redis++.h>
#include <iostream>
using namespace sw::redis;
int main() {
    auto redis = Redis("tcp://127.0.0.1:6379");
    redis.set("stock", "100");
    std::string script = R"(
        local current = redis.call('GET', KEYS[1])
        if not current then return -1 end
        local stock = tonumber(current)
        local amount = tonumber(ARGV[1])
        if stock < amount then return -2 end
        redis.call('SET', KEYS[1], stock - amount)
        return stock - amount
    )";
    auto result = redis.eval<long long>(script, {"stock"}, {"5"});
    std::cout << "차감 후 재고: " << result << "\n";
    return 0;
}

분산 락 해제 Lua (같은 토큰일 때만 DEL)

-- unlock.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end
// C++에서 Lua 락 해제
const char* UNLOCK = "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
redisReply* r = (redisReply*)redisCommand(ctx, "EVAL %s 1 lock:resource %s", UNLOCK, token.c_str());
freeReplyObject(r);

7. 트랜잭션 MULTI/EXEC

hiredis 트랜잭션

// transaction_hiredis.cpp
#include <hiredis/hiredis.h>
#include <iostream>
int main() {
    redisContext* ctx = redisConnect("127.0.0.1", 6379);
    if (!ctx || ctx->err) return 1;
    redisReply* r;
    r = (redisReply*)redisCommand(ctx, "MULTI");
    freeReplyObject(r);
    r = (redisReply*)redisCommand(ctx, "SET a 1");
    freeReplyObject(r);
    r = (redisReply*)redisCommand(ctx, "SET b 2");
    freeReplyObject(r);
    r = (redisReply*)redisCommand(ctx, "EXEC");
    if (r->type == REDIS_REPLY_ARRAY) {
        for (size_t i = 0; i < r->elements; ++i) {
            std::cout << "결과 " << i << ": " << r->element[i]->str << "\n";
        }
    }
    freeReplyObject(r);
    redisFree(ctx);
    return 0;
}

redis-plus-plus 트랜잭션

// transaction_redispp.cpp
#include <sw/redis++/redis++.h>
#include <iostream>
using namespace sw::redis;
int main() {
    auto redis = Redis("tcp://127.0.0.1:6379");
    auto tx = redis.transaction();
    tx.set("a", "1");
    tx.set("b", "2");
    auto replies = tx.exec();
    std::cout << "트랜잭션 완료: " << replies.size() << " 건\n";
    return 0;
}

주의: Redis 트랜잭션은 롤백을 지원하지 않습니다. 원자적 롤백이 필요하면 Lua를 사용하세요.

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

에러 1: Connection timeout / Connection refused

증상: ctx->errstr에 “Connection refused” 또는 “Connection timed out” 원인: Redis 미실행, 잘못된 호스트/포트, 방화벽 해결법:

# Redis 서버 확인
redis-cli ping
# PONG 응답이면 정상
// ✅ 타임아웃 설정
struct timeval tv = {5, 0};
redisContext* ctx = redisConnectWithTimeout("127.0.0.1", 6379, tv);

에러 2: freeReplyObject 누락으로 메모리 누수

증상: 장시간 실행 시 메모리 사용량 증가

// ❌ 메모리 누수
redisReply* reply = (redisReply*)redisCommand(ctx, "GET key");
std::string result = reply->str;
// freeReplyObject(reply) 누락!
// ✅ RAII 또는 항상 freeReplyObject 호출
redisReply* reply = (redisReply*)redisCommand(ctx, "GET key");
if (reply) {
    std::string result(reply->str, reply->len);
    freeReplyObject(reply);
}

에러 3: %s vs %b 혼동

증상: 값에 null 문자 포함 시 잘림

// ❌ 바이너리 데이터 잘림
std::string data = "hello\0world";
redisCommand(ctx, "SET key %s", data.c_str());  // "hello"만 저장
// ✅ 바이너리 안전
redisCommand(ctx, "SET key %b", data.data(), data.size());

에러 4: SUBSCRIBE 연결에서 GET/SET 시도

증상: (error) ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed 해결법: 발행·구독을 별도 연결로 처리

에러 5: 파이프라인 중간에 다른 명령

증상: 응답 순서 꼬임

// ❌ 잘못된 사용
redisAppendCommand(ctx, "SET a 1");
redisCommand(ctx, "GET b");  // 파이프라인 깨짐
redisGetReply(ctx, &reply);
// ✅ 파이프라인은 한 번에
redisAppendCommand(ctx, "SET a 1");
redisAppendCommand(ctx, "SET b 2");
redisGetReply(ctx, &reply); freeReplyObject(reply);
redisGetReply(ctx, &reply); freeReplyObject(reply);

에러 6: Lua nil 비교

증상: attempt to compare nil with number

-- ❌ nil 체크 없음
local current = redis.call('GET', KEYS[1])
local stock = tonumber(current)  -- nil이면 에러
-- ✅ nil 체크
local current = redis.call('GET', KEYS[1])
if not current then return -1 end
local stock = tonumber(current)

에러 7: CROSSSLOT (Redis Cluster)

증상: Keys in request don't hash to the same slot 해결법: {hash_tag}로 같은 슬롯 보장

// ❌ 다른 슬롯
cluster.mget({"user:1:name", "user:2:name"});
// ✅ 같은 슬롯
cluster.mget({"user:{1}:name", "user:{1}:age"});

에러 8: NOAUTH Authentication required

해결법:

// hiredis
redisCommand(ctx, "AUTH %s", password);
// redis-plus-plus
auto redis = Redis("tcp://127.0.0.1:6379", Options{}.password("mypassword"));

에러 9: 멀티스레드에서 연결 공유

증상: 간헐적 크래시, 잘못된 응답 원인: hiredis redisContext스레드 안전하지 않음

// ✅ 스레드당 연결 또는 연결 풀
void worker() {
    thread_local RedisClient redis("127.0.0.1", 6379);
    redis.get("key");
}

9. 베스트 프랙티스

1. RAII로 리소스 관리

struct ReplyGuard {
    redisReply* r;
    ~ReplyGuard() { if (r) freeReplyObject(r); }
};

2. 연결 풀 사용

// redis-plus-plus는 기본이 연결 풀
ConnectionPoolOptions pool_opts;
pool_opts.size = 10;
auto redis = Redis(opts, pool_opts);

3. 파이프라인 배치 크기 100~500

const int BATCH_SIZE = 200;
for (int offset = 0; offset < total; offset += BATCH_SIZE) {
    auto pipe = redis.pipeline();
    for (int i = 0; i < BATCH_SIZE && offset + i < total; ++i) {
        pipe.set(keys[offset + i], values[offset + i]);
    }
    pipe.exec();
}

4. 캐시 TTL 필수

redis.set("cache:product:123", json, 300);  // 5분 TTL

5. 키 설계 — 짧고 일관되게

// ❌ 긴 키
"user:session:cache:data:12345:profile:settings"
// ✅ 짧고 일관된 키
"u:12345:prof"

6. 대량 조회 시 MGET

// ❌ N번 왕복
for (int i = 0; i < 100; ++i) redis.get("key:" + std::to_string(i));
// ✅ MGET 1번
redis.mget({"key:1", "key:2", ..., "key:100"});

10. 프로덕션 패턴

패턴 1: Health Check 및 재연결

bool RedisClient::ping() {
    redisReply* r = (redisReply*)redisCommand(ctx_, "PING");
    if (!r) return false;
    bool ok = (r->type == REDIS_REPLY_STATUS && std::string(r->str) == "PONG");
    freeReplyObject(r);
    return ok;
}

패턴 2: 캐시 스탬피드 방지

std::string getWithStampedePrevention(RedisClient& redis,
                                      const std::string& key,
                                      std::function<std::string()> fetcher,
                                      int ttl = 300) {
    auto cached = redis.get(key);
    if (cached) return *cached;
    std::string lockKey = "lock:" + key;
    std::string lockVal = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
    if (redis.setNX(lockKey, lockVal, 10)) {
        std::string data = fetcher();
        redis.set(key, data, ttl);
        redis.del(lockKey);
        return data;
    }
    for (int i = 0; i < 20; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        cached = redis.get(key);
        if (cached) return *cached;
    }
    return fetcher();
}

패턴 3: Pub/Sub 구독자 재연결

void run_subscriber_with_reconnect(Redis& redis, const std::string& channel) {
    while (true) {
        try {
            auto sub = redis.subscriber();
            sub.on_message( {
                std::cout << ch << ": " << msg << "\n";
            });
            sub.subscribe(channel);
            sub.consume();
        } catch (const std::exception& e) {
            std::cerr << "구독 재연결: " << e.what() << "\n";
            std::this_thread::sleep_for(std::chrono::seconds(5));
        }
    }
}

패턴 4: 설정 외부화

struct RedisConfig {
    std::string host = "127.0.0.1";
    int port = 6379;
    std::string password;
};
RedisConfig loadFromEnv() {
    RedisConfig c;
    if (const char* h = std::getenv("REDIS_HOST")) c.host = h;
    if (const char* p = std::getenv("REDIS_PORT")) c.port = std::stoi(p);
    if (const char* pw = std::getenv("REDIS_PASSWORD")) c.password = pw;
    return c;
}

패턴 5: WATCH 낙관적 락

redisReply* r = (redisReply*)redisCommand(ctx, "WATCH stock");
freeReplyObject(r);
r = (redisReply*)redisCommand(ctx, "MULTI");
// ....명령 추가 ...
r = (redisReply*)redisCommand(ctx, "EXEC");
if (r->type == REDIS_REPLY_NIL) {
    // 값이 변경됨 → 재시도
}

11. 구현 체크리스트

환경 설정

  • Redis 서버 실행 확인 (redis-cli ping)
  • hiredis 또는 redis-plus-plus 설치
  • CMake/vcpkg 연동

연결 및 기본 사용

  • redisConnectWithTimeout으로 타임아웃 설정
  • RAII로 redisContext/redisReply 관리
  • freeReplyObject 누락 없이 호출

Pub/Sub

  • 발행·구독을 별도 연결로 처리
  • 구독 연결 끊김 시 재연결 로직

파이프라인

  • 배치 크기 100~500 권장
  • redisAppendCommandredisGetReply 사이에 다른 명령 금지

Lua

  • nil 체크·타입 검증
  • KEYS·ARGV 개수 명시

프로덕션

  • Health Check (PING) 주기적 수행
  • 비밀번호(AUTH) 설정 시 환경 변수 사용
  • 캐시 스탬피드 방지 적용

12. 정리

기능용도hiredisredis-plus-plus
기본 GET/SET캐싱, 세션redisCommandset/get
Pub/Sub실시간 알림별도 연결, SUBSCRIBEsubscriber()
파이프라인대량 처리redisAppendCommandpipeline()
Lua원자적 작업EVALeval()
트랜잭션MULTI/EXECMULTI/EXEC 수동transaction()
핵심 원칙:
  1. RAII로 연결·응답 관리
  2. 바이너리 데이터%b 사용
  3. 멀티스레드에서는 연결 풀 또는 스레드당 연결
  4. 캐시는 반드시 TTL 설정
  5. Pub/Sub은 발행·구독 연결 분리 이전 글 Redis 클라이언트(#52-2)Redis 고급(#52-3)에서 기초·고급을 익혔다면, 이 글의 종합 예제와 프로덕션 패턴을 실무에 적용해 보세요.


자주 묻는 질문 (FAQ)

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

A. C++ Redis 연동 종합: hiredis·redis-plus-plus 설치부터 Pub/Sub 실시간 알림, 파이프라인 대량 처리, Lua 원자적 스크립팅, 트랜잭션까지. 실무 문제 시나리오, 완전한 예제 코드, … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

참고 자료


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ Redis 완전 실전 가이드 | hiredis·redis-plus-plus」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ Redis 완전 실전 가이드 | hiredis·redis-plus-plus」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


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

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


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

C++, Redis, hiredis, redis-plus-plus, Pub-Sub, 파이프라인, Lua 등으로 검색하시면 이 글이 도움이 됩니다.