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 선택
  2. hiredis 완전 예제
  3. redis-plus-plus 완전 예제
  4. Pub/Sub 실시간 메시징
  5. 파이프라인 대량 처리
  6. Lua 스크립팅
  7. 트랜잭션 MULTI/EXEC
  8. 자주 발생하는 에러와 해결법
  9. 베스트 프랙티스
  10. 프로덕션 패턴
  11. 구현 체크리스트
  12. 정리

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)에서 기초·고급을 익혔다면, 이 글의 종합 예제와 프로덕션 패턴을 실무에 적용해 보세요.


참고 자료


관련 글

  • C++ Redis 클라이언트 완벽 가이드 | hiredis·redis-plus-plus·캐싱·세션·분산락
  • C++ Redis 고급 활용 | Pub/Sub·파이프라인·Lua 스크립팅 완벽 가이드 [#52-3]
  • C++ 데이터베이스 연동 완벽 가이드 | SQLite·PostgreSQL·연결 풀·트랜잭션 [#31-3]
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3