[2026] C++ Redis 클라이언트 완벽 가이드 | hiredis·redis-plus-plus·캐싱·세션·분산락
이 글의 핵심
C++에서 Redis 연동: hiredis·redis-plus-plus 설치·연결, GET/SET·Hash·분산락 실전 코드. Connection timeout·메모리 누수 등 흔한 에러 해결, 성능 최적화, 프로덕션 패턴까지 900줄 분량으로 다룹니다.
들어가며: C++에서 Redis를 왜 쓰나요?
핵심 질문
"DB 쿼리가 느려서 API 응답이 500ms 넘어가요."
"세션을 여러 서버에서 공유해야 하는데, 어떻게 하죠?"
"재고 차감을 분산 환경에서 안전하게 하려면?"
Redis는 인메모리 Key-Value 스토어로, C++ 서버에서 캐싱, 세션 저장, 분산 락, Rate Limiting 등에 널리 쓰입니다. 이 글은 hiredis(C 기반, 경량)와 redis-plus-plus(Modern C++, 풍부한 API)를 사용해 Redis를 C++에서 연동하는 완전한 가이드입니다. 이 글을 읽으면:
- hiredis·redis-plus-plus 설치 및 기본 연결을 할 수 있습니다.
- GET/SET, Hash, TTL, 분산 락 등 실전 패턴을 구현할 수 있습니다.
- Connection timeout, 메모리 누수 등 흔한 에러를 해결할 수 있습니다.
- 성능 최적화와 프로덕션 배포 패턴을 적용할 수 있습니다. 요구 환경: C++17 이상, Redis 6.x 이상 권장
실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
문제 시나리오
시나리오 1: DB 쿼리 병목으로 API 지연
"상품 상세 API가 DB 조회 때문에 300~500ms 걸려요."
"같은 상품을 매번 조회하는데, 캐시가 없어요."
상황: 웹 API에서 상품 정보를 DB에서 매번 조회합니다. 동일 상품에 대한 반복 요청이 많아 DB 부하와 응답 지연이 발생합니다. 해결 포인트: Redis에 Cache-Aside 패턴으로 상품 JSON을 캐싱. TTL 300초 설정으로 DB 부하를 90% 이상 줄일 수 있습니다.
시나리오 2: 로드밸런서 뒤 다중 서버 세션
"서버를 3대로 늘렸는데, 로그인 후 다른 서버로 가면 세션이 사라져요."
상황: 세션을 프로세스 메모리에 저장하면, 요청이 다른 서버로 가면 세션을 찾을 수 없습니다. 해결 포인트: Redis에 세션 데이터(Hash 또는 JSON)를 저장. 모든 서버가 동일 Redis를 바라보면 세션 공유가 됩니다.
시나리오 3: 재고 차감 경쟁 조건
"여러 서버에서 동시에 재고를 차감하는데, 음수로 떨어질 때가 있어요."
상황: 분산 환경에서 SELECT ....FOR UPDATE만으로는 부족하고, Redis 분산 락(SET NX EX)으로 리소스 접근을 직렬화해야 합니다.
해결 포인트: Redis SET key value NX EX ttl로 락 획득, Lua 스크립트로 같은 토큰일 때만 락 해제하여 안전하게 구현합니다.
시나리오 4: 실시간 순위표
"게임 점수 순위를 실시간으로 보여줘야 해요."
상황: DB ORDER BY score DESC LIMIT 100은 부하가 크고, 실시간 반영이 어렵습니다.
해결 포인트: Redis Sorted Set(ZADD, ZREVRANGE)로 점수·멤버를 저장. O(log N)으로 순위 조회가 가능합니다.
시나리오별 권장 패턴
| 시나리오 | Redis 자료구조 | C++ 클라이언트 |
|---|---|---|
| API 캐싱 | String (SET/GET) | hiredis, redis-plus-plus |
| 세션 저장 | Hash 또는 String | hiredis, redis-plus-plus |
| 분산 락 | String (SET NX EX) | hiredis, redis-plus-plus |
| 순위표 | Sorted Set | redis-plus-plus (편의 API) |
목차
- 환경 설정 및 설치
- hiredis 기본 연결 및 GET/SET
- redis-plus-plus Modern C++ 클라이언트
- 완전한 Redis C++ 예제
- 자주 발생하는 에러와 해결법
- 성능 최적화 팁
- 프로덕션 패턴
- 구현 체크리스트
1. 환경 설정 및 설치
Redis 서버 실행
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Docker로 Redis 실행 (권장)
docker run -d -p 6379:6379 redis:7-alpine
# 또는 로컬 설치 후
redis-server
hiredis 설치
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# Ubuntu/Debian
sudo apt-get install libhiredis-dev
# macOS (Homebrew)
brew install hiredis
# vcpkg
vcpkg install hiredis
redis-plus-plus 설치
redis-plus-plus는 hiredis 위에 구축된 C++ 래퍼입니다. 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# vcpkg (권장)
vcpkg install redis-plus-plus
# 또는 소스 빌드
git clone https://github.com/sewenew/redis-plus-plus.git
cd redis-plus-plus
mkdir build && cd build
cmake ...-DCMAKE_BUILD_TYPE=Release
make && sudo make install
CMake 연동 예시
아래 코드는 cmake를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# CMakeLists.txt
find_package(PkgConfig REQUIRED)
pkg_check_modules(HIREDIS REQUIRED hiredis)
add_executable(redis_demo main.cpp)
target_link_libraries(redis_demo PRIVATE ${HIREDIS_LIBRARIES})
target_include_directories(redis_demo PRIVATE ${HIREDIS_INCLUDE_DIRS})
2. hiredis 기본 연결 및 GET/SET
아키텍처 다이어그램
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart TB
subgraph App[C++ 애플리케이션]
Main[main]
Client[RedisClient]
end
subgraph Hiredis[hiredis]
Ctx[redisContext]
Cmd[redisCommand]
Reply[redisReply]
end
subgraph Redis[Redis 서버]
Store[(Key-Value Store)]
end
Main --> Client
Client --> Ctx
Client --> Cmd
Cmd --> Reply
Ctx -->|TCP 6379| Store
기본 연결 (RAII)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// redis_basic.cpp
// 컴파일: g++ -std=c++17 -o redis_basic redis_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);
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);
// SET key value
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);
// GET key
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);
// SET key value EX seconds (TTL)
reply = (redisReply*)redisCommand(conn.ctx, "SET cache:product:123 %s EX 300",
"{\"name\":\"상품A\",\"price\":9900}");
freeReplyObject(reply);
} catch (const std::exception& e) {
std::cerr << "에러: " << e.what() << "\n";
return 1;
}
return 0;
}
RAII 래퍼 클래스 (GET/SET/DEL/SETNX)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// redis_wrapper.hpp
#pragma once
#include <hiredis/hiredis.h>
#include <memory>
#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_);
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 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;
}
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;
}
std::optional<long long> incr(const std::string& key) {
redisReply* reply = (redisReply*)redisCommand(ctx_, "INCR %s", key.c_str());
if (!reply || reply->type != REDIS_REPLY_INTEGER) {
if (reply) freeReplyObject(reply);
return std::nullopt;
}
long long val = reply->integer;
freeReplyObject(reply);
return val;
}
bool expire(const std::string& key, int seconds) {
redisReply* reply = (redisReply*)redisCommand(ctx_, "EXPIRE %s %d", key.c_str(), seconds);
if (!reply) return false;
bool ok = (reply->type == REDIS_REPLY_INTEGER && reply->integer == 1);
freeReplyObject(reply);
return ok;
}
private:
redisContext* ctx_ = nullptr;
};
주의: %b는 바이너리 안전(binary-safe) 포맷으로, value.data()와 value.size()를 사용합니다. %s는 null 종료 문자열에만 사용하세요.
3. redis-plus-plus Modern C++ 클라이언트
연결 풀 및 STL 스타일 API
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// redis_plus_plus_demo.cpp
// vcpkg install redis-plus-plus 후 컴파일
#include <sw/redis++/redis++.h>
#include <iostream>
#include <string>
using namespace sw::redis;
int main() {
try {
// 연결 풀 생성 (기본 1~10 연결)
auto redis = Redis("tcp://127.0.0.1:6379");
// SET / GET
redis.set("key", "value");
auto val = redis.get("key");
if (val) {
std::cout << "key = " << *val << "\n";
}
// TTL과 함께 SET
redis.set("session:abc", "user_data", std::chrono::seconds(3600));
// Hash
redis.hset("user:1001", "name", "김철수");
redis.hset("user:1001", "email", "kim@example.com");
auto name = redis.hget("user:1001", "name");
// Sorted Set (순위표)
redis.zadd("leaderboard", "player1", 1500.0);
redis.zadd("leaderboard", "player2", 2300.0);
redis.zadd("leaderboard", "player3", 1800.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;
}
redis-plus-plus vs hiredis 비교
| 항목 | hiredis | redis-plus-plus |
|---|---|---|
| 언어 | C | C++11/14/17 |
| 의존성 | 없음 (hiredis만) | hiredis |
| 연결 풀 | 직접 구현 | 내장 |
| STL 호환 | 없음 | optional, vector 등 |
| 설치 | 간단 | vcpkg 또는 빌드 |
| 용량 | 작음 | 상대적으로 큼 |
4. 완전한 Redis C++ 예제
예제 1: Cache-Aside API 캐싱
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// cache_aside.cpp
#include "redis_wrapper.hpp"
#include <functional>
#include <string>
std::string getCachedOrFetch(RedisClient& redis,
const std::string& cacheKey,
std::function<std::string()> fetcher,
int ttl = 300) {
auto cached = redis.get(cacheKey);
if (cached) return *cached;
std::string data = fetcher();
redis.set(cacheKey, data, ttl);
return data;
}
// 사용 예
// std::string productJson = getCachedOrFetch(redis, "product:123",
// { return db.queryProduct(123).toJson(); }, 300);
예제 2: 세션 저장 (Hash)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// session_store.cpp
#include <hiredis/hiredis.h>
#include <optional>
#include <string>
class SessionStore {
public:
SessionStore(redisContext* ctx) : ctx_(ctx) {}
void setSession(const std::string& sessionId,
const std::string& userId,
const std::string& data,
int ttlSeconds = 3600) {
std::string key = "session:" + sessionId;
redisReply* r;
r = (redisReply*)redisCommand(ctx_, "HSET %s user_id %s data %s",
key.c_str(), userId.c_str(), data.c_str());
freeReplyObject(r);
r = (redisReply*)redisCommand(ctx_, "EXPIRE %s %d", key.c_str(), ttlSeconds);
freeReplyObject(r);
}
std::optional<std::string> getSession(const std::string& sessionId) {
std::string key = "session:" + sessionId;
redisReply* r = (redisReply*)redisCommand(ctx_, "HGET %s data", key.c_str());
if (!r || r->type != REDIS_REPLY_STRING) {
if (r) freeReplyObject(r);
return std::nullopt;
}
std::string result(r->str, r->len);
freeReplyObject(r);
return result;
}
void extendSession(const std::string& sessionId, int ttlSeconds = 3600) {
std::string key = "session:" + sessionId;
redisReply* r = (redisReply*)redisCommand(ctx_, "EXPIRE %s %d", key.c_str(), ttlSeconds);
freeReplyObject(r);
}
private:
redisContext* ctx_;
};
예제 3: 분산 락 (SET NX EX)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// distributed_lock.cpp
#include "redis_wrapper.hpp"
#include <chrono>
#include <string>
#include <thread>
class DistributedLock {
public:
DistributedLock(RedisClient& redis, const std::string& resource, int ttlSeconds = 10)
: redis_(redis), resource_(resource), key_("lock:" + resource), ttl_(ttlSeconds) {}
bool tryLock() {
token_ = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
return redis_.setNX(key_, token_, ttl_);
}
void unlock() {
redis_.del(key_);
}
template <typename Func>
bool withLock(Func&& f) {
if (!tryLock()) return false;
bool ok = false;
try {
f();
ok = true;
} catch (...) {}
unlock();
return ok;
}
private:
RedisClient& redis_;
std::string resource_;
std::string key_;
int ttl_;
std::string token_;
};
// 사용 예
// DistributedLock lock(redis, "inventory:product:123", 5);
// if (lock.tryLock()) {
// // 재고 차감 로직
// lock.unlock();
// }
예제 4: Rate Limiter (고정 윈도우 — INCR+EXPIRE)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// rate_limiter.cpp
// 고정 윈도우: INCR로 카운트 증가, 첫 요청 시 EXPIRE로 TTL 설정
#include "redis_wrapper.hpp"
#include <string>
class RateLimiter {
public:
RateLimiter(RedisClient& redis, int maxRequests, int windowSeconds)
: redis_(redis), max_(maxRequests), window_(windowSeconds) {}
bool allow(const std::string& clientKey) {
std::string redisKey = "ratelimit:" + clientKey;
auto countOpt = redis_.incr(redisKey);
if (!countOpt) return false;
if (*countOpt == 1) {
redis_.expire(redisKey, window_);
}
return *countOpt <= static_cast<long long>(max_);
}
private:
RedisClient& redis_;
int max_;
int window_;
};
참고: 슬라이딩 윈도우가 필요하면 ZADD+ZREMRANGEBYSCORE+ZCARD 조합을 사용하세요. Redis 고급 활용(#52-3)에서 Lua로 원자적 처리 예시를 다룹니다.
5. 자주 발생하는 에러와 해결법
에러 1: Connection timeout / Connection refused
증상: redisConnect 실패, ctx->errstr에 “Connection refused” 또는 “Connection timed out”
원인:
- Redis 서버가 실행 중이 아님
- 잘못된 호스트/포트
- 방화벽 차단
- Redis가
bind 127.0.0.1만 허용하는데 외부 IP로 접속 시도 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 설정
RedisClient redis("redis.example.com", 6379); // Redis 미실행 또는 네트워크 불통
// ✅ 타임아웃 설정 + 재시도
struct timeval tv = {5, 0};
redisContext* ctx = redisConnectWithTimeout("127.0.0.1", 6379, tv);
if (ctx->err) {
// 로그 남기고 재시도 또는 폴백
fprintf(stderr, "Redis 연결 실패: %s\n", ctx->errstr);
}
# Redis 서버 확인
redis-cli ping
# PONG 응답이면 정상
에러 2: freeReplyObject 누락으로 메모리 누수
증상: 장시간 실행 시 메모리 사용량이 계속 증가
원인: redisCommand가 반환하는 redisReply*를 freeReplyObject로 해제하지 않음
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 메모리 누수
redisReply* reply = (redisReply*)redisCommand(ctx, "GET key");
std::string result = reply->str; // 사용 후
// freeReplyObject(reply) 누락!
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ RAII 래퍼 사용
struct ReplyGuard {
redisReply* r;
~ReplyGuard() { if (r) freeReplyObject(r); }
};
redisReply* reply = (redisReply*)redisCommand(ctx, "GET key");
ReplyGuard guard{reply};
if (reply->type == REDIS_REPLY_STRING) {
std::string result(reply->str, reply->len);
}
에러 3: %s vs %b 혼동 (바이너리 안전)
증상: 값에 null 문자(\0)가 포함되면 잘림
원인: %s는 null 종료 문자열만 처리. 바이너리 데이터에는 %b 사용 필요
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 바이너리 데이터 잘림
std::string data = "hello\0world"; // 11바이트
redisCommand(ctx, "SET key %s", data.c_str()); // "hello"만 저장됨 (5바이트)
// ✅ 바이너리 안전
redisCommand(ctx, "SET key %b", data.data(), data.size());
에러 4: MOVED/ASK (Redis Cluster)
증상: (error) MOVED 12345 192.168.1.10:6379
원인: Redis Cluster 모드에서 키가 다른 슬롯에 있을 때. hiredis 단일 연결은 리다이렉트를 자동 처리하지 않음
해결법:
- Redis Cluster용으로는
redis-plus-plus의RedisCluster사용 - 또는 단일 노드 Redis 사용
// redis-plus-plus Cluster
#include <sw/redis++/redis++.h>
sw::redis::RedisCluster redis("tcp://127.0.0.1:7000");
에러 5: NOAUTH Authentication required
증상: (error) NOAUTH Authentication required
원인: Redis에 비밀번호가 설정되어 있는데 AUTH 없이 명령 실행
해결법:
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// hiredis
redisReply* r = (redisReply*)redisCommand(ctx, "AUTH %s", password);
freeReplyObject(r);
// redis-plus-plus
auto redis = Redis("tcp://127.0.0.1:6379", Options{}.password("mypassword"));
에러 6: 같은 연결을 멀티스레드에서 공유
증상: 간헐적 크래시, 잘못된 응답
원인: hiredis redisContext는 스레드 안전하지 않음
다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 위험
RedisClient redis("127.0.0.1", 6379);
std::thread t1([&]() { redis.get("key1"); });
std::thread t2([&]() { redis.get("key2"); });
해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ 스레드당 연결 또는 연결 풀
void worker() {
thread_local RedisClient redis("127.0.0.1", 6379);
redis.get("key");
}
// 또는 redis-plus-plus 연결 풀 (내부적으로 스레드 안전)
auto redis = Redis("tcp://127.0.0.1:6379"); // 연결 풀
6. 성능 최적화 팁
팁 1: 파이프라인으로 RTT 감소
단일 명령마다 왕복(RTT)이 발생합니다. 여러 명령을 파이프라인으로 묶으면 RTT를 줄일 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// hiredis 파이프라인
redisReply* reply;
redisAppendCommand(ctx, "SET key1 %s", "v1");
redisAppendCommand(ctx, "SET key2 %s", "v2");
redisAppendCommand(ctx, "GET key1");
redisGetReply(ctx, (void**)&reply);
freeReplyObject(reply);
redisGetReply(ctx, (void**)&reply);
freeReplyObject(reply);
redisGetReply(ctx, (void**)&reply);
freeReplyObject(reply);
Redis 고급 활용(#52-3)에서 파이프라인을 더 자세히 다룹니다.
팁 2: 연결 풀 사용
매 요청마다 새 연결을 만들면 TCP 핸드셰이크 비용이 큽니다. 연결 풀로 재사용하세요. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// redis-plus-plus는 기본이 연결 풀
auto redis = Redis("tcp://127.0.0.1:6379");
// 풀 크기 조정
ConnectionOptions opts;
opts.host = "127.0.0.1";
opts.port = 6379;
ConnectionPoolOptions pool_opts;
pool_opts.size = 10;
auto redis = Redis(opts, pool_opts);
팁 3: 키 설계 — 짧고 일관되게
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ 긴 키
"user:session:cache:data:12345:profile:settings"
// ✅ 짧고 일관된 키
"u:12345:prof"
팁 4: 대량 조회 시 MGET
아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ N번 왕복
for (int i = 0; i < 100; ++i) {
redis.get("key:" + std::to_string(i));
}
// ✅ MGET 1번
redisReply* r = (redisReply*)redisCommand(ctx, "MGET k1 k2 k3 ....k100");
팁 5: TTL 적절히 설정
캐시는 반드시 TTL을 두어 메모리 폭증을 방지하세요. 무기한 캐시는 Redis OOM으로 이어질 수 있습니다.
redis.set("cache:product:123", json, 300); // 5분 TTL
7. 프로덕션 패턴
패턴 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;
}
void ensureConnected(RedisClient& redis) {
if (!redis.ping()) {
// 재연결 또는 알림
throw std::runtime_error("Redis 연결 끊김");
}
}
패턴 2: 캐시 스탬피드 방지 (분산 락)
여러 요청이 동시에 캐시 미스 시 DB를 중복 조회하지 않도록, 락으로 한 요청만 DB 조회하고 나머지는 대기합니다. 다음은 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: Lua로 원자적 락 해제
분산 락 해제 시 같은 토큰을 가진 클라이언트만 해제해야 합니다. Lua로 원자적으로 처리합니다. 아래 코드는 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 실행
redisReply* r = (redisReply*)redisCommand(ctx,
"EVAL \"if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end\" 1 lock:resource %s",
token.c_str());
freeReplyObject(r);
패턴 4: 설정 외부화
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct RedisConfig {
std::string host = "127.0.0.1";
int port = 6379;
int timeout_sec = 5;
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;
}
8. 구현 체크리스트
환경 설정
- Redis 서버 실행 확인 (
redis-cli ping) - hiredis 또는 redis-plus-plus 설치
- CMake/vcpkg 연동
연결 및 기본 사용
-
redisConnectWithTimeout으로 타임아웃 설정 - RAII로
redisContext/redisReply관리 -
freeReplyObject누락 없이 호출
에러 처리
-
ctx->err체크 -
reply->type == REDIS_REPLY_ERROR처리 - Connection timeout 시 재시도 또는 폴백
성능
- 연결 풀 또는 스레드당 연결
- 대량 조회 시 MGET/파이프라인 고려
- 캐시 키에 TTL 설정
프로덕션
- Health Check (PING) 주기적 수행
- 비밀번호(AUTH) 설정 시 환경 변수 사용
- 캐시 스탬피드 방지 (분산 락) 적용
정리
| 항목 | hiredis | redis-plus-plus |
|---|---|---|
| 용도 | 경량, C 호환, 임베디드 | Modern C++, 풍부한 API |
| 연결 | 단일, 직접 관리 | 연결 풀 내장 |
| 에러 | 수동 체크 | 예외 기반 |
| 권장 | 레거시, 최소 의존성 | 신규 프로젝트 |
| 핵심 원칙: |
- RAII로 연결·응답 관리
- 바이너리 데이터는
%b사용 - 멀티스레드에서는 연결 풀 또는 스레드당 연결
- 캐시는 반드시 TTL 설정 다음 글 Redis 고급 활용(#52-3)에서는 Pub/Sub, 파이프라인, Lua 스크립팅, Redis Cluster를 다룹니다.
참고 자료
- Redis 공식 문서
- hiredis GitHub
- redis-plus-plus GitHub
- 캐싱 전략(#50-8) — Cache-Aside, Write-Through