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 |
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 완전 예제
환경 설정
# 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 권장
-
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)에서 기초·고급을 익혔다면, 이 글의 종합 예제와 프로덕션 패턴을 실무에 적용해 보세요.
참고 자료
관련 글
- C++ Redis 클라이언트 완벽 가이드 | hiredis·redis-plus-plus·캐싱·세션·분산락
- C++ Redis 고급 활용 | Pub/Sub·파이프라인·Lua 스크립팅 완벽 가이드 [#52-3]
- C++ 데이터베이스 연동 완벽 가이드 | SQLite·PostgreSQL·연결 풀·트랜잭션 [#31-3]