Page History
...
| 항목 | 설명 |
|---|---|
| Document Store | JSON 기반의 문서 저장 (MongoDB처럼) |
| Full-Text Search 내장 | Lucene 기반 검색엔진 포함 (Elasticsearch 대체 가능) |
| Graph-Like Traversal 지원 | Include, Load, RelatedDocuments 로 Graph traversal 흉내 가능 |
| 벡터 검색 (Vector Search) | 6.0 이상 버전에서 Vector search 지원 (Preview → Stable 예정) |
| ACID 트랜잭션 지원 | NoSQL 중 드물게 단일 DB 내 ACID 지원 |
| 자동 인덱싱/쿼리 최적화 | 쿼리 기반으로 자동 인덱싱 생성 |
| Change Vector / ETL 기능 내장 | 다른 Raven 클러스터 또는 외부 시스템으로 데이터 복제 가능 |
| 클라우드 + 온프렘 지원 | 다양한 배포 환경 대응 |
| Sharding + Replication | 분산 구조 대응 가능 (Sharded DB) |
✅ 기존 DB 구성 중 대체 가능한 역할
| 기존 시스템 | RavenDB로 대체 가능 여부 | 설명 |
|---|---|---|
| MongoDB (Document DB) | ✅ 완전 대체 | JSON 기반 문서 저장, 컬렉션 → 문서 분리 모델 |
| Elasticsearch | ✅ 부분 대체 | Full-text 검색 지원, 복잡한 분석쿼리는 제한적이나 일반 검색에는 충분 |
| Neo4j (Graph DB) | ⚠️ 간단한 관계 트래버설은 가능 | 명시적 Graph 모델링은 어려움 (복잡한 네트워크 분석에는 부적합) |
| Vector DB (예: Weaviate, Milvus) | ✅ 단순 벡터 검색은 대체 가능 | 다차원 벡터 검색 API 제공, 모델링+쿼리 결합 쉬움 |
| RDB (CRUD/정형) | ⚠️ 단순 CRUD는 가능, 복잡한 조인과 트랜잭션은 제한적 | 정형 테이블 기반보다는 문서 중심 모델 필요 |
RavenDB Docker StandAlone 구동
| Code Block | ||
|---|---|---|
| ||
version: '3.8'
services:
ravendb:
image: ravendb/ravendb:ubuntu-latest
container_name: ravendb
ports:
- "9000:8080"
environment:
- RAVEN_Setup_Mode=None
- RAVEN_License_Eula_Accepted=true
volumes:
- ravendb_data:/ravendb/data
- ravendb_logs:/ravendb/logs
volumes:
ravendb_data:
ravendb_logs: |
- 클라우드로도 이용가능하며~ 로컬또는 온프레미스로도 운영가능합니다.
IDE 환경
- Docker와 통합된 IDE환경으로 RavenDB 구동
- 닷넷개발 IDE VisualStudio가 개인적으로 익숙하지만 최근 젯브레인진영 Rider를 이용해보고 있습니다.
...
- DB를 관리할 웹툴을 포함하며 기본적인 관리가 가능합니다.
- IDE내에서 연동돠는 플러그인은 아직 없어보입니다.
- Postgres 호환플러그인을 지원하니 BI툴및 IDE내 기본조회연동은 가능할듯
- AI Hub는 DB펑션을 LLM에 연결해 쿼리와 통합해 AI기능 활용가능한 것으로 보여집니다. - 개발1개팀이 필요한 AIOps-ETL을 구축할필요없어보임
언어별 지원 클라이언트
- Akka.net을 연계해 사용예정이기때문에 .NET 기반실험이 진행되었습니다.
- 지원 클라이언트가 소개될때 .NET 라이브러리가 첫번째에 잘 노출되는 일이 없는데~ 닷넷친화적 DB로 추정해봅니다.
RavenClient for .NET
- 설치된 서버 Mazor버전과 맞춰서 패키지 설치
Database생성
- 몽고DB와 유사하게 스키마리스 DB이기때문에 DDL코드가 필요로 하지 않습니다. 엔티티를 관리해야할 부담을 덜수 있습니다.
- 처음이용할때 DBManageMent Tool을 한참 찾았는데~ 웹에 통합되어 대부분의 관리기능을 웹을 통해 가능합니다. - 로컬설치버전 기준
Repository 코드
| Code Block | ||
|---|---|---|
| ||
public class Member
{
public string Id { get; set; } // RavenDB는 기본적으로 Id를 문서 키로 사용
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
public class MemberRepository
{
private readonly IDocumentStore _store;
public MemberRepository(IDocumentStore store)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
}
public void AddMember(Member member)
{
using (var session = _store.OpenSession())
{
session.Store(member);
session.SaveChanges();
}
}
public Member GetMemberById(string id)
{
using (var session = _store.OpenSession())
{
return session.Load<Member>(id);
}
}
public void UpdateMember(Member member)
{
using (var session = _store.OpenSession())
{
var existingMember = session.Load<Member>(member.Id);
if (existingMember != null)
{
existingMember.Name = member.Name;
existingMember.Email = member.Email;
existingMember.Age = member.Age;
session.SaveChanges();
}
}
}
public void DeleteMember(string id)
{
using (var session = _store.OpenSession())
{
var member = session.Load<Member>(id);
if (member != null)
{
session.Delete(member);
session.SaveChanges();
}
}
}
} |
CRUD TEST 코드
| Code Block | ||
|---|---|---|
| ||
public class MemberRepositoryTest : TestKitXunit
{
private readonly IDocumentStore _store;
private readonly MemberRepository _repository;
public MemberRepositoryTest(ITestOutputHelper output) : base(output)
{
// RavenDB 임베디드 서버 초기화
_store = new DocumentStore
{
Urls = new[] { "http://localhost:9000" }, // 로컬 RavenDB URL
Database = "net-core-labs"
};
_store.Initialize();
// MemberRepository 초기화
_repository = new MemberRepository(_store);
}
[Fact]
public void AddMember_ShouldAddMemberSuccessfully()
{
// Arrange
var member = new Member
{
Name = "John Doe",
Email = "john.doe@example.com",
Age = 30
};
// Act
_repository.AddMember(member);
// Assert
var retrievedMember = _repository.GetMemberById(member.Id);
Assert.NotNull(retrievedMember);
Assert.Equal("John Doe", retrievedMember.Name);
}
[Fact]
public void UpdateMember_ShouldUpdateMemberSuccessfully()
{
// Arrange
var member = new Member
{
Name = "Jane Doe",
Email = "jane.doe@example.com",
Age = 25
};
_repository.AddMember(member);
// Act
member.Age = 26;
_repository.UpdateMember(member);
// Assert
var updatedMember = _repository.GetMemberById(member.Id);
Assert.NotNull(updatedMember);
Assert.Equal(26, updatedMember.Age);
}
[Fact]
public void DeleteMember_ShouldDeleteMemberSuccessfully()
{
// Arrange
var member = new Member
{
Name = "Mark Smith",
Email = "mark.smith@example.com",
Age = 40
};
_repository.AddMember(member);
// Act
_repository.DeleteMember(member.Id);
// Assert
var deletedMember = _repository.GetMemberById(member.Id);
Assert.Null(deletedMember);
}
} |
...
- 비교적 심플한 코드작성으로 CRUD 테스트 수행이 완료되었습니다.
단일 DB에서 다양한 검색수행
반경검색/FullText검색및 Vector검색등 고급 검색기능을 단일DB만사용해 검색할수 있습니다.
| Code Block | ||
|---|---|---|
| ||
using ActorLib.Persistent.Model;
using Raven.Client.Documents;
using Raven.Client.Documents.Indexes.Vector;
using Raven.Client.Documents.Linq;
namespace ActorLib.Persistent;
public class TravelReviewRepository
{
private readonly IDocumentStore _store;
public TravelReviewRepository(IDocumentStore store)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
}
public void AddReview(TravelReview review)
{
using (var session = _store.OpenSession())
{
session.Store(review);
session.SaveChanges();
}
}
public List<TravelReview> SearchReviews(string keyword, double latitude, double longitude, double radiusKm, string category = null)
{
using (var session = _store.OpenSession())
{
// 명시적으로 변수로 선언
var keywordValue = keyword;
var categoryValue = category;
IRavenQueryable<TravelReview> query = session.Query<TravelReview>();
if (!string.IsNullOrEmpty(keywordValue))
{
query = query.Search(r => r.Content, keywordValue); // 제목 검색 추가
}
if (!string.IsNullOrEmpty(categoryValue))
{
query = query.Where(r => r.Category == categoryValue); // 제목 검색 추가
}
// RavenDB에서 서버 측 필터링 후 클라이언트 측에서 반경 필터링
var results = query.ToList();
return results.Where(r =>
6371 * Math.Acos(
Math.Cos(DegToRad(latitude)) * Math.Cos(DegToRad(r.Latitude)) *
Math.Cos(DegToRad(r.Longitude) - DegToRad(longitude)) +
Math.Sin(DegToRad(latitude)) * Math.Sin(DegToRad(r.Latitude))
) <= radiusKm).ToList();
}
}
public List<TravelReview> SearchReviewsByRadius(double latitude, double longitude, double radiusKm)
{
using (var session = _store.OpenSession())
{
return session.Query<TravelReview>()
.Spatial(
r => r.Point(x => x.Latitude, x => x.Longitude),
criteria => criteria.WithinRadius(radiusKm, latitude, longitude))
.ToList();
}
}
public List<TravelReview> SearchReviewsByVector(float[] vector, int topN = 5)
{
using (var session = _store.OpenSession())
{
var results = session.Query<TravelReview>()
.VectorSearch(
field => field.WithEmbedding(x => x.TagsEmbeddedAsSingle, VectorEmbeddingType.Single),
queryVector => queryVector.ByEmbedding(new RavenVector<float>(vector)),
0.85f,
topN)
.Customize(x => x.WaitForNonStaleResults())
.ToList();
return results;
}
}
private double DegToRad(double degrees)
{
return degrees * (Math.PI / 180);
}
} |
...
RavenDB가 Akka.net의 Persitence 기능을 지원하며 이 코드를 이해하기전 이벤트 소싱패턴을 액터모델을 함께 이용했을때 특징을 먼저 살펴보고 작동가능한 구현된 샘플코드도 유닛테스트를 통해 살펴보겠습니다.
Akka.NET의 액터모델은 CQRS (Command Query Responsibility Segregation) 및 이벤트 소싱 (Event Sourcing) 패턴을 구현하기에 매우 적합한 구조를 제공합니다. 특히 Akka.Persistence 모듈과의 연계를 통해 상태 저장과 복구, 그리고 이벤트 기반 모델링이 가능해집니다. 아래에 그 기능과 장점을 정리해드립니다.
...
✅ Akka.NET 액터모델 + Akka.Persistence 를 활용한 CQRS + 이벤트 소싱
1. 핵심 구성요소
| 구성요소 | 설명 |
|---|---|
PersistentActor | 이벤트를 저장하고 재생할 수 있는 상태 유지 액터 |
Snapshotting | 빠른 복구를 위해 특정 시점의 상태를 저장 |
Event Journal | 모든 상태 변화(event)를 append-only 로그로 저장 |
Read Model Actor | 쿼리에 최적화된 projection을 담당 |
Command Handler Actor | 명령(Command)을 받아 이벤트로 전환 및 persistence 수행 |
2. CQRS(이벤트 소싱) with ActorModel
- 이벤트가 발생할때 즉각 CRUD하여 이용하는 패턴이 아닌 액터모델의 메일박스를 이용해 상태있는 프로그래밍을 통해 이벤트소싱을 설계할수 있으며~ 이러한 느낌
3. 장점 요약
✅ 액터모델 기반의 장점
상태 격리: 각 액터는 고유 상태를 가지며 병렬 처리에 유리함.
비동기 메시지 기반 처리: 병목 없이 고성능 분산 처리 가능.
자연스러운 도메인 분리: 액터 자체가 DDD의 Aggregate Root 역할을 하기에 적합.
...
고객 주문 시스템:
OrderActor가PlaceOrderCommand를 받고OrderPlacedEvent를 생성 및 저장챗봇 세션 관리:
SessionActor가 메시지를 이벤트로 저장하여 상태 기반 대화 흐름 관리금융 거래 기록:
AccountActor가Withdrawn,Deposited이벤트를 기록하여 완전한 거래 이력 확보
SalesActor
| Code Block | ||||
|---|---|---|---|---|
| ||||
public class SalesActor: ReceivePersistentActor
{
// The unique actor id
public override string PersistenceId => "sales-actor";
// The state that will be persisted in SNAPSHOTS
private SalesActorState _state;
public SalesActor(long expectedProfit, TaskCompletionSource<bool> taskCompletion)
{
_state = new SalesActorState
{
totalSales = 0
};
// Process a sale:
Command<Sale>(saleInfo =>
{
if (_state.totalSales < expectedProfit)
{
// Persist an EVENT to RavenDB
// ===========================
// The handler function is executed after the EVENT was saved successfully
Persist(saleInfo, _ =>
{
// Update the latest state in the actor
_state.totalSales += saleInfo.Price;
ConsoleHelper.WriteToConsole(ConsoleColor.Black,
$"Sale was persisted. Phone brand: {saleInfo.Brand}. Price: {saleInfo.Price}");
// Store a SNAPSHOT every 5 sale events
// ====================================
if (LastSequenceNr != 0 && LastSequenceNr % 5 == 0)
{
SaveSnapshot(_state.totalSales);
}
});
}
else if (!taskCompletion.Task.IsCompleted)
{
Sender.Tell(new StopSimulate());
ConsoleHelper.WriteToConsole(ConsoleColor.DarkMagenta,
$"Sale not persisted: " +
$"Total sales have already reached the expected profit of {expectedProfit}");
ConsoleHelper.WriteToConsole(ConsoleColor.DarkMagenta,
_state.ToString());
taskCompletion.TrySetResult(true);
}
});
// Handle a SNAPSHOT success msg
Command<SaveSnapshotSuccess>(success =>
{
ConsoleHelper.WriteToConsole(ConsoleColor.Blue,
$"Snapshot saved successfully at sequence number {success.Metadata.SequenceNr}");
// Optionally, delete old snapshots or events here if needed
// DeleteMessages(success.Metadata.SequenceNr);
});
// Recover an EVENT
Recover<Sale>(saleInfo =>
{
_state.totalSales += saleInfo.Price;
ConsoleHelper.WriteToConsole(ConsoleColor.DarkGreen,
$"Event was recovered. Price: {saleInfo.Price}");
});
// Recover a SNAPSHOT
Recover<SnapshotOffer>(offer =>
{
var salesFromSnapshot = (long) offer.Snapshot;
_state.totalSales = salesFromSnapshot;
ConsoleHelper.WriteToConsole(ConsoleColor.DarkGreen,
$"Snapshot was recovered. Total sales from snapshot: {salesFromSnapshot}");
});
}
} |
...
1. PersistenceId
| Code Block | ||
|---|---|---|
| ||
public override string PersistenceId => "sales-actor"; |
액터의 고유 식별자로 이벤트와 스냅샷을 식별하기 위한 키
RavenDB 또는 다른
Akka.Persistencebackend에서 이 키로 데이터를 저장/조회함
2. 상태 관리 변수
| Code Block | ||
|---|---|---|
| ||
private SalesActorState _state; |
현재 액터 상태를 저장하는 내부 객체
여기서는
totalSales(누적 매출액)를 관리
3. 명령 처리 (Command<Sale>)
| Code Block | ||
|---|---|---|
| ||
Command<Sale>(saleInfo => { ... }); |
외부에서 Sale 메시지를 받으면 처리
조건에 따라 이벤트를 Persist하거나 무시
주요 로직
| Code Block | ||
|---|---|---|
| ||
Persist(saleInfo, _ =>
{
_state.totalSales += saleInfo.Price;
if (LastSequenceNr % 5 == 0) {
SaveSnapshot(_state.totalSales);
}
}); |
Persist()는 이벤트 소싱의 핵심: 이벤트를 저장한 후 상태를 변경5개의 Sale 이벤트마다
SaveSnapshot()호출 → 빠른 복구용
4. 종료 시그널 전달
| Code Block | ||
|---|---|---|
| ||
Sender.Tell(new StopSimulate());
taskCompletion.TrySetResult(true); |
매출 목표(
expectedProfit)에 도달하면, 더 이상 이벤트를 저장하지 않고 종료 시그널 전송TaskCompletionSource는 테스트나 외부 시뮬레이션 종료에 사용
5. 스냅샷 성공 처리
| Code Block | ||
|---|---|---|
| ||
Command<SaveSnapshotSuccess>(success => {
Console.WriteLine(...);
}); |
스냅샷이 성공적으로 저장되었을 때 로깅
필요시
DeleteMessages()같은 클린업 가능
6. 복구 처리 (Recover<>)
이벤트 복구
| Code Block | ||
|---|---|---|
| ||
Recover<Sale>(saleInfo => {
_state.totalSales += saleInfo.Price;
}); |
스냅샷 복구
| Code Block | ||
|---|---|---|
| ||
Recover<SnapshotOffer>(offer => {
_state.totalSales = (long)offer.Snapshot;
});
|
가장 최근의 스냅샷부터 시작 → 남은 이벤트만 재생
✅ 핵심 특징 요약
| 기능 | 설명 |
|---|---|
| 🎯 명령-이벤트 분리 | Sale은 명령(Command), Persist된 이벤트로 상태 변경 |
| 🧠 이벤트 소싱 기반 상태 | 상태 변경 내역은 전부 이벤트로 기록되어 리플레이 가능 |
| 💾 스냅샷 | 매 5건 이벤트마다 현재 상태 저장으로 복구 최적화 |
| 🔄 복구 내장 | 스냅샷 → 이벤트 순으로 복원함 |
| 🚦 목표 달성시 종료 | 외부 TaskCompletionSource로 흐름 제어 |
유닛테스트를 통한 시뮬레이터
| Code Block | ||
|---|---|---|
| ||
public class SalesSimulatorActor : ReceiveActor
{
private readonly IActorRef _salesActor;
private ICancelable scheduler;
public SalesSimulatorActor(IActorRef salesActor)
{
_salesActor = salesActor;
// Schedule the first sale simulation immediately and then every 2 seconds:
scheduler = Context.System.Scheduler.ScheduleTellRepeatedlyCancelable(TimeSpan.Zero,
TimeSpan.FromSeconds(2), Self, new StartSimulate(), Self);
Receive<StartSimulate>(HandleStart);
Receive<StopSimulate>(HandleStop);
}
private void HandleStart(StartSimulate message)
{
ConsoleHelper.WriteToConsole(ConsoleColor.Black,
$"About to simulate a sale...");
Random random = new Random();
string[] products = { "Apple", "Google", "Nokia", "Xiaomi", "Huawei" };
var randomBrand = products[random.Next(products.Length)];
var randomPrice = random.Next(1, 6) * 100; // 100, 200, 300, 400, or 500
var nextSale = new Sale(randomPrice, randomBrand);
_salesActor.Tell(nextSale);
}
private void HandleStop(StopSimulate message)
{
scheduler.Cancel();
ConsoleHelper.WriteToConsole(ConsoleColor.DarkRed,
"Simulation stopped");
}
} |
- Raven에 제공하는 가이드를 유닛테스트로 옮겨두었으며 여기서 설명된 SaleActor 이벤트소싱버전을 로컬에서 수행해볼수 있습니다.
...
- RavenDB를 로컬 구동하는 도커스크립트와 유닛테스트설정은 위와같이 IDE에 구성되어 유닛테스트를 통해 작동시킬수 있습니다.
이상 RavenDB을 Akka.net과 함께 이용해 이벤트 소싱을 구현하는 샘플을 간략하게 살펴보았으며
...






