Marten은 CQRS 아키텍처에 대해 더 좋아졌습니다.

in #kr-dev2 years ago

여기에서는 Event Sourcing에 대한 사전 지식을 아키텍처 패턴으로 가정하고 있습니다. Oskar Dudycz의 Introduction to Event Sourcing 교육 키트 또는 Derek Comartin의 이 비디오를 적극 권장 합니다. Event Sourcing과 밀접하게 연결된 CQRS 아키텍처 스타일 은 둘 다 다른 것이 없어도 유용하지만 여기서는 더 큰 CQRS 아키텍처 내에서 이벤트 소싱에 Marten을 사용하는 데 관심이 있다고 가정합니다.

따라서 더 큰 CQRS 아키텍처 스타일 내에서 지속성을 위해 Marten 과 함께 이벤트 소싱 스타일을 채택하고 있습니다. 대략적으로 말하면, 시스템 상태에 대한 모든 "쓰기"에는 다음과 같은 워크플로를 사용하여 CQRS 서비스에 명령 메시지를 보내는 작업이 포함됩니다.

명령 메시지를 처리하는 과정에서 명령 처리기(또는 HTTP 끝점)는 다음을 수행해야 합니다.

  1. 현재 워크플로의 상태를 나타내는 "쓰기 모델"을 가져옵니다. 이 예상된 "쓰기 모델"은 들어오는 명령의 유효성을 검사하고 다음을 수행하기 위해 명령 처리기에서 사용됩니다.
  2. 기존 상태 및 들어오는 명령을 기반으로 시스템의 상태를 업데이트하기 위해 게시해야 하는 후속 이벤트를 결정 합니다.
  3. 진행 중인 마튼 이벤트 스토어에 신규 이벤트 지속
  4. 새 이벤트의 일부 또는 전부를 비동기적으로 처리할 발신 전송에 게시할 수 있습니다.
  5. 동시성 문제를 처리합니다. 특히 다른 관련 명령이 동일한 논리적 워크플로에 대해 동시에 들어올 수 있는 상당한 가능성이 있는 경우에 처리합니다.

구현으로 전환할 때 디자인 패턴에 대한 논의나 개인적으로 .Net 또는 JVM 세계의 일반적인 CQRS 접근 방식에서 쓸데없는 크러프트라고 생각하는 내용을 대부분 우회할 것이라는 점에 유의하십시오. 즉, 이 코드에서는 리포지토리가 사용되지 않습니다.

시스템의 예로서 무엇보다도 의료 제공자가 근무 시간 동안 환자를 돕는 교대 근무 시간을 추적하는 새로운 온라인 원격 의료 시스템을 구축하고 있다고 가정해 보겠습니다. Marten의 "자체 집계" 지원 을 사용 하여 공급자 이동 상태의 단순화된 버전이 이 모델로 표시됩니다.

1

2

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

public class ProviderShift

{

    public Guid Id { get; set; }

    public int Version { get; set; }

    public Guid BoardId { get; private set; }

    public Guid ProviderId { get; init; }

    public ProviderStatus Status { get; private set; }

    public string Name { get; init; }

    public Guid? AppointmentId { get; set; }

    public static async Task<ProviderShift> Create(ProviderJoined joined, IQuerySession session)

    {

        var provider = await session.LoadAsync<Provider>(joined.ProviderId);

        return new ProviderShift

        {

            Name = $"{provider.FirstName} {provider.LastName}",

            Status = ProviderStatus.Ready,

            ProviderId = joined.ProviderId,

            BoardId = joined.BoardId

        };

    }

    public void Apply(ProviderReady ready)

    {

        AppointmentId = null;

        Status = ProviderStatus.Ready;

    }

    public void Apply(ProviderAssigned assigned)

    {

        Status = ProviderStatus.Assigned;

        AppointmentId = assigned.AppointmentId;

    }

    public void Apply(ProviderPaused paused)

    {

        Status = ProviderStatus.Paused;

        AppointmentId = null;

    }

    public void Apply(ChartingStarted charting) => Status = ProviderStatus.Charting;

}

다음으로 환자 약속이 끝난 후 "차트 작성" 활동을 완료하기 위해 공급자에 대한 사용자 스토리를 재생해 보겠습니다. 시퀀스 다이어그램과 각 명령 처리기에 대한 글머리 기호 목록을 보면 걱정해야 할 몇 가지 사항이 있습니다. 그러나 Marten이 지난 주 Marten v5.4에 도입된 몇 가지 새로운 기능을 오늘 (대부분) 다루었기 때문에 두려워하지 마십시오.

다음과 같은 간단한 명령으로 시작합니다.

1

2

4

public record CompleteCharting(

    Guid ShiftId,

    Guid AppointmentId,

    int Version);

Marten의 새로운 IEventStore.FetchForWriting<T>()API를 사용하여 기본 명령 처리기(단지 작은 ASP.Net Core Controller 끝점)를 채울 것입니다.

1

2

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

public async Task CompleteCharting(

    [FromBody] CompleteCharting charting,

    [FromServices] IDocumentSession session)

{

    var stream = await session

        .Events.FetchForWriting<ProviderShift>(charting.ShiftId);

    if (stream.Aggregate.Status != ProviderStatus.Charting)

    {

        throw new Exception("The shift is not currently charting");

    }

    stream.AppendOne(new ChartingFinished(stream.Aggregate.AppointmentId.Value, stream.Aggregate.BoardId));

    await session.SaveChangesAsync();

}

위에서 사용 된 FetchForWriting()방법은 몇 가지 다른 작업을 수행합니다.

  1. 낙관적 동시성 검사에 도움이 되도록 공급자 이동에 대한 현재 지속된 버전의 이벤트 스트림을 찾아 현재 문서 세션에 로드
  2. ProviderShift명령에서 나타나는 시프트 ID에 대한 집계 의 현재 상태를 가져옵니다 . 이 API는 문제의 집계가 원시 이벤트에서 즉석에서 계산해야 하거나 이전에 인라인 또는 비동기 프로젝션 에 의해 Marten 문서로만 유지되어야 하는 " 라이브 집계 "인지 여부에 대해 설명 합니다. 궁극적인 일관성 문제 를 피하기 위해 "쓰기 모델" 집합체를 인라인 또는 라이브로 사용하는 것이 좋습니다 .

동시성?!?

어려운 사실은 명령이 메시징 인프라에서 실수로 또는 부수적으로 서비스에 여러 번 전달되거나, 여러 사용자가 서로 다른 세션에서 동일한 작업을 수행하거나, 나처럼 서투른 사람이 실수로 버튼을 너무 많이 클릭하기 쉽다는 것입니다. . 어떤 식으로든 동시성 문제 에 대비하여 명령 처리기를 강화해야 할 수도 있습니다 .

의 사용 FetchForWriting<T>()은 실제로 낙관적 동시성 검사 를 설정합니다 . 다른 사람이 FetchForWriting<T>()및 에 대한 호출 사이에서 동일한 제공자 이동에 대해 명령을 성공적으로 처리하는 경우 트랜잭션을 중단하고 롤백 IDocumentSession.SaveChangesAsync()하는 Marten이 ConcurrencyException발생 합니다.SaveChangesAsync()

그래도 계속 진행하여 먼저 Marten에게 제공자 이동의 버전이 무엇인지 알려줌으로써 우리의 명령이 제공자 이동이 서버에 있다고 생각하는 것을 알려줌으로써 낙관적 버전 확인을 강화해 보겠습니다. 하지만 먼저 공급자 변경에 대한 변경 사항을 수집하는 클라이언트에 현재 버전을 다시 가져와야 합니다. ProviderShift위의 집계 를 다시 스캔하면 다음 속성이 표시됩니다.

1

public int Version { get; set; }

Marten v5.4의 또 다른 새로운 작은 기능을 통해 Marten 프로젝션 지원은 자동으로 a의 값 VersionProviderShift. ProviderShift이를 알고 인라인으로 업데이트 된다고 가정하면 ProviderShift이 작은 웹 서비스 엔드포인트( Marten.AspNetCore 확장 사용 )를 사용하여 전체를 클라이언트에 전달할 수 있습니다.

1

2

4

5

[HttpGet("/shift/{shiftId}")]

public Task GetProviderShift(Guid shiftId, [FromServices] IQuerySession session)

{

    return session.Json.WriteById<ProviderShift>(shiftId, HttpContext);

}

Version 속성은 내부 또는 읽기 전용으로 범위가 지정된 필드일 수 있습니다. Marten은 이 집계가 나타내는 스트림의 최신 이벤트 버전을 설정하기 위해 필요한 모든 범위 지정 규칙을 우회할 수 있는 동적으로 생성된 Lambda를 사용하고 있습니다. Version명명 규칙을 명시적으로 무시하거나 완전히 다르게 명명된 멤버로 리디렉션할 수도 있습니다 . 마지막으로 .Net Int64유형일 수도 있습니다. 하지만 그렇게 하는 경우 먼저 해결해야 할 심각한 모델링 문제가 있을 수 있습니다!

명령 처리기로 돌아갑니다. 클라이언트가 사실상 "예상 시작 버전"을 가지고 있고 해당 버전으로 명령을 ProviderShift보내는 경우 CompleteCharting처리기 메서드 코드의 첫 번째 줄을 다음과 같이 변경할 수 있습니다.

1

2

4

5

var stream = await session

    .Events.FetchForWriting<ProviderShift>(charting.ShiftId, charting.Version);

이 새 버전은 ConcurrencyException예상되는 시작 버전이 데이터베이스에서 마지막으로 지속되는 버전과 동일하지 않은 경우 바로 실행됩니다. SaveChangesAsync()그 후 변경 사항을 커밋하기 위해 호출하는 시점에서 동일한 낙관적 동시성 검사 입니다.

마지막으로 Marten은 다른 많은 이벤트 소싱 도구와 같이 고유한 전문 스토리지 엔진이 되는 대신 실제 데이터베이스를 기반으로 구축되므로 마지막 요령이 하나 있습니다. 낙관적 동시성 검사를 사용하는 대신 특정 제공자 전환에 대한 비관적이고 배타적인 잠금으로 이동하여 한 번에 하나의 세션만 이 변형을 사용하여 해당 제공자 전환에 쓸 수 있도록 합시다.

1

2

4

5

6

var stream = await session

    .Events.FetchForExclusiveWriting<ProviderShift>(charting.ShiftId);

보시다시피 Marten에는 투사된 상태의 쿼리와 Marten의 동시성 제어를 사용해야 하는 명령 처리기 모두에서 이전에 반복되었던 일부 코드를 제거하여 CQRS 아키텍처 내에서 Marten을 훨씬 더 쉽게 사용할 수 있도록 하는 몇 가지 새로운 기능이 있습니다.

잠깐만요, 너무 빠르지 않아요, 당신은 몇 가지를 놓쳤어요!

위의 샘플 코드에서 몇 가지 중요한 사항을 놓쳤습니다. 우선, 우리는 다른 시스템이나 우리 자신의 시스템이 다른 작업을 비동기적으로 수행할 수 있도록 일종의 서비스 버스를 통해 새로운 이벤트를 브로드캐스트하기를 원할 것입니다(제공자를 다른 준비된 환자 약속에 할당하려고 시도하는 것과 같은). 이벤트 캡처와 게시되는 나가는 이벤트가 하나의 원자적 작업에서 함께 성공하거나 실패하도록 안정적으로 수행하려면 Marten에 통합된 일종의 " 발신함 "이 정말 필요합니다 .

또한 동시성 예외에 대한 모든 종류의 잠재적인 오류 처리 또는 메시지 재시도 기능을 생략했습니다. 그리고 마지막으로 (직접 생각할 수 있는) 어떤 종류의 성인 시스템에서든 원하는 계측에 대한 논의를 완전히 생략했습니다.

우리는 NBA 플레이오프 한가운데에 있기 때문에 Shaquille O'Neal의 백업이 Alonzo Mourning이었을 때 인용한 말이 생각납니다. Mourning은 벤치 밖에서 멋진 경기를 펼쳤습니다. 폐선." 이 경우 Marten의 미래 중 일부는 Jasper 라는 다른 프로젝트와 결합되어 Marten이 CQRS 아키텍처를 위한 전체 스택을 만들기 위해 강력한 아웃박스 구현과 함께 외부 메시징을 추가할 것입니다. 빠르면 다음 주 말이나 적어도 6월에는 이 게시물의 큰 누락 부분을 다루는 Marten + Jasper 조합을 보여주는 후속 글을 작성할 것입니다.

출처 : https://jeremydmiller.com/2022/05/31/marten-just-got-better-for-cqrs-architectures/

Sort:  

[광고] STEEM 개발자 커뮤니티에 참여 하시면, 다양한 혜택을 받을 수 있습니다.