Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Info

액터모델의 메일박스로 통해 전달되는 이벤트들은 다양한 DB장치를 이용해 영속화가 가능하며 상태프로그래밍을 할수 있습니다.
RDB,MongoDB,ElasticSearch등 다양한 특징을 가진 전통강호 영속장치를 이용해 각각 특화된 기능을 이용할수 있겠지만
이러한 기능을 한방에 보편적으로 이용할수 있는 장치가 없을까 조사하다 발견한 DB로 Akka를 연구하는 NetCorelabs에 변종실험 대상으로 추가하였습니다.

Image Modified

데이터베이스는 지루하면 안된다라는 슬로건을 내건 모던 DB

전통 데이터베이스의 문제는 전문화된 기능을 각각 가지고 안정적으로 발전해온 그자체가 아니라 서비스가 확장되고 다양한 분석기능으로 확장되고

AI시대 특히 RAG를 강화 하기위해 다음과 같은 특화된 DB를 모두 필요로 하게됩니다. 

  • RDB
  • Elastic Search
  • Mongo Document DB
  • Neo4J Graph DB
  • Vector DB

전통적 DB를포함 특화된 NoSQL 모두 다뤄 필연적으로 발생하는 불가피한 작업을 Boring 이라 표현하는것같습니다.

다양한 DB를 관리함으로 발생하는 관리비용뿐만 아니라~ 데이터가 이동됨에 따른 가공과정의 파이프라이닝을 다루는  높은수준의 ETL 능력을 가진 팀을 필요로하게됩니다. 

필요로하는 ETL 파이프라이닝

Code Block
themeEmacs
[RDB / Mongo]     [File Upload]        [API]
     │                │                  │
     ▼                ▼                  ▼
 ┌─────────┐    ┌──────────┐     ┌─────────────┐
 │ Debezium│    │ Fluent Bit│     │ REST Events │
 └─────────┘    └──────────┘     └─────────────┘
       ▼                ▼                 ▼
               ┌─────────────────────┐
               │    Apache Kafka     │
               └─────────────────────┘
                     ▼
         ┌─────────────────────┐
         │  Flink / Beam / NiFi│  ← 변환, 정제
         └─────────────────────┘
        ▼      ▼         ▼       ▼
  [Elastic] [Mongo] [Neo4j] [Vector DB]

데이터 엔지니어팀이 이미 존재하다면  이러한 파이프라인을 이미 구축하고 그 기반으로 성장을 가속화를 했을테지만

소규모로 구성된 스타트업은 핵심 서비스도 개발하면서 동시에 고도화된 데이터파이프라이닝을 구축할 여력이 없습니다.

대안으로 클라우드가 제공하는 파이프라인툴은 Nosql과 연동되어 ETL플랫폼을 제공하며 그 자체로  휼륭하지만

트래픽에 따른 인프비용 증가를 통제하기 쉽지 않습니다. 

AI OPS 등장

Image Removed

AI 를 활용하는 AI OPS의 등장으로 MSA의 API GateWay는 Context Gateway로 전환을 준비하고 기중중심에서 문맥중심으로 활용해야할 DB와 인프라는 더 복잡해자고

활용 인프라아키텍처는 더 복잡해지고 더 많은 이벤트를 처리할것이 분명해 보입니다. 

✅ 1. Next 아키텍처의 키워드

핵심 키워드

설명

AI-NativeAI가 모든 서비스 흐름에 기본 내장됨 (예: 추천, 예측, 분류)
Context-Aware사용자, 시스템 상태에 따라 유동적으로 동작 (MCP 적용)
Event-Driven모든 변화가 이벤트로 감지/처리됨 (Kafka, NATS)
Composable기능을 블록처럼 재조립 (Low-code, Function as a Service)
Autonomous Ops스스로 모니터링, 복구, 확장하는 인프라
Multi-Agent Collaboration

여러 AI Agent가 분산되어 협력 작업 수행

전통RDB진영 또한 Nosql기능을 탑재해가며 발전해 나가겠지만~ 스몰비즈니스 개발팀이 각각 특화된 모든 DB를 다루고 복잡한 파이프라인을 다룰 역량을 가지기 까지

너무오랜 시간이 걸리기때문에  다양한 데이터베이스를 다룸으로 발생하는 복잡성및 비용을 줄이기위해  하루가 다르게 변화하는 AI의 인기에는 가려질수 있겠지만

조용하게 이용이 될것으로 예상해봅니다. ( 전통DB를 필수로 사용해야하다는 인식의 전환)

서론이 길었으며~ 다양한 하이브리드 모던 DB중 하나인 RavenDB의 특징과 사용법 그리고 Akka.net에서의 확장 사용법을 간단하게 알아보겠습니다.

✅ RavenDB 특징

항목설명
Document StoreJSON 기반의 문서 저장 (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는 가능, 복잡한 조인과 트랜잭션은 제한적정형 테이블 기반보다는 문서 중심 모델 필요
Code Block
themeEmacs
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 환경

Image Removed

RavenDB 웹관리툴

Image Removed

  • DB를 관리할 웹툴을 포함 기본제공하기 때문에 

언어별 지원 클라이언트

Image Removed

  • JAVA또는 Node.js가 일반적으로 첫번째 지원인경우가 많은데(노출순) .NET이 처음인것으로보아 다소 .NET친화적인 DB인듯.....

RabenClient for .NET

✅ RavenDB 특징

항목설명
Document StoreJSON 기반의 문서 저장 (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
themeEmacs

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 환경

Image Added

  • Docker와 통합된 IDE환경으로 RavenDB 구동
  • 닷넷개발 IDE VisualStudio가 개인적으로 익숙하지만 최근 젯브레인진영 Rider를 이용해보고 있습니다. 

RavenDB 웹관리툴

Image Added

  • DB를 관리할 웹툴을 포함하며 기본적인 관리가 가능합니다.
  • IDE내에서 연동돠는 플러그인은 아직 없어보입니다.
    • Postgres 호환플러그인을 지원하니 BI툴및 IDE내 기본조회연동은 가능할듯
  • AI Hub는 DB펑션을 다양한 AI와 연결해 쿼리와 통합해 AI기능 활용가능한 것으로 보여집니다. - 개발1개팀이 필요한 AIOps-ETL을 구축할필요없어보임

 

언어별 지원 클라이언트

Image Added

  • Akka.net을  연계해 사용예정이기때문에 .NET 기반실험이 진행되었습니다.
  • 지원 클라이언트가 소개될때 .NET 라이브러리가 첫번째에 잘 노출되는 일이 없는데~ 닷넷친화적 DB로 추정해봅니다.

 

RavenClient for .NET

Image Added

  • 설치된 서버 Mazor버전과 맞춰서 패키지 설치

 

Database생성

Image Added

  • 몽고DB와 유사하게 스키마리스 DB이기때문에 DDL코드가 필요로 하지 않습니다. 엔티티를 관리해야할 부담을 덜수 있습니다.
  • 처음이용할때 DBManageMent Tool을 한참 찾았는데~ 웹에 통합되어 대부분의 관리기능을 웹을 통해 가능합니다. - 로컬설치버전 기준 

Repository 코드

Code Block
themeEmacs

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
themeEmacs

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);
    }
    
}
  • 처음 사용하는 DB사용 자체를 연구하고 테스트하는 코드이기때문에 Mocking을 이용하지 않고 로컬구축 DB직접 Access방식이용
  • 도큐먼트 DB인 몽고DB를 ORM으로 이용하는 느낌과 유사한듯 합니다.

테스트 수행및 확인

Image Added

Image Added

  • 비교적 심플한 코드작성으로 CRUD 테스트 수행이 완료되었습니다.

 

단일 DB에서 다양한 검색수행

반경검색/FullText검색및 Vector검색등 고급 검색기능을 단일DB만사용해  검색할수 있습니다.

Code Block
themeEmacs

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);
    }
}

Image Added

CQRS로의 여정

CRUD또는 검색기능만 이용하려고 RavenDB를 조사한것은 아니며 Akka.net의 Persist기능이 제공하는 CQRS장치와 연동이됨으로

이벤트 소싱이 필요한 기능을 만들고자 할때, 가장 심플한 방식으로 구현하고 강력하게 이용할수 있는 방식중 하나가 될수 있습니다.

Image Added

 

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

Image Added

Image Added

  • 이벤트가 발생할때 즉각 CRUD하여 이용하는 패턴이 아닌 액터모델의 메일박스를 이용해 상태있는 프로그래밍을 통해 이벤트소싱을 설계할수 있으며~ 이러한 느낌

 

3. 장점 요약

✅ 액터모델 기반의 장점

  • 상태 격리: 각 액터는 고유 상태를 가지며 병렬 처리에 유리함.

  • 비동기 메시지 기반 처리: 병목 없이 고성능 분산 처리 가능.

  • 자연스러운 도메인 분리: 액터 자체가 DDD의 Aggregate Root 역할을 하기에 적합.

✅ CQRS & Event Sourcing의 장점

  • 이벤트 이력 관리: 모든 상태 변화가 이벤트로 저장되므로 과거 상태 추적 가능 (감사, 재처리).

  • 읽기/쓰기 분리: 쿼리와 명령이 분리되어 성능 최적화 가능.

  • 스냅샷 사용 가능: 복구 속도 향상.

  • 스케일 아웃 용이: 각 액터가 독립적으로 배포/스케일링 가능.

✅ Akka.NET 전용의 추가 장점

  • Akka.Cluster와 연계 시 분산 시스템 구성도 가능

  • Akka.Remote로 마이크로서비스 간 통신에도 활용 가능

  • Pluggable Journal Backend: SQL, NoSQL, 이벤트 스토어 등 다양한 저장소와 연동 가능

...

✨ 실사용 시나리오 예

  • 고객 주문 시스템: OrderActorPlaceOrderCommand를 받고 OrderPlacedEvent를 생성 및 저장

  • 챗봇 세션 관리: SessionActor가 메시지를 이벤트로 저장하여 상태 기반 대화 흐름 관리

  • 금융 거래 기록: AccountActorWithdrawn, Deposited 이벤트를 기록하여 완전한 거래 이력 확보

 

 

SalesActor

Code Block
themeEmacs
linenumberstrue

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}");
        });
    }
}

SalesActor 클래스는 **Akka.NET의 ReceivePersistentActor**를 상속하여 **이벤트 소싱(Event Sourcing)**과 스냅샷(Snapshot) 기능을 활용해 상태를 안정적으로 유지하는 **영속 액터(Persistent Actor)**입니다. 아래에 코드 전체 흐름과 구성 요소를 설명드리겠습니다.

...

🔍 전체 구조 분석

📌 클래스 목적

  • 판매 이벤트(Sale)를 처리하고 누적된 매출을 상태로 유지

  • 특정 조건(예: 매출 목표 달성)에 도달하면 처리 중단

  • RavenDB 등을 백엔드로 사용해 이벤트를 저장

  • 일정 횟수마다 스냅샷 저장하여 복구 최적화

🧱 주요 구성 요소 분석

1. PersistenceId

Code Block
themeEmacs

public override string PersistenceId => "sales-actor";

 

  • 액터의 고유 식별자로 이벤트와 스냅샷을 식별하기 위한 키

  • RavenDB 또는 다른 Akka.Persistence backend에서 이 키로 데이터를 저장/조회함

 

2. 상태 관리 변수

Code Block
themeEmacs

private SalesActorState _state;

 

  • 현재 액터 상태를 저장하는 내부 객체

  • 여기서는 totalSales (누적 매출액)를 관리

3. 명령 처리 (Command<Sale>)

Code Block
themeEmacs

Command<Sale>(saleInfo => { ... });
  • 외부에서 Sale 메시지를 받으면 처리

  • 조건에 따라 이벤트를 Persist하거나 무시

 

주요 로직

Code Block
themeEmacs

Persist(saleInfo, _ =>
{
    _state.totalSales += saleInfo.Price;
    
    if (LastSequenceNr % 5 == 0) {
        SaveSnapshot(_state.totalSales);
    }
});
  • Persist()는 이벤트 소싱의 핵심: 이벤트를 저장한 후 상태를 변경

  • 5개의 Sale 이벤트마다 SaveSnapshot() 호출 → 빠른 복구용

 

4. 종료 시그널 전달

Code Block
themeEmacs

Sender.Tell(new StopSimulate());
taskCompletion.TrySetResult(true);
  • 매출 목표(expectedProfit)에 도달하면, 더 이상 이벤트를 저장하지 않고 종료 시그널 전송

  • TaskCompletionSource는 테스트나 외부 시뮬레이션 종료에 사용

 

5. 스냅샷 성공 처리

Code Block
themeEmacs

Command<SaveSnapshotSuccess>(success => {
    Console.WriteLine(...);
});
  • 스냅샷이 성공적으로 저장되었을 때 로깅

  • 필요시 DeleteMessages() 같은 클린업 가능

 

6. 복구 처리 (Recover<>)

이벤트 복구

Code Block
themeEmacs

Recover<Sale>(saleInfo => {
    _state.totalSales += saleInfo.Price;
});

스냅샷 복구

Code Block
themeEmacs

Recover<SnapshotOffer>(offer => {
    _state.totalSales = (long)offer.Snapshot;
});


가장 최근의 스냅샷부터 시작 → 남은 이벤트만 재생

✅ 핵심 특징 요약

 

기능설명
🎯 명령-이벤트 분리Sale은 명령(Command), Persist된 이벤트로 상태 변경
🧠 이벤트 소싱 기반 상태상태 변경 내역은 전부 이벤트로 기록되어 리플레이 가능
💾 스냅샷매 5건 이벤트마다 현재 상태 저장으로 복구 최적화
🔄 복구 내장스냅샷 → 이벤트 순으로 복원함
🚦 목표 달성시 종료외부 TaskCompletionSource로 흐름 제어

 

 

유닛테스트를 통한 시뮬레이터

Code Block
themeEmacs

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");
    }
}
  • 액터모델은 액티브오브젝트의 패턴과 유사하며 카프카처럼 메일박스를 보유하고 능동적으로 작동할수 있는 특징이있어 상호작용 능동적 Agent를 만들게 될때도 유용할수있습니다.
  • 이러한 기능은 재귀호출함수로도 심플하게 구현가능하지만 네트워크확장및 분산처리시 별도의 장치를 추가구현해야할수있지만~ 코드변경없이 분산확장 장치를 지원합니다.

Image Added

  • Raven에 제공하는 가이드를 유닛테스트로 옮겨두었으며 여기서 설명된 SaleActor 이벤트소싱버전을 로컬에서 수행해볼수 있습니다.

이벤트 스토어 확인

Image Added

  • 이벤트 스토어가 객체자체를 다루기때문에 객체변경시 호환버전과 고성능 직렬화 낭비없은 저장소등은 부가과제로 akka persist tool에서 어느정도 신경을 덜쓰게하지만 중요한 개념입니다

여기서 설명하고 작동된 액터모델을 통해 작동된 이벤트 스토어를 확인할수 있습니다.

이벤트 소싱은 저널과 스냅샷이 함께 이용되며 CRUD대비 이벤트를 모두 저장하기때문에 데이터가 비대해질수 있으며

CRUD처럼 마지막 상태데이터를 CQRS방식으로 이용하려면 DurableState Persist를 이용할수도 있습니다.

DurableState는 이벤트 발생시 변화되는  객체의 마지막 상태를 저장하는것을 의미하며 

상태로 설계된 객체가 복원을 위해 이용될수 있으며 CRUD랑 유사하게 작동하며

Journal은 발생 이벤트를 모두 기록해 이벤트 소싱과 같이 시계열이 필요하고 이벤트 재생이 필요한곳에 이용될수 있습니다.

SnapShot은 Journal과 함께 이용되어  이벤트소스로부터 너무 많은 재생이 일어나지않게 방지를 하는 장치입니다. 

이러한 개발패러다임의 변화는 데이터중심적 기획및 설계도 중요하지만 이벤트드리븐 도입을 통한 이벤트 스토밍 방식으로 복잡한 도메인모델을 가속화하는 방법론으로 확장됩니다. -개발만 이것을 적용한다고 가치있게 작동하지않으며 DDD를 학습하는 개념으로 시작합니다.

적용할수 있는 도메인영역

  • 세그멘트분석기능에서 배치 대규모로 처리하는것이 아닌~ 세그멘트에 영향주는 이벤트는 저널로구성하고 계산되어야할 중요 세그멘트를 스트림으로 상태보유 -광고플랫폼
  • 채팅히스토리를 즉각 CRUD하는것이아닌 핵심기능을 실시간으로 작동시키고 파생된 이벤트를 저널로 작동하고 Read기능을 완전분리 -채팅플랫폼
  • 월렛의 입출금이력을 마지막값이 아닌 저널로 작동하고 재생을통해 최종월렛 잔여파악 -핀테크 
  • 장바구니를 담은것만 관리하는것이 아닌 빼고넣고를 저널로 구성해~ 장바구니를 통한 소비결정심리분석기능 -커머스

샘플코드

Image Added

  • RavenDB를 로컬 구동하는 도커스크립트와 유닛테스트설정은 위와같이 IDE에 구성되어 유닛테스트를 통해 작동시킬수 있습니다.

 

이상 RavenDB을 Akka.net과 함께 이용해 이벤트 소싱을 구현하는 샘플을 간략하게 살펴보았으며

여기서 설명된 내용은 Akka.net기반 액터모델을 중심으로한 다양한 샘플기능들을 살펴볼수 있습니다.  

 

 

 Image Removed