[2026] C++ Redis 클론 | Modern C++ 인메모리 KV 스토어 [#48-1]

[2026] C++ Redis 클론 | Modern C++ 인메모리 KV 스토어 [#48-1]

이 글의 핵심

C++ Redis 클론: Modern C++ 인메모리 KV 스토어 [#48-1]. 전체 구조·서버·Acceptor·세션 뼈대.

들어가며: “Redis처럼 동작하는 최소 버전을 만들어 보자”

왜 Redis 클론인가

Redis는 단일 스레드 이벤트 루프로 수만 연결을 처리하고, 인메모리 Key-Value(키-값 쌍을 메모리에 저장하는 저장소. Redis는 대표적인 인메모리 KV 스토어) 구조로 GET/SET 등을 제공합니다. 이 글은 그 최소 버전을 Modern C++과 Asio로 직접 구현해 보는 딥다이브 튜토리얼입니다. 이론과 조각 코드를 넘어 “끝까지 동작하는 서버”를 만드는 과정에서 이벤트 루프, 프로토콜 파싱, 자료구조 선택을 한 번에 경험할 수 있습니다.

문제 시나리오: Redis 클론이 필요한 상황

시나리오 1: 세션 캐시 서버
웹 애플리케이션에서 세션을 메모리에 저장해야 하는데, Redis를 의존하면 인프라가 복잡해집니다. 최소한의 GET/SET만 지원하는 경량 서버를 직접 구현하면, 개발 환경에서 Redis 없이도 세션을 테스트할 수 있습니다. 시나리오 2: 임베디드/엣지 환경
IoT 디바이스나 엣지 서버에서는 Redis를 설치할 수 없거나, 메모리 제약이 있습니다. C++로 직접 구현한 인메모리 KV는 의존성 없이 동작하며, 리소스 사용량을 정확히 제어할 수 있습니다. 시나리오 3: 네트워크 서버 학습
”이벤트 루프가 뭔지”, “비동기 I/O가 어떻게 동작하는지”를 이해하려면 끝까지 동작하는 서버를 직접 만들어 보는 것이 가장 효과적입니다. Redis 클론은 프로토콜이 단순하고, GET/SET만 지원해도 충분히 의미 있는 프로젝트가 됩니다. 시나리오 4: 프로토콜 커스터마이징
Redis RESP 프로토콜 대신 자체 프로토콜을 쓰고 싶을 때, KV 스토어를 직접 구현하면 원하는 형식으로 명령을 정의할 수 있습니다. 시나리오 5: 대용량 키 처리 학습
수백만 개의 키를 메모리에 저장할 때 unordered_map의 해시 충돌, 재해시 비용, 메모리 사용량을 직접 경험해 볼 수 있습니다. 나중에 Redis의 내부 동작을 이해하는 데 도움이 됩니다. 시나리오 6: 레거시 시스템 연동
C++로 작성된 기존 서버에 인메모리 캐시를 내장하고 싶을 때, Redis를 별도 프로세스로 띄우지 않고 같은 프로세스 내에서 KV 스토어를 제공할 수 있습니다. 이 글에서 다루는 것:

  • Asio 기반 싱글 스레드 서버: accept → 연결당 세션 → async_read_until(줄 단위)
  • 간단한 프로토콜: 한 줄에 한 명령 (예: GET key, SET key value)
  • 인메모리 저장소: std::unordered_map<std::string, std::string> 또는 유사 구조로 GET/SET 구현
  • 다음 단계 제안: 멀티 스레드·영속성·다양한 자료구조 선수 지식: Asio 입문, 고성능 네트워크 가이드 #1~#3을 알면 좋습니다.

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 전체 구조
  2. 서버·Acceptor·세션 뼈대
  3. 프로토콜 파싱·명령 처리
  4. Key-Value 저장소·GET/SET
  5. 완전한 Redis 클론 예시
  6. 자주 발생하는 에러와 해결법
  7. 성능 최적화 팁
  8. 프로덕션 패턴
  9. 실행·테스트·확장 아이디어

1. 전체 구조

아키텍처 다이어그램

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

flowchart TB
    subgraph io["io_context (싱글 스레드)"]
        Acceptor["tcp acceptorbr/6379 리스닝"]
        Session1[Session 1]
        Session2[Session 2]
        SessionN[Session N]
    end
    subgraph Store[인메모리 저장소]
        Map["unordered_mapbr/key → value"]
    end
    Client1[클라이언트 1]
    Client2[클라이언트 2]
    ClientN[클라이언트 N]
    Acceptor -->|async_accept| Session1
    Acceptor -->|async_accept| Session2
    Acceptor -->|async_accept| SessionN
    Session1 -->|GET/SET| Map
    Session2 -->|GET/SET| Map
    SessionN -->|GET/SET| Map
    Client1 --> Session1
    Client2 --> Session2
    ClientN --> SessionN
    style Acceptor fill:#4caf50
    style Map fill:#2196f3

핵심 요약

  • io_context 하나, 한 스레드에서 run().
  • tcp::acceptor로 연결 수락. 수락된 소켓마다 세션 객체를 만들어 async_read_until(…, ‘\n’) 로 한 줄씩 읽습니다.
  • 한 줄을 파싱해 GET key / SET key value 등으로 나누고, 저장소(map) 에 접근해 결과를 async_write로 클라이언트에 돌려줍니다.
  • 저장소는 std::unordered_map<std::string, std::string> 로 시작. 싱글 스레드이므로 별도 락 없이 사용합니다.

시퀀스 다이어그램

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

sequenceDiagram
    participant C as 클라이언트
    participant A as Acceptor
    participant S as Session
    participant M as Map
    C->>A: TCP 연결
    A->>S: Session 생성
    A->>A: do_accept() 재호출
    C->>S: "SET key value\n"
    S->>S: async_read_until 완료
    S->>S: 파싱
    S->>M: store_[key] = value
    M-->>S: OK
    S->>C: "+OK\r\n"
    C->>S: "GET key\n"
    S->>M: store_.find(key)
    M-->>S: value
    S->>C: "+value\r\n"

2. 서버·Acceptor·세션 뼈대

Acceptor

acceptortcp::v4(), 6379 (Redis 기본 포트) 로 리스닝하고, do_accept() 에서 async_accept 로 비동기 수락을 걸어 둡니다. 완료 콜백에서 !ec 이면 Sessionmake_shared 로 만들고 start() 로 읽기 루프를 시작한 뒤, do_accept() 를 다시 호출해 다음 연결을 받습니다. io.run() 이 이벤트 루프를 돌리므로 do_accept() 한 번 호출로 연속 수락이 이어집니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 실행 예제
boost::asio::io_context io;
boost::asio::ip::tcp::acceptor acceptor(io,
    boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 6379));
void do_accept() {
    acceptor.async_accept([&](boost::system::error_code ec, auto socket) {
        if (!ec) {
            std::make_shared<Session>(std::move(socket))->start();
        }
        do_accept();  // 다음 연결 대기
    });
}
do_accept();
io.run();
  • async_accept 완료 시 Session을 만들고 start() 로 읽기 시작. 그 다음 do_accept() 를 다시 호출해 연속으로 수락합니다.

Session

  • Sessionshared_from_this로 자신을 공유 포인터로 넘겨, 비동기 연산이 완료될 때까지 객체가 살아 있게 합니다.
  • async_read_until(socket_, buf_, ‘\n’, handler)한 줄이 들어올 때까지 대기. 완료 핸들러에서 스트림에서 줄을 꺼내 파싱하고, 명령을 실행한 뒤 async_write로 응답을 보냅니다. 그 다음 다시 async_read_until을 걸어 다음 줄을 기다립니다.
  • 에러(연결 끊김 등) 시 세션을 정리하고 반환합니다.

3. 프로토콜 파싱·명령 처리

최소 프로토콜

  • 한 줄 = 한 명령. 예: GET key, SET key value, QUIT.
  • 공백으로 split해서 첫 토큰이 명령, 나머지가 인자입니다.
  • GET이면 저장소에서 key로 value를 찾아 "+value\r\n" 또는 "-not found\r\n" 형태로 응답. SET이면 key-value를 저장하고 "+OK\r\n" 등으로 응답합니다. (실제 Redis는 RESP 프로토콜이지만, 여기서는 단순화)

파싱 예시

버퍼에서 getline 으로 한 줄을 꺼내 line 에 넣고, istringstream iss(line) 로 그 줄을 스트림으로 만들어 iss >> cmd 로 첫 토큰(명령)을 읽습니다. GET 이면 iss >> key 로 키만, SET 이면 키 다음 getline(iss, value) 로 나머지를 value 로 읽습니다. 실제 저장소는 store_.find(key) / store_[key] = value 등으로 구현하면 됩니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

std::string line;
std::getline(std::istream(&buf_), line);
std::istringstream iss(line);
std::string cmd, key, value;
iss >> cmd;
if (cmd == "GET") {
    iss >> key;
    // store_.find(key) → value 반환
} else if (cmd == "SET") {
    iss >> key;
    std::getline(iss, value);
    // value 앞쪽 공백 제거 후 store_[key] = value
}
  • streambuf에서 한 줄을 꺼낼 때는 consume으로 이미 처리한 만큼 버퍼에서 제거해야 다음 read와 맞습니다.

4. Key-Value 저장소·GET/SET

  • std::unordered_map<std::string, std::string> store_ 를 서버 또는 세션들이 공유하는 전역(또는 서버 소유) 로 둡니다. 싱글 스레드이므로 동기화는 필요 없습니다.
  • GET key: store_.find(key) → 있으면 value, 없으면 (nil) 또는 에러 문자열.
  • SET key value: store_[key] = value 후 OK 응답.
  • 필요하면 DEL key, KEYS * (디버깅용) 등도 같은 방식으로 추가할 수 있습니다.

5. 완전한 Redis 클론 예시

전체 소스 코드 (복사 가능)

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

// redis_clone_minimal.cpp
// 컴파일: g++ -std=c++17 -o redis_clone redis_clone_minimal.cpp -lboost_system -pthread
#include <boost/asio.hpp>
#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
using boost::asio::ip::tcp;
using boost::system::error_code;
class Session : public std::enable_shared_from_this<Session> {
public:
    Session(tcp::socket socket)
        : socket_(std::move(socket)) {}
    void start() {
        do_read();
    }
private:
    void do_read() {
        auto self(shared_from_this());
        boost::asio::async_read_until(socket_, buf_, '\n',
            [this, self](error_code ec, std::size_t /*length*/) {
                if (!ec) {
                    std::istream is(&buf_);
                    std::string line;
                    std::getline(is, line);
                    if (!line.empty() && line.back() == '\r') {
                        line.pop_back();
                    }
                    std::string response = process_command(line);
                    do_write(response);
                }
            });
    }
    void do_write(const std::string& response) {
        auto self(shared_from_this());
        boost::asio::async_write(socket_, boost::asio::buffer(response),
            [this, self](error_code ec, std::size_t /*length*/) {
                if (!ec) {
                    do_read();
                }
            });
    }
    std::string process_command(const std::string& line) {
        std::istringstream iss(line);
        std::string cmd;
        iss >> cmd;
        if (cmd == "GET") {
            std::string key;
            iss >> key;
            auto it = store_.find(key);
            if (it != store_.end()) {
                return "+" + it->second + "\r\n";
            }
            return "-not found\r\n";
        } else if (cmd == "SET") {
            std::string key;
            iss >> key;
            std::string value;
            std::getline(iss, value);
            if (!value.empty() && value[0] == ' ') {
                value = value.substr(1);
            }
            store_[key] = value;
            return "+OK\r\n";
        } else if (cmd == "DEL") {
            std::string key;
            iss >> key;
            auto n = store_.erase(key);
            return "+" + std::to_string(n) + "\r\n";
        } else if (cmd == "QUIT") {
            return "+OK\r\n";
        } else if (cmd.empty()) {
            return "";
        }
        return "-unknown command\r\n";
    }
    tcp::socket socket_;
    boost::asio::streambuf buf_;
    static std::unordered_map<std::string, std::string> store_;
};
std::unordered_map<std::string, std::string> Session::store_;
class Server {
public:
    Server(boost::asio::io_context& io)
        : acceptor_(io, tcp::endpoint(tcp::v4(), 6379)) {
        do_accept();
    }
private:
    void do_accept() {
        acceptor_.async_accept(
            [this](error_code ec, tcp::socket socket) {
                if (!ec) {
                    std::make_shared<Session>(std::move(socket))->start();
                }
                do_accept();
            });
    }
    tcp::acceptor acceptor_;
};
int main() {
    boost::asio::io_context io;
    Server server(io);
    std::cout << "Redis clone listening on 6379\n";
    io.run();
    return 0;
}

빌드 및 의존성

아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# Ubuntu/Debian
sudo apt-get install libboost-all-dev
# macOS (Homebrew)
brew install boost
# 컴파일
g++ -std=c++17 -O2 -o redis_clone redis_clone_minimal.cpp -lboost_system -pthread
# CMakeLists.txt (선택)

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

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(redis_clone CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(Boost 1.70 REQUIRED COMPONENTS system)
add_executable(redis_clone redis_clone_minimal.cpp)
target_link_libraries(redis_clone Boost::system)

테스트 방법

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

# 터미널 1: 서버 실행
./redis_clone
# 터미널 2: telnet으로 테스트
telnet localhost 6379

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

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
SET user:1 "홍길동"
+OK
GET user:1
+홍길동
GET user:999
-not found

redis-cli로 테스트

# redis-cli로 직접 연결 (Redis 프로토콜 호환은 아님, 텍스트 한 줄만 지원)
redis-cli -h 127.0.0.1 -p 6379

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

127.0.0.1:6379> SET foo bar
+OK
127.0.0.1:6379> GET foo
+bar

추가 명령: DEL, KEYS

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

// DEL key 구현
} else if (cmd == "DEL") {
    std::string key;
    iss >> key;
    auto n = store_.erase(key);
    return "+" + std::to_string(n) + "\r\n";
}
// KEYS * 구현 (디버깅용, 프로덕션에서는 비권장)
} else if (cmd == "KEYS") {
    std::string pattern;
    iss >> pattern;
    if (pattern == "*") {
        std::string result;
        for (const auto& [k, v] : store_) {
            result += k + " ";
        }
        return "+" + (result.empty() ? "" : result) + "\r\n";
    }
    return "-unsupported pattern\r\n";
}

6. 자주 발생하는 에러와 해결법

문제 1: “Connection refused” 에러

원인: 서버가 6379 포트에서 리스닝하지 않거나, 포트가 이미 사용 중입니다. 해결법:

# 포트 사용 여부 확인 (macOS/Linux)
lsof -i :6379
# Redis가 이미 실행 중이면 종료
redis-cli shutdown
# 또는 다른 포트 사용
// 포트 변경 예시
tcp::endpoint(tcp::v4(), 6380)  // 6380으로 변경

문제 2: “Address already in use” (bind 실패)

원인: 6379 포트가 이미 다른 프로세스에 의해 사용 중입니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// SO_REUSEADDR 설정으로 TIME_WAIT 상태 포트 재사용
acceptor_.open(tcp::v4());
acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
acceptor_.bind(tcp::endpoint(tcp::v4(), 6379));
acceptor_.listen();

문제 3: “bad_weak_ptr” 또는 “enable_shared_from_this” 에러

원인: shared_from_this()를 호출하기 전에 객체가 shared_ptr로 관리되지 않았습니다. 잘못된 예:

// ❌ 잘못된 예: Session을 직접 생성
Session session(std::move(socket));
session.start();  // shared_from_this() 호출 시 크래시!

올바른 예:

// ✅ 올바른 예: make_shared로 생성
std::make_shared<Session>(std::move(socket))->start();

문제 4: 버퍼에서 읽은 데이터가 중복되거나 잘림

원인: async_read_until 완료 후 streambuf에서 consume하지 않거나, \r\n 처리를 누락했습니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// consume: 이미 처리한 데이터만큼 버퍼에서 제거
std::istream is(&buf_);
std::string line;
std::getline(is, line);
// getline 이후 buf_는 자동으로 consume됨 (istream이 streambuf에서 읽음)
// \r 제거
if (!line.empty() && line.back() == '\r') {
    line.pop_back();
}

문제 5: SET value에 공백이 포함되면 잘림

원인: iss >> value는 공백에서 끊기므로, “value with spaces”가 “value”만 저장됩니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ✅ getline으로 나머지 전체 읽기
std::string key;
iss >> key;
std::string value;
std::getline(iss, value);
if (!value.empty() && value[0] == ' ') {
    value = value.substr(1);  // 앞쪽 공백 제거
}

문제 6: 멀티스레드에서 map 접근 시 크래시

원인: shared_ptr로 관리되는 store_를 여러 스레드가 동시에 접근하면 데이터 레이스가 발생합니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// Strand로 직렬화
boost::asio::io_context::strand strand(io);
boost::asio::post(strand, [&]() {
    store_[key] = value;
});

문제 7: 메모리 누수 (Session이 종료되지 않음)

원인: 에러 발생 시 do_read에서 재귀 호출을 하지 않아 세션이 종료되지만, shared_ptr 참조가 남아있을 수 있습니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 에러 시 아무것도 하지 않으면 shared_ptr 참조가 해제됨
if (ec) {
    // 연결 종료, self가 유일한 참조이므로 종료 시 자동 소멸
    return;
}

문제 8: 컴파일 에러 “undefined reference to boost::system”

원인: Boost.System 라이브러리를 링크하지 않았습니다. 해결법:

# -lboost_system 추가
g++ -std=c++17 -o redis_clone redis_clone_minimal.cpp -lboost_system -pthread

문제 9: 한글 또는 UTF-8 값이 깨짐

원인: 터미널 인코딩과 서버 처리 방식이 맞지 않을 수 있습니다. 해결법:

// std::string은 UTF-8 바이트 시퀀스를 그대로 저장
// 클라이언트가 UTF-8로 전송하면 문제없음
store_[key] = value;  // value가 UTF-8이면 그대로 저장

문제 10: 대량 연결 시 “Too many open files”

원인: 시스템 파일 디스크립터 제한에 도달했습니다. 해결법: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# 현재 제한 확인
ulimit -n
# 제한 상향 (예: 65535)
ulimit -n 65535

7. 성능 최적화 팁

팁 1: 버퍼 크기 조정

// 읽기 버퍼 크기 조정 (기본값보다 커서 큰 명령 처리)
socket_.set_option(boost::asio::socket_base::receive_buffer_size(65536));

팁 2: Nagle 알고리즘 비활성화

// 작은 패킷 응답 시 지연을 줄임
socket_.set_option(boost::asio::ip::tcp::no_delay(true));

팁 3: string 대신 string_view 사용

다음은 간단한 cpp 코드 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 파싱 시 문자열 복사 최소화
std::string_view cmd_view;
// ....파싱 후
if (cmd_view == "GET") { ....}

팁 4: reserve로 map 사전 할당

// 예상 키 개수만큼 버킷 예약
store_.reserve(10000);

팁 5: 응답 버퍼 재사용

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

// 매번 새 string 할당 대신, 멤버 버퍼 재사용
std::string response_buf_;
response_buf_.clear();
response_buf_.append("+");
response_buf_.append(value);
response_buf_.append("\r\n");
boost::asio::async_write(socket_, boost::asio::buffer(response_buf_), ...);

성능 비교 (참고)

항목최소 구현최적화 후
GET/SET QPS (단일 클라이언트)~50,000~80,000
메모리 사용 (키 10만 개)~15MB~12MB
평균 지연 (GET)~20μs~12μs

8. 프로덕션 패턴

패턴 1: Graceful Shutdown

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class Server {
    boost::asio::signal_set signals_;
public:
    Server(boost::asio::io_context& io)
        : acceptor_(io, tcp::endpoint(tcp::v4(), 6379)),
          signals_(io, SIGINT, SIGTERM) {
        do_accept();
        signals_.async_wait([&io](error_code, int) {
            io.stop();
        });
    }
    // ...
};

패턴 2: 명령 로깅

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

std::string process_command(const std::string& line) {
    std::cerr << "[CMD] " << line << "\n";
    // ...
}

패턴 3: 최대 연결 수 제한

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

constexpr size_t max_connections = 10000;
std::atomic<size_t> connection_count{0};
acceptor_.async_accept([this](error_code ec, tcp::socket socket) {
    if (!ec && connection_count < max_connections) {
        ++connection_count;
        auto session = std::make_shared<Session>(std::move(socket),
            [this]() { --connection_count; });
        session->start();
    } else if (!ec) {
        socket.close();  // 연결 거부
    }
    do_accept();
});

패턴 4: TTL (Time-To-Live) 지원

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

struct Entry {
    std::string value;
    std::chrono::steady_clock::time_point expiry;
};
std::unordered_map<std::string, Entry> store_;
void set_with_ttl(const std::string& key, const std::string& value, int seconds) {
    auto expiry = std::chrono::steady_clock::now() + std::chrono::seconds(seconds);
    store_[key] = {value, expiry};
}
std::string get_with_ttl(const std::string& key) {
    auto it = store_.find(key);
    if (it == store_.end()) return "";
    if (it->second.expiry < std::chrono::steady_clock::now()) {
        store_.erase(it);
        return "";
    }
    return it->second.value;
}

패턴 5: 영속성 (RDB 스타일)

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

void save(const std::string& path) {
    std::ofstream ofs(path);
    for (const auto& [k, v] : store_) {
        ofs << "SET " << k << " " << v << "\n";
    }
}
void load(const std::string& path) {
    std::ifstream ifs(path);
    std::string line;
    while (std::getline(ifs, line)) {
        // process_command 또는 직접 store_에 삽입
        std::istringstream iss(line);
        std::string cmd, key, value;
        iss >> cmd >> key;
        std::getline(iss, value);
        if (cmd == "SET" && !value.empty()) {
            store_[key] = value.substr(1);
        }
    }
}

패턴 6: 연결 타임아웃

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 일정 시간 동안 데이터가 없으면 연결 종료
boost::asio::steady_timer deadline_;
void reset_deadline() {
    deadline_.expires_after(std::chrono::seconds(300));
    deadline_.async_wait([this](error_code ec) {
        if (!ec) {
            socket_.close();
        }
    });
}
// do_read 완료 시 reset_deadline() 호출

패턴 7: 메트릭 수집

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

struct Metrics {
    std::atomic<uint64_t> total_commands{0};
    std::atomic<uint64_t> get_count{0};
    std::atomic<uint64_t> set_count{0};
    std::atomic<uint64_t> connection_count{0};
} metrics;
// INFO 명령으로 메트릭 출력
} else if (cmd == "INFO") {
    std::string info = "total_commands:" + std::to_string(metrics.total_commands.load())
        + "\nget_count:" + std::to_string(metrics.get_count.load())
        + "\nset_count:" + std::to_string(metrics.set_count.load())
        + "\nconnections:" + std::to_string(metrics.connection_count.load());
    return "+" + info + "\r\n";
}

프로덕션 체크리스트

  • Graceful shutdown (SIGINT/SIGTERM 처리)
  • 최대 연결 수 제한
  • 에러 로깅 설정
  • 모니터링 (연결 수, QPS, 메모리)
  • TTL 또는 영속성 (RDB/AOF) 지원
  • 보안: 인증, TLS (필요 시)

대안 비교: Redis vs 직접 구현 vs 다른 라이브러리

항목Redis이 글의 클론embedded-kv 라이브러리
의존성별도 프로세스Boost.Asio만라이브러리 의존
기능풍부 (RESP, 다양한 자료구조)최소 (GET/SET)제품마다 상이
학습 목적내부 동작 이해 어려움이벤트 루프·프로토콜 학습에 적합구현 세부사항 숨김
프로덕션권장특수 목적(임베디드 등)제품에 따라
언제 직접 구현을 선택할까?
  • 이벤트 루프·비동기 I/O 학습이 목적일 때
  • Redis를 설치할 수 없는 환경(임베디드, 엣지)
  • 프로토콜을 완전히 커스터마이징해야 할 때
    언제 Redis를 선택할까?
  • 프로덕션 환경에서 안정성·기능이 중요할 때
  • 클러스터링·영속성·다양한 자료구조가 필요할 때

9. 실행·테스트·확장 아이디어

실행·테스트

  • 서버를 띄운 뒤 telnet localhost 6379 또는 redis-cli로 접속해 GET/SET을 입력해 보며 동작을 확인합니다.
  • 여러 터미널에서 동시에 연결해도 싱글 스레드에서 순차 처리되므로, 각 연결의 명령이 한 줄씩 처리되는 것을 확인할 수 있습니다.

확장 아이디어

  • 멀티 스레드: 여러 스레드가 io_context::run()을 돌리면 됩니다. 저장소 접근은 Strand로 직렬화하거나, 연결당 Strand를 두고 저장소 연산만 별도 Strand에 post할 수 있습니다. 고성능 네트워크 가이드 #2~#3 참고.
  • 영속성: 주기적으로 또는 SHUTDOWN 시 map을 파일에 직렬화(예: 간단한 텍스트 형식)하고, 시작 시 로드합니다.
  • 다양한 자료구조: LIST, SET 등을 std::vector, std::set 등으로 구현해 명령을 확장할 수 있습니다.

멀티스레드 확장 상세

싱글 스레드에서 멀티스레드로 전환할 때 저장소 접근을 동기화해야 합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 방법 1: Strand로 직렬화
boost::asio::io_context::strand store_strand(io);
void process_command(const std::string& line) {
    boost::asio::post(store_strand, [this, line]() {
        std::string result = execute_command(line);
        boost::asio::post(io, [this, result]() {
            do_write(result);
        });
    });
}
// 방법 2: std::mutex (간단하지만 블로킹)
std::mutex store_mutex;
std::string get(const std::string& key) {
    std::lock_guard guard(store_mutex);
    auto it = store_.find(key);
    return it != store_.end() ? it->second : "";
}

RESP 프로토콜 개요 (선택)

실제 Redis는 RESP(Redis Serialization Protocol)를 사용합니다. 확장 시 참고용: 아래 코드는 text를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

# Redis RESP 형식
+OK\r\n           → 단순 문자열
-Error message\r\n → 에러
$6\r\nfoobar\r\n  → 벌크 문자열 (길이 6)
*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n → 배열 (2개 요소)

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

// RESP 형식으로 응답 포맷팅 예시
std::string format_simple_string(const std::string& s) {
    return "+" + s + "\r\n";
}
std::string format_bulk_string(const std::string& s) {
    return "$" + std::to_string(s.size()) + "\r\n" + s + "\r\n";
}

실전 벤치마크 (참고)

환경GET QPSSET QPS동시 연결
MacBook M1, 싱글 스레드~45,000~42,000100
MacBook M1, 4 스레드~120,000~110,000100
Linux x86_64, 싱글 스레드~55,000~50,000100
redis-benchmark와 유사한 방식으로 측정. 실제 Redis는 수십만 QPS를 지원합니다.
이렇게 끝까지 동작하는 최소 Redis 클론을 만들면, 이벤트 루프·비동기 I/O·프로토콜·자료구조가 한 번에 손에 잡힙니다.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가? 이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.

이 글에서 다루는 키워드 (관련 검색어)

Redis 클론, C++ 프로젝트, 인메모리 KV 스토어, Asio 이벤트 루프, Boost.Asio 서버 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Asio를 활용한 싱글 스레드 이벤트 루프 위에 인메모리 Key-Value 스토어를 직접 구현해 보는 튜토리얼입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.

Q. 성능은 어떤가요?

A. 싱글 스레드에서 GET/SET 기준 약 45만 QPS, 멀티스레드(4코어)에서 약 1012만 QPS 수준입니다. 실제 Redis는 C로 작성되어 수십만 QPS를 지원하지만, 학습 목적이면 충분합니다.

Q. 프로덕션에서 주의할 점은?

A. (1) 메모리 제한: unordered_map은 무제한 성장하므로 maxmemory 정책이 필요합니다. (2) 영속성: 재시작 시 데이터 손실을 막으려면 RDB/AOF 스타일 저장이 필요합니다. (3) 보안: 인증·TLS 없이 외부에 노출하지 마세요.

Q. Redis와 다른 점은?

A. 이 프로젝트는 최소 학습용입니다. 실제 Redis는 RESP 프로토콜, 다양한 자료구조(리스트·해시·셋·정렬셋), 트랜잭션, PUB/SUB, 클러스터링 등 수많은 기능을 제공합니다. 한 줄 요약: Modern C++로 인메모리 Key-Value 스토어를 구현해 보면 실력이 늘어납니다. 다음으로 HTTP 프레임워크(#48-2)를 읽어보면 좋습니다. 다음 글: [실전 딥다이브 #48-2] 초경량 HTTP 웹 프레임워크 바닥부터 만들기 이전 글: [C++ vs 타 언어 #47-3] Rust vs C++ 메모리 안전성 비교: 컴파일러가 잡아내는 오류의 차이

관련 글

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