Meata가 개발한 LSM-Tree 기반의 Key-Value 스토어. 빠른 쓰기 성능과 디스크 영속성을 동시에 챙긴다.
📚 목차
RocksDB란?
핵심 이론 개념
실제 활용과 장단점
Spring Boot 코드 예시
정리
1. RocksDB란?
백엔드 개발을 하다 보면 데이터를 어디에 저장할지 고민할 때 선택지가 보통 이렇게 나뉜다.
구분
대표 제품
특징
RDB
PostgreSQL, MySQL
정형 데이터, 트랜잭션, 디스크 저장
Memory DB
Redis, Memcached
초고속, 휘발성(기본), 메모리 한계
RocksDB
RocksDB
빠른 쓰기, 디스크 영속성, Key-Value
RocksDB는 Redis처럼 빠르지만, PostgreSQL처럼 디스크에 저장된다. 2013년 Facebook이 내부 스토리지 엔진으로 개발했고, 현재는 오픈소스로 공개되어 Apache Kafka, TiKV, CockroachDB 등 수많은 유명 시스템의 내부 스토리지 엔진으로 사용되고 있다.
💡 실제로 Kafka의 로그 스토리지, TiDB의 내부 KV 스토어가 RocksDB 기반이다.
각 CF는 독립적인 MemTable, SSTable을 가지므로 데이터 특성에 따라 설정을 다르게 줄 수 있다.
https://github.com/facebook/rocksdb
RocksDB의 계층 구조는 우연이 아니라 하드웨어 설계 원칙을 의도적으로 소프트웨어에 적용한 것이다.
CPU 데이터 탐색 : Register → L1 → L2 → L3 Cache → RAM → Disk RocksDB 데이터 탐색 : MemTable → Immutable MemTable → L0 → L1 → Ln
이는 참조 지역성(Locality of Reference) 원칙, 즉 "최근에 접근한 데이터는 곧 다시 접근할 가능성이 높다"는 하드웨어에서 검증된 개념을 그대로 차용한 것이다. 성능을 극대화하려면 결국 하드웨어가 동작하는 방식에 맞게 소프트웨어를 설계해야 하기 때문이다.
3. 실제 활용과 장단점
언제 RocksDB를 쓸까?
상황
이유
쓰기가 읽기보다 훨씬 많은 경우
LSM-Tree의 순차 I/O 강점
대용량 데이터를 디스크에 영속 저장해야 할 때
Redis보다 저렴하게 대용량 처리
메모리가 충분하지 않은 환경
디스크 기반이라 메모리 한계 없음
Kafka 같은 시스템의 내부 스토리지
로그성 데이터, append-only 패턴
✅ 장점
쓰기 성능이 극도로 빠름: 초당 수백만 건 write도 처리 가능
디스크 영속성: 재시작해도 데이터 유지 (Redis AOF보다 효율적)
압축 지원: Snappy, LZ4, Zstd 등으로 디스크 사용량 절감
Column Family: 논리적 데이터 분리 가능
Fine-grained 설정: MemTable 크기, Compaction 전략 등 세밀한 튜닝
❌ 단점
읽기 성능은 Redis보다 느림: 여러 레벨의 SSTable을 탐색해야 함
Write Amplification: Compaction 과정에서 실제로 여러 번 데이터가 쓰임
SQL 없음: 복잡한 쿼리 불가, 단순 Key-Value 조회만 가능
트랜잭션 제한적: ACID 트랜잭션을 RDB 수준으로 지원하지 않음
운영 복잡도: Compaction 튜닝, 메모리 관리 등 학습 비용 존재
⚠️ Redis와 헷갈리지 말자
Redis: 빠름 + 메모리 기반 + 데이터 크기 제한 + TTL 기반 캐시에 최적
RocksDB: 빠름 + 디스크 기반 + 대용량 가능 + 영속 스토리지에 최적
Redis를 캐시로 쓰고, 그 뒤에 RocksDB를 영속 레이어로 두는 구조도 실무에서 사용된다.
@Configuration
public class RocksDBConfig {
@Value("${rocksdb.path:/data/rocksdb}")
private String dbPath;
@Bean(destroyMethod = "close")
public RocksDB rocksDB() throws RocksDBException {
RocksDB.loadLibrary();
Options options = new Options()
.setCreateIfMissing(true)
.setWriteBufferSize(64 * 1024 * 1024L) // MemTable 크기: 64MB
.setMaxWriteBufferNumber(3) // MemTable 최대 개수
.setCompressionType(CompressionType.LZ4_COMPRESSION); // 압축
// DB 경로 디렉터리 생성
new File(dbPath).mkdirs();
return RocksDB.open(options, dbPath);
}
}
4-3. Repository 구현
@Repository
@RequiredArgsConstructor
public class RocksDBRepository {
private final RocksDB rocksDB;
/**
* 데이터 저장
*/
public void put(String key, String value) throws RocksDBException {
rocksDB.put(
key.getBytes(StandardCharsets.UTF_8),
value.getBytes(StandardCharsets.UTF_8)
);
}
/**
* 데이터 조회
*/
public Optional<String> get(String key) throws RocksDBException {
byte[] valueBytes = rocksDB.get(key.getBytes(StandardCharsets.UTF_8));
if (valueBytes == null) {
return Optional.empty();
}
return Optional.of(new String(valueBytes, StandardCharsets.UTF_8));
}
/**
* 데이터 삭제
*/
public void delete(String key) throws RocksDBException {
rocksDB.delete(key.getBytes(StandardCharsets.UTF_8));
}
/**
* 배치 쓰기 (WriteBatch) - 대량 처리 시 성능 극대화
*/
public void batchPut(Map<String, String> entries) throws RocksDBException {
try (WriteBatch batch = new WriteBatch()) {
for (Map.Entry<String, String> entry : entries.entrySet()) {
batch.put(
entry.getKey().getBytes(StandardCharsets.UTF_8),
entry.getValue().getBytes(StandardCharsets.UTF_8)
);
}
rocksDB.write(new WriteOptions(), batch);
}
}
}
4-4. Column Family 활용
@Configuration
public class RocksDBColumnFamilyConfig {
@Bean(destroyMethod = "close")
public RocksDB rocksDBWithCF() throws RocksDBException {
RocksDB.loadLibrary();
// Column Family 정의
List<ColumnFamilyDescriptor> cfDescriptors = List.of(
new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY),
new ColumnFamilyDescriptor("user_session".getBytes()),
new ColumnFamilyDescriptor("access_log".getBytes())
);
List<ColumnFamilyHandle> cfHandles = new ArrayList<>();
DBOptions dbOptions = new DBOptions()
.setCreateIfMissing(true)
.setCreateMissingColumnFamilies(true);
return RocksDB.open(dbOptions, "/data/rocksdb", cfDescriptors, cfHandles);
}
}
4-5. 실제 서비스 활용 예시 - 대용량 이벤트 로그 저장
@Service
@RequiredArgsConstructor
@Slf4j
public class EventLogService {
private final RocksDBRepository rocksDBRepository;
private final ObjectMapper objectMapper;
/**
* 이벤트 로그 저장
* key: "event:{userId}:{timestamp}"
* value: JSON 직렬화된 이벤트 데이터
*/
public void saveEventLog(String userId, EventLog eventLog) {
String key = buildKey(userId, eventLog.getTimestamp());
try {
String value = objectMapper.writeValueAsString(eventLog);
rocksDBRepository.put(key, value);
} catch (Exception e) {
log.error("이벤트 로그 저장 실패. userId={}, timestamp={}",
userId, eventLog.getTimestamp(), e);
throw new RuntimeException("이벤트 로그 저장 실패", e);
}
}
/**
* 배치로 대량 이벤트 저장 (WriteBatch 활용)
*/
public void saveEventLogBatch(List<EventLog> eventLogs) throws RocksDBException {
Map<String, String> entries = new HashMap<>();
for (EventLog log : eventLogs) {
try {
String key = buildKey(log.getUserId(), log.getTimestamp());
String value = objectMapper.writeValueAsString(log);
entries.put(key, value);
} catch (JsonProcessingException e) {
log.error("직렬화 실패: {}", log, e);
}
}
rocksDBRepository.batchPut(entries);
log.info("{}건 이벤트 배치 저장 완료", entries.size());
}
private String buildKey(String userId, long timestamp) {
return String.format("event:%s:%d", userId, timestamp);
}
}
5. 정리
핵심 포인트
구분
설명
자료구조
LSM-Tree → 쓰기에 최적화
저장 위치
디스크 기반, 영속성 보장
쓰기 방식
WAL → MemTable → SSTable (순차 I/O)
데이터 구조
Key-Value (스키마 없음)
경쟁 대상
Redis(속도) + PostgreSQL(영속) 사이
✅ RocksDB를 선택할 때
초당 수만 건 이상의 대량 쓰기가 필요할 때
데이터가 너무 커서 Redis 메모리에 다 올릴 수 없을 때
디스크에 영속 저장해야 하지만 SQL은 필요 없을 때
Kafka, TiDB처럼 내부 스토리지 엔진으로 쓸 때
❌ RocksDB가 맞지 않는 경우
복잡한 JOIN, 집계 쿼리가 필요한 경우 → PostgreSQL
단순 캐시 용도, 데이터 크기가 작은 경우 → Redis
ACID 트랜잭션이 중요한 비즈니스 로직 → PostgreSQL
마무리
RocksDB는 "빠른 쓰기 + 디스크 영속성"이라는 두 마리 토끼를 잡은 엔진이다.
Redis처럼 모든 걸 메모리에 올릴 수 없고, PostgreSQL처럼 복잡한 쿼리가 필요 없는 대용량 로그, 이벤트 스트림, 시계열 데이터 같은 상황에서 탁월한 선택이 된다.
직접 서비스에 붙여 쓰기보다 Kafka, TiKV 같은 시스템 안에서 이미 쓰이고 있는 경우가 많으니, 이 엔진의 원리를 이해하면 해당 시스템들의 동작 방식도 더 명확하게 보일 것이다.