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

[2026] 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
아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
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 완전 예제

환경 설정

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

기본 연결 (RAII)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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", "kim@example.com");
        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는 블로킹이므로 별도 연결·스레드에서 구독합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 파이프라인

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 파이프라인

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 스크립트

아래 코드는 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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)

아래 코드는 lua를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- unlock.lua
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
else
    return 0
end

다음은 간단한 cpp 코드 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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 트랜잭션

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 트랜잭션

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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 누락으로 메모리 누수

증상: 장시간 실행 시 메모리 사용량 증가 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 메모리 누수
redisReply* reply = (redisReply*)redisCommand(ctx, "GET key");
std::string result = reply->str;
// freeReplyObject(reply) 누락!

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ 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 문자 포함 시 잘림 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 바이너리 데이터 잘림
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: 파이프라인 중간에 다른 명령

증상: 응답 순서 꼬임 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 사용
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 아래 코드는 lua를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- ❌ 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}로 같은 슬롯 보장 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

에러 8: NOAUTH Authentication required

해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

증상: 간헐적 크래시, 잘못된 응답 원인: hiredis redisContext스레드 안전하지 않음 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

9. 베스트 프랙티스

1. RAII로 리소스 관리

다음은 간단한 cpp 코드 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

2. 연결 풀 사용

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

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

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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. 키 설계 — 짧고 일관되게

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

6. 대량 조회 시 MGET

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 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 및 재연결

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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: 캐시 스탬피드 방지

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 구독자 재연결

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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: 설정 외부화

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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 낙관적 락

아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

참고 자료


관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3