SPiCa는 Netty 프레임워크를 사용하여 구현된 고성능 In-Memory Key-Value Store Server입니다.
클라이언트는 TCP를 통해 접속하여 PING, SET, GET, DEL, MGET 명령어를 사용할 수 있습니다.
- PING: 서버 상태 확인 (
Pong응답) - SET: 키-값 저장 (
SET key value) - SET MATCH: 조건부 저장 (
SET key newValue MATCH oldValue) - GET: 키로 값 조회 (
GET key) - DEL: 키 삭제 (
DEL key) - MGET: 여러 키 동시 조회 (
MGET key1 key2 ...) - In-Memory Storage:
ConcurrentHashMap을 사용한 Thread-safe한 데이터 저장 - Memory Optimization:
byte[]저장소와 ByteBuf 풀링으로 메모리 효율 극대화
| Command | Throughput | Latency (p50) | Latency (p99) |
|---|---|---|---|
| PING | 58,651 ops/sec | 0.063 ms | 0.144 ms |
| SET | 51,881 ops/sec | 0.073 ms | 0.154 ms |
| GET | 46,458 ops/sec | 0.074 ms | 0.211 ms |
테스트 환경: 4 threads × 10,000 ops, JIT Warm-up 3회, TCP_NODELAY 적용
- Memory Locality:
String대신byte[]저장으로 61% 메모리 절감 - ByteBuf Pooling: 상수 응답 재사용으로 Zero-Copy 전송
- Lazy Split: 필요한 명령어에서만 문자열 분리 수행
시스템의 스레드 모델은 역할에 따라 물리적으로 격리된 두 개의 EventLoopGroup으로 구성됩니다.
- Acceptor Group (Boss):
- 단일 스레드(혹은 설정된 소수)로 구성되며
OP_ACCEPT이벤트만을 전담합니다. - TCP 3-way Handshake를 완료한
SocketChannel을 생성하고, 즉시 Worker Group의 Selector에 등록(Register)하여 병목을 제거합니다.
- 단일 스레드(혹은 설정된 소수)로 구성되며
- I/O Processor Group (Worker):
- CPU 코어 수에 비례하여 할당된 스레드 풀입니다.
- 할당된 Channel의 모든
OP_READ,OP_WRITE이벤트를 Single-Threaded Event Loop 내에서 순차 처리하여, 불필요한 동기화(Lock) 비용을 제거합니다(Lock-free Architecture).
데이터 처리 로직은 Interceptor Filter Pattern의 변형인 ChannelPipeline을 통해 계층화되어 있습니다. 각 핸들러는 단일 책임 원칙(SRP)에 따라 엄격히 분리됩니다.
- Protocol Adaptation Layer (Inbound):
LineBasedFrameDecoder: TCP의 스트림(Stream) 특성으로 인한 Packet Fragmentation/Coalescing 문제를 해결하기 위해, Delimiter(\n) 기반으로 바이트 스트림을 온전한 프레임으로 재조립합니다.
- Business Logic Layer:
CommandHandler: 명령어를 파싱하고 적절한 핸들러로 라우팅합니다.SetHandler,GetHandler,DeleteHandler,MultiGetHandler: 각 명령어별 비즈니스 로직을 수행합니다.
- Response Layer:
Responses: 상수 ByteBuf를 재사용하여 Zero-Copy 응답을 제공합니다.
Java 24의 Virtual Threads를 활용한 하이브리드 아키텍처를 계획 중입니다.
- Fast Path: PING, GET, SET, DEL 등 빠른 명령어는 Netty Event Loop에서 직접 처리
- Slow Path: SAVE, EVAL 등 블로킹 작업은 Virtual Thread에서 처리하여 다른 요청에 영향 없음
graph TD
%% --- [Nodes: External Actors] ---
User["Developer / App Main"]:::actor
Client["Remote TCP Client"]:::client
%% --- [Subgraph: Server Context] ---
subgraph APP ["Application Layer (JVM)"]
direction TB
%% Config Component
CONFIG[("ServerConfiguration\n(Port, Threads, FrameLength)")]:::config
%% Main Server Class
subgraph NETTY_SERVER ["NettyServer Class"]
direction TB
START((start)):::method
STOP((stop)):::method
BLOCK((blockUntilClose)):::method
BOOTSTRAP["ServerBootstrap\n(Factory)"]:::netty
end
%% Dependency Injection
CONFIG -.->|Injects settings| NETTY_SERVER
User -->|1. calls| START
end
%% --- [Subgraph: Netty Reactor Layer] ---
subgraph REACTOR ["Netty Reactor Layer (Event Loops)"]
direction TB
BOSS["Boss Group\n(NioEventLoopGroup)\n[Acceptor Thread]"]:::boss
WORKER["Worker Group\n(NioEventLoopGroup)\n[I/O Threads]"]:::worker
START -->|2. init & bind| BOSS
START -->|3. init| WORKER
BOSS -->|4. accepts connection\n& registers| WORKER
STOP -->|shutdownGracefully| BOSS & WORKER
end
%% --- [Subgraph: The Pipeline (Per Connection)] ---
subgraph PIPELINE ["SocketChannel Pipeline (Runtime)"]
direction TB
SOCKET[("Socket Channel")]:::socket
subgraph HANDLERS ["Handler Chain"]
direction TB
%% Inbound Flow
subgraph INBOUND ["Inbound Flow (Read)"]
H1["1. LineBasedFrameDecoder\n(ByteBuf -> ByteBuf [Framed])"]:::decoder
H2["2. CommandHandler\n(Parse & Route)"]:::handler
end
%% Handlers
subgraph LOGIC ["Business Logic"]
H3["SetHandler\nGetHandler\nDeleteHandler\nMultiGetHandler"]:::handler
end
end
%% Flow connections
Client <==>|TCP/IP| SOCKET
SOCKET ==>|ByteBuf Stream| H1
H1 ==>|Frame| H2
H2 ==>|Dispatch| H3
H3 -. "Responses.send()" .-> SOCKET
WORKER -.->|executes| HANDLERS
end
%% --- [Styling Definitions] ---
classDef actor fill:#1a237e,stroke:#fff,stroke-width:2px,color:#fff
classDef client fill:#b71c1c,stroke:#fff,stroke-width:2px,color:#fff
classDef config fill:#fbc02d,stroke:#333,stroke-width:1px,color:#333
classDef method fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#333,stroke-dasharray: 5 5
classDef netty fill:#e0f7fa,stroke:#006064,stroke-width:2px,color:#006064
classDef boss fill:#3f51b5,stroke:#fff,stroke-width:2px,color:#fff
classDef worker fill:#039be5,stroke:#fff,stroke-width:2px,color:#fff
classDef socket fill:#4e342e,stroke:#fff,stroke-width:2px,color:#fff
classDef decoder fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#2e7d32
classDef encoder fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#7b1fa2
classDef handler fill:#212121,stroke:#00e676,stroke-width:3px,color:#fff
linkStyle default stroke:#546e7a,stroke-width:2px,fill:none
sequenceDiagram
participant C as Client
participant S as Netty Server
participant H as CommandHandler
participant M as DataStore (byte[])
C->>S: Connect (TCP Handshake)
Note over C, S: PING Command
C->>S: "PING\n"
S->>H: channelRead0(ByteBuf)
H-->>S: Responses.pong(ctx)
S-->>C: "Pong\n"
Note over C, S: SET Command
C->>S: "SET mykey hello\n"
S->>H: channelRead0(ByteBuf)
H->>M: putIfAbsent("mykey", bytes)
H-->>S: Responses.ok(ctx)
S-->>C: "OK\n"
Note over C, S: GET Command
C->>S: "GET mykey\n"
S->>H: channelRead0(ByteBuf)
H->>M: get("mykey")
M-->>H: byte[]
H-->>S: Responses.send(ctx, "value: hello\n")
S-->>C: "value: hello\n"
Note over C, S: DEL Command
C->>S: "DEL mykey\n"
S->>H: channelRead0(ByteBuf)
H->>M: remove("mykey")
H-->>S: Responses.ok(ctx)
S-->>C: "OK\n"
데이터의 흐름은 양방향(Duplex) 파이프라인을 통해 다음과 같은 상태 전이(State Transition)를 거칩니다.
-
Ingress (Connection & Read):
- Client Connection
$\rightarrow$ Boss Group (Accept)$\rightarrow$ Worker Group (Registration). - Socket Read
$\rightarrow$ [Framing] (Byte Stream assembly)$\rightarrow$ CommandHandler.
- Client Connection
-
Processing (Execution):
- Decoded Command
$\rightarrow$ Handler Routing$\rightarrow$ Business Logic Execution (e.g.,ConcurrentHashMapAccess).
- Decoded Command
-
Egress (Write & Flush):
- Execution Result
$\rightarrow$ Responses.send()$\rightarrow$ ByteBuf (Pooled/Constant)$\rightarrow$ Socket Buffer Flush$\rightarrow$ Client.
- Execution Result
src/main/java/com/spica/
├── Application.java # Entry Point
├── server/
│ ├── Server.java # Server Interface
│ ├── ServerConfiguration.java # Configuration (port, threads)
│ └── NettyServer.java # Netty Bootstrap & Pipeline
└── handler/
├── Responses.java # ByteBuf 상수 & 풀링 유틸리티
├── CommandHandler.java # 명령어 파싱 & 라우팅
├── SetHandler.java # SET 명령어 처리
├── GetHandler.java # GET 명령어 처리
├── DeleteHandler.java # DEL 명령어 처리
├── MultiGetHandler.java # MGET 명령어 처리
├── PingPongHandler.java # PING 명령어 처리
└── SleepHandler.java # SLEEP 명령어 처리 (테스트용)
- Java 24 or higher
- Gradle
./gradlew runor
gradle run터미널을 열고 다음 명령어들을 입력하여 테스트할 수 있습니다.
1. Ping Test
echo "PING" | nc localhost 6379
# 응답: Pong2. Set Test
echo "SET mykey myvalue" | nc localhost 6379
# 응답: OK3. Get Test
echo "GET mykey" | nc localhost 6379
# 응답: value: myvalue4. Delete Test
echo "DEL mykey" | nc localhost 6379
# 응답: OK5. Multi-Get Test
echo "MGET key1 key2 key3" | nc localhost 6379
# 응답: 각 키별 결과updatedAt: 2025.12.14