스냅샷은 앞장 이벤트소싱에서도 부분적으로 사용이되었습니다. 스냅샷은 다양한곳에서 사용될수 있으며 그 컨셉은 간단합니다.
무수히 발생하는 이벤트로 인해 우리가 설계한 어떠한 객체의 상태는 지속적으로 변경된다는 점이며
상태를 기록하는 범위와 주기에따라 세가지로 구분되며, 그 차이를통해 스냅샷을 이해할수가 있습니다.
- 이벤트저장 : 상태복원은 연속된 이벤트 재생만을 통해가능하며 리플레이와같은 구현을 위해 이벤트 자체를 모두 기록해야하는것과
- 상태 히스토리 저장 : 모든 이벤트를 기록할필요없이 특정시점의 상태를 복원을 하면되기 때문에, 원하는 만큼 사진을 찍는 스냡삽과 같은 기능이 필요합니다.
- 마지막 상태만 저장 : 마지막 저장값만 보장함으로, 왜 변경되었고 바로 이전으로 돌리는것은 어렵습니다. 중앙DB를 이용한 일반적 개발 방식입니다.
범위가 넓고 주기가 짧을수록 성능을 고려해야하며, 스냅샷은 중간정도에 있는 저장 기능입니다.
연관 키워드 : difference between redo and snapshot
스냅샷 구현
@Component @Scope("prototype") public class SnapShotActor extends AbstractPersistentActor { private final LoggingAdapter log = Logging.getLogger(getContext().system(), "AbstractPersistentActor"); private Object state; private int snapShotInterval = 5; private int msgCnt = 0; @Override public String persistenceId() { return "ExamplePersistentActor-id-1"; } @Override public Receive createReceiveRecover() { return receiveBuilder(). match(SnapshotOffer.class, s -> { state = s.snapshot(); //상태복원 log.info("상태복원"); // ... }). match(String.class, s -> {/* ...*/}).build(); } // 스냅샷을 지원하는 메시지 정의 @Override public Receive createReceive() { return receiveBuilder(). match(SaveSnapshotSuccess.class, ss -> { SnapshotMetadata metadata = ss.metadata(); // ... }). match(SaveSnapshotFailure.class, sf -> { SnapshotMetadata metadata = sf.metadata(); // ... }). match(String.class, cmd -> { log.info("EventFired:"+cmd); if(cmd.indexOf("print") ==0 ) { log.info("상태확인:"+state); }else { msgCnt++; if (msgCnt % snapShotInterval == 0 ) { //이벤트가 ?회 발생할때마다,상태를 변경하고 스냅샷을 찍음 ( 테스트를 위한 임의 조건) state = cmd + "을 먹은 상태"; log.info("SaveSnapShot:" + state); saveSnapshot(state); } } }) .build(); } }
이벤트를 받게되면, 상태가 변경되게되며 자신이 원하는 타이밍에
카메라 셔터(saveSnapshot) 를 누르기만 하면됩니다.
여기서는 수많은 이벤트를 모두 저장하는것은 비효율적이니, 매 5번째 상태의 스냅샷정보를
유지하기로 하였다고 가정하였습니다.
스냅샷 복원
private Object state; @Override public Receive createReceiveRecover() { return receiveBuilder(). match(SnapshotOffer.class, s -> { state = s.snapshot(); // ... }). match(String.class, s -> {/* ...*/}).build(); } 또는 @Override public Recovery recovery() { return Recovery.create( SnapshotSelectionCriteria .create(457L, System.currentTimeMillis())); }
마지막 상태를 복원(SnapshotOffer)할수도 있고,
각 시간대별 스냅샷은 고유 아이디가 존재함으로
특정 스냅샷(SnapshotSelectionCriteria)을 복원할수도 있습니다.
스냅샷 테스트
protected void persistenceSnapShot() { new TestKit(system) {{ ActorRef probe = getRef(); Props snapShotActorProp = ext.props("snapShotActor"); System.out.println("snapShotActor 액터생성"); ActorRef snapShotActor = system.actorOf(snapShotActorProp, "snapShotActor"); System.out.println("event 생성"); snapShotActor.tell("커피", ActorRef.noSender()); snapShotActor.tell("사탕", ActorRef.noSender()); snapShotActor.tell("커피", ActorRef.noSender()); snapShotActor.tell("스테이크", ActorRef.noSender()); snapShotActor.tell("라면", ActorRef.noSender()); // <-- 복구기대 상태 snapShotActor.tell("사탕", ActorRef.noSender()); snapShotActor.tell("커피", ActorRef.noSender()); System.out.println("상태확인"); snapShotActor.tell( "print" , ActorRef.noSender()); expectNoMessage(java.time.Duration.ofSeconds(1)); System.out.println("snapShotActor 종료또는 비정상종료"); snapShotActor.tell( akka.actor.PoisonPill.getInstance() , ActorRef.noSender()); expectNoMessage(java.time.Duration.ofSeconds(1)); System.out.println("snapShotActor 마지막 상태 확인"); ActorRef snapShotActo2 = system.actorOf(snapShotActorProp, "eventActor"); System.out.println("상태 복원확인"); snapShotActo2.tell( "print" , ActorRef.noSender()); expectNoMessage(java.time.Duration.ofSeconds(1)); }}; }
샘플은 모든 이용자가 먹을때마다 이벤트가 발생하고,그 시식 결과에의해 어떠한 상태로 변경이 됩니다.
이벤트드리븐 의 예를 간단하게 설명하는것으로 대단한 기능이 있는것은 아닙니다.
단지 이벤트는 무수히 발생하며 모든것을 기록하고 이용하기 어려우니, 스냅샷은 특정한 이벤트 조건에따라(여기서는 단순하게 5번째)
사진을 찍게되며 , 모든 사용자의 청사진을 최근 몇개까지 운영할것인가를 성능과 복원과을 고려하여 설계에 반영할수 있다란것입니다..
스냅샷 시도에따른 반응 메시지
Method | Success | Failure message |
---|---|---|
saveSnapshot(Any) | SaveSnapshotSuccess | SaveSnapshotFailure |
deleteSnapshot(Long) | DeleteSnapshotSuccess | DeleteSnapshotFailure |
deleteSnapshots(SnapshotSelectionCriteria) | DeleteSnapshotsSuccess | DeleteSnapshotsFailure |
스냅샷은 다양한 이유로(디스크풀및 메모리풀등) 실패가 있을수 있으며
그에 대응하는 코드작성도 가능합니다.
원문 참고 :