[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 Stampede | SET NX EX, DEL | hiredis/redis-plus-plus |
| 실시간 알림 | Pub/Sub | 별도 연결, SUBSCRIBE/PUBLISH |
| 대량 SET/GET | Pipeline | redisAppendCommand / redis.pipeline() |
| 재고 차감 | Lua EVAL | EVAL 스크립트 |
| 원자적 다중 작업 | MULTI/EXEC 또는 Lua | transaction() / redisCommand |
| 아래 코드는 mermaid를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요. |
flowchart TB
subgraph 문제[실무 문제]
P1[캐시 폭주] --> S1[분산 락]
P2[실시간 알림] --> S2[Pub/Sub]
P3[대량 처리] --> S3[파이프라인]
P4[경쟁 조건] --> S4[Lua]
P5[원자적 작업] --> S5[트랜잭션]
end
목차
- hiredis vs redis-plus-plus 선택
- hiredis 완전 예제
- redis-plus-plus 완전 예제
- Pub/Sub 실시간 메시징
- 파이프라인 대량 처리
- Lua 스크립팅
- 트랜잭션 MULTI/EXEC
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 구현 체크리스트
- 정리
1. hiredis vs redis-plus-plus 선택
비교표
| 항목 | hiredis | redis-plus-plus |
|---|---|---|
| 언어 | C | C++11/14/17 |
| 의존성 | 없음 | hiredis |
| 연결 풀 | 직접 구현 | 내장 |
| API 스타일 | redisCommand, redisReply | STL 스타일 (optional, vector) |
| 에러 처리 | 수동 (ctx->err, reply->type) | 예외 기반 |
| Pub/Sub | 수동 (별도 연결, redisGetReply) | subscriber() API |
| 파이프라인 | redisAppendCommand + redisGetReply | pipeline().set().exec() |
| Lua | redisCommand(“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 권장
-
redisAppendCommand와redisGetReply사이에 다른 명령 금지
Lua
- nil 체크·타입 검증
- KEYS·ARGV 개수 명시
프로덕션
- Health Check (PING) 주기적 수행
- 비밀번호(AUTH) 설정 시 환경 변수 사용
- 캐시 스탬피드 방지 적용
12. 정리
| 기능 | 용도 | hiredis | redis-plus-plus |
|---|---|---|---|
| 기본 GET/SET | 캐싱, 세션 | redisCommand | set/get |
| Pub/Sub | 실시간 알림 | 별도 연결, SUBSCRIBE | subscriber() |
| 파이프라인 | 대량 처리 | redisAppendCommand | pipeline() |
| Lua | 원자적 작업 | EVAL | eval |
| 트랜잭션 | MULTI/EXEC | MULTI/EXEC 수동 | transaction() |
| 핵심 원칙: |
- RAII로 연결·응답 관리
- 바이너리 데이터는
%b사용 - 멀티스레드에서는 연결 풀 또는 스레드당 연결
- 캐시는 반드시 TTL 설정
- Pub/Sub은 발행·구독 연결 분리 이전 글 Redis 클라이언트(#52-2)와 Redis 고급(#52-3)에서 기초·고급을 익혔다면, 이 글의 종합 예제와 프로덕션 패턴을 실무에 적용해 보세요.