[2026] C++ Redis 클라이언트 완벽 가이드 | hiredis·redis-plus-plus·캐싱·세션·분산락

[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 또는 Stringhiredis, redis-plus-plus
분산 락String (SET NX EX)hiredis, redis-plus-plus
순위표Sorted Setredis-plus-plus (편의 API)

목차

  1. 환경 설정 및 설치
  2. hiredis 기본 연결 및 GET/SET
  3. redis-plus-plus Modern C++ 클라이언트
  4. 완전한 Redis C++ 예제
  5. 자주 발생하는 에러와 해결법
  6. 성능 최적화 팁
  7. 프로덕션 패턴
  8. 구현 체크리스트

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 비교

항목hiredisredis-plus-plus
언어CC++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-plusRedisCluster 사용
  • 또는 단일 노드 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) 설정 시 환경 변수 사용
  • 캐시 스탬피드 방지 (분산 락) 적용

정리

항목hiredisredis-plus-plus
용도경량, C 호환, 임베디드Modern C++, 풍부한 API
연결단일, 직접 관리연결 풀 내장
에러수동 체크예외 기반
권장레거시, 최소 의존성신규 프로젝트
핵심 원칙:
  1. RAII로 연결·응답 관리
  2. 바이너리 데이터%b 사용
  3. 멀티스레드에서는 연결 풀 또는 스레드당 연결
  4. 캐시는 반드시 TTL 설정 다음 글 Redis 고급 활용(#52-3)에서는 Pub/Sub, 파이프라인, Lua 스크립팅, Redis Cluster를 다룹니다.

참고 자료


관련 글

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