Fusion - slog

먼저 뭔가를 파악하기 위해 프로그래머는 코드를 실행해보는게 빠릅니다.

git clone GitHub - servicetitan/Stl.Fusion.Samples: A collection of samples for Fusion library: https://github.com/servicetitan/Stl.Fusion

퓨전에 대해 대략적으로 살펴봅시다.

일단 Overview만 봐도 머리에 쥐가 나려고 합니다. 먼저 개념적인 접근을 해봅니다.

Fusion은 "Real-time User Interface"를 제공하는 환경입니다. 일반적으로 이런 환경을 제공하기 위해선 코드가 상당히 복잡해지고, 작업량이 많습니다. 그리고 페이지마다 기능 구현을 할 때 숙련된 개발자가 아니면 일관되게 작성하기가 어렵고, 그 안에서 다양한 버그가 발생합니다. 관련해서 페이스북이나 트위터 등의 서비스들은 이런 고통을 이미 경험했습니다.

Fusion을 이용하면 "Real-time User Interface"를 구현해야 할 개발자가 캐싱, 무효화, 최종 일관성 관점에서 작업해야 할 상당한 코딩량을 줄여주는 것으로 보입니다. (아직 파악이 덜 되어서 개념적으로 접근)

분산 컴퓨팅 서비스, 투명 추상화의 특징으로, SignalR 없이 Fusion만으로 서버와 클라이언트가 소통할 수 있습니다. 그리고 투명 추상화로 인해, 기존 코드를 거의 그대로 사용할 수 있게 됩니다.

1개의 좋아요

Fusion은 2020년 10월부터 시작된 아주 따끈한 프로젝트 입니다. 그렇기 때문에 샘플코드도 record등도 적극적으로 사용되어 있어서 C# 9을 배우는데 좋은 샘플이 됩니다.

1개의 좋아요

다음의 온라인 샘플을 통해 Fusion에 대해 좀 더 잘 알 수 있습니다.

Stl.Samples.Blazor (servicetitan.com)

Board Games (alexyakunin.com)

샘플에서 Blazor Server 및 Webassembly 모드를 둘다 지원한다는 것은 인상깊습니다. Fusion을 이용한 코드가 Blazor 모드와 상관없이 거의 같다는 것을 의미합니다.

1개의 좋아요

일단 코드 감각을 느끼기 위해 간단한 Blazor 앱을 만들고자 합니다. 이 앱은 기본 템플릿으로 생기는 클릭을 공유하는게 될 텐데요, 물론 샘플을 보고 따라할 겁니다. 그러면서 익혀 보죠

샘플의 위치는, 위 git clone으로 내려받은 샘플의 src/Blazor/HelloBlazorServer 입니다

먼저 NuGet 참조를 봅시다.

  • Stl.Fusion.Client와 Stl.Fusion.Server로 각각 Blazor Webassembly와 Server 패키지를 설치할 수 있습니다. 아무래도 Blazor Server가 구조가 단순할 것이므로 이것으로 진행해봅시다.

먼저 Blazor Server로 새로운 프로젝트를 생성합니다. 최초 기본으로 구성되어 있는 화면은 다음과 같습니다. 웹브라우저 두개로 띄워보겠습니다.

image

실행해보면 왼쪽과 오른쪽의 카운터는 서로 공유되지 않습니다. 실시간성을 가지려면 이화면으로 간단히 설명해볼 때,

왼쪽 버튼을 클릭하면 당연히 왼쪽의 카운트는 증가할 텐데, 오른쪽 창으로 이 정보를 보낼 메커니즘이 없습니다. 즉,

왼쪽 버튼을 누름 → 자신의 카운트를 증가함 → 카운트가 증가했음을 같은 세션의 구성원에게 전파함 → 같은 세션의 다른 구성원이 변경되었음을 인식함 → 자신의 카운트를 변경된 값으로 동기화 함

뭐 이런 메커니즘으로 실시간 변화를 적용하게 되는데요, Fusion은 어떻게 이를 처리하는지 살펴봅시다.

1개의 좋아요

NuGet을 통해 Stl.Fusion.Server를 설치합니다.

그런 다음 Startup.csFusion 관련 설정들을 합니다.

        public void ConfigureServices(IServiceCollection services)
        {
...
            // Fusion services
            services.AddSingleton<IUpdateDelayer>(_ => new UpdateDelayer(0.5));
            services.AddFusion(fusion => {
                fusion.AddFusionTime(); // IFusionTime is one of built-in compute services you can use
            });
            // This method registers services marked with any of ServiceAttributeBase descendants, including:
            // [Service], [ComputeService], [CommandService], [RestEaseReplicaService], etc.
            services.UseAttributeScanner().AddServicesFrom(Assembly.GetExecutingAssembly());
...

나머지는 동일한 것 같군요. 여기서 IUpdateDelayer는 수정된 내용을 몇초뒤에 반영할지에 대한 싱글톤 서비스를 등록합니다. (이게 어떤 메커니즘으로 반영되는지는 아직은 잘 모르겠습니다)
그리고 AddFusion()을 통해 Fusion 관련 기능들을 추가할 수 있는것으로 보입니다. 샘플에는 FusionTime기능을 추가하였습니다. (갱신된 시각 기준으로 시간을 측정하는 기능으로 보입니다.)

그리고 UseAttributeScanner()는 Fusion에서 사용하는 Attribute를 검색하는 것으로 보이는데요, AddServicesFrom()을 통해 현재 실행 어셈블리에서만 찾는 코드입니다.

1개의 좋아요

App.Razor나 _Host.cshtml, 심지어 MainLayout.razor까지 달라지는건 없습니다.

먼저 Counter.razor를 살펴보죠

Computed 상태를 관리해야 하기 때문에 기존 Component로는 안됩니다. 그래서 Fusion에서는 ComputedStateComponent를 사용하는데요,

@inherits ComputedStateComponent<string>

Counter.razor를 더 보기 전에 CounterService.cs를 봅시다.

    [ComputeService]
    public class CounterService
    {
        private readonly object _lock = new object();
        private int _count;
        private DateTime _changeTime = DateTime.Now;

        [ComputeMethod]
        public virtual Task<(int, DateTime)> Get()
        {
            lock (_lock) {
                return Task.FromResult((_count, _changeTime));
            }
        }

        public Task Increment()
        {
            lock (_lock) {
                ++_count;
                _changeTime = DateTime.Now;
            }
            using (Computed.Invalidate())
                Get();
            return Task.CompletedTask;
        }
    }

두사람이 해당 페이지에 접속했을 때 CounterService의 인스턴스는 총 몇개일까요? (아직 저도 정확히는 모르겠지만, 개념상으로는 하나여야 합니다)

1개의 좋아요

흠. 코드를 해석하기 위해 조금 더 학습이 필요합니다. 다음의 튜토리얼을 따라가 봅시다.

Part 0: NuGet packages
Part 1: Compute Services
Part 2: Computed Values and IComputed<T>
Part 3: State: IState<T> and Its flavors
Part 4: Replica Services
Part 5: Caching and Fusion on Server-Side Only
Part 6: Real-time UI in Blazor Apps
Part 7: Real-time UI in JS / React Apps
Part 8: Scaling Fusion Services
Part 9: CommandR

1개의 좋아요

Fusion을 공부하면서 샘플을 따라 하는것 만으로는 Fusion을 이해하는데 부족함이 있었습니다. 그래서 제공하는 튜토리얼을 따라가게 되었습니다.

Fusion이 제공하는 여러 기능중에서 가장 핵심적인 기능은 Compute Service로 자연스럽게 구현할 수 있는 캐싱 기능입니다.

일반적인 캐싱은 원자가 무효화(다시 계산되어야 할)가 되었을 때 캐싱 트리를 다시 계산합니다. 이 전략은 작은 규모의 서비스에서는 잘 동작하지만 사용자가 많아지거나 규모가 큰 서비스에서는 그리 효율적이지 못합니다. 그래서 Fusion은 반대의 전략을 선택했습니다.

전체를 캐싱하되, 원자적 무효화가 발생하더라도, 전체를 다시 계산하지 않습니다. 이것은 큰 규모의 서비스에서 필요로 하는 캐싱에서 유용한데, 캐싱 트리 안에서 무효화가 빈번히 발생할 여지가 상당하기 때문입니다.

1개의 좋아요

Fusion은 Console 응용 프로그램에서도 캐싱기능을 테스트 해 볼 수 있습니다.

.NET 5 에서 테스트 했습니다.

dotnet new console
Install-Package Stl.Fusion

주석을 풀고 하면서 동작을 확인해보시죠!

using Microsoft.Extensions.DependencyInjection;
using Stl.Async;
using Stl.DependencyInjection;
using Stl.Fusion;
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Threading.Tasks;
using static System.Console;


var counters = CreateServices().GetRequiredService<CounterService>();
for (var i = 0; i < 5; i++)
    WriteLine(await counters.Get("a"));

counters.Increment("a");

for (var i = 0; i < 5; i++)
    WriteLine(await counters.Get("a"));

//var services = CreateServices();
//var counterSum = services.GetRequiredService<CounterSumService>();
//WriteLine(await counterSum.Sum("a", "b"));
//WriteLine(await counterSum.Sum("a", "b"));

//var services = CreateServices();
//var counterSum = services.GetRequiredService<CounterSumService>();
//WriteLine("Nothing is cached (yet):");
//WriteLine(await counterSum.Sum("a", "b"));
//WriteLine("Only GetAsync(a) and GetAsync(b) outputs are cached:");
//WriteLine(await counterSum.Sum("b", "a"));
//WriteLine("Everything is cached:");
//WriteLine(await counterSum.Sum("a", "b"));

//var services = CreateServices();
//var counters = services.GetRequiredService<CounterService>();
//var counterSum = services.GetRequiredService<CounterSumService>();
//WriteLine(await counterSum.Sum("a", "b"));
//counters.Increment("a");
//WriteLine(await counterSum.Sum("a", "b"));

static IServiceProvider CreateServices()
{
    var services = new ServiceCollection();
    services.AddFusion();
    services.UseAttributeScanner().AddServicesFrom(Assembly.GetExecutingAssembly());
    return services.BuildServiceProvider();
}

[ComputeService]
public class CounterService
{
    private readonly ConcurrentDictionary<string, int> counters = new();

    [ComputeMethod]
    public virtual async Task<int> Get(string key)
    {
        WriteLine($"{nameof(Get)}({key})");

        await Task.Delay(500);

        return counters.TryGetValue(key, out var value) ? value : 0;
    }

    public void Increment(string key)
    {
        WriteLine($"{nameof(Increment)}({key})");
        counters.AddOrUpdate(key, k => 1, (k, v) => v + 1);
        using (Computed.Invalidate())
        {
            Get(key).Ignore();
        }
    }
}

[ComputeService]
public class CounterSumService
{
    public CounterService Counters { get; }

    public CounterSumService(CounterService counters) => Counters = counters;

    [ComputeMethod]
    public virtual async Task<int> Sum(string key1, string key2)
    {
        WriteLine($"{nameof(Sum)}({key1}, {key2})");
        return await Counters.Get(key1) + await Counters.Get(key2);
    }
}
1개의 좋아요

위의 소스코드를 간단히 설명하겠습니다.

  • ComputeServiceAttribute에 의해 CounterServer는 Compute Service가 됩니다.
  • ComputeMethodAttribute에 의해 Get() 메소드는 Compute Method가 됩니다.
  • Fusion에 의해 Compute Service와 Method는 관리됩니다.
  • Compute Method는 virtual async Task<TResult> MethodName(params1, params2, ...) 형태를 가져야 합니다.
    • Fusion에 의해 이 메소드는 자동으로 재정의되어 IComputed<TResult> 상태를 가지게 되고, 이것을 통해 캐싱처리를 하게 됩니다.
    • 이 때, 캐싱 기준(Key)은 메소드정보, 인스턴스, 인자가 됩니다.
    • Fusion이 자동으로 재정의 하기 위해 반드시 virtual 키워드가 있어야 합니다.

Get() 메소드는 오래 걸리는 수행을 시뮬레이션 하기 위해 Task.Delay(500)으로 0.5초 딜레이를 주었습니다. 결과는 다음과 같습니다.

Get(a)
0
0
0
0
0
Increment(a)
Get(a)
1
1
1
1
1

이것의 의미는 무엇일까요? 최초 값이 계산된 후 이후 캐싱된 값을 바로 반환하는 것을 볼 수 있습니다. (0.5초의 처리 소요시간이 캐싱을 통해 없어졌습니다!)

그런데, 반환해야 할 값은 Increment()메소드에 의해 영향을 받게 되는데요, Fusion에서는 명시적으로 무효화 코드를 using (Computed.Invalidate()) { ... } 블럭 안에서 한번은 호출하는것으로 처리합니다. (Ignore()는 컴파일 경고를 없애기 위한 Fusion에서 제공하는 일종의 방편입니다.)
Fusion은 이 블럭(ComputeContextScope)의 Compute Method를 무효화 합니다.

두번째 Compute ServiceCounterSumService를 볼까요?

Fusion은 DependencyInjection을 통해 생성인자로 다른 Compute Service 인스턴스를 얻을 수 있습니다. (이 즈음부터 Compute Service는 싱글톤 인스턴스임이 자명해집니다)
그리고 ComputeAttribute를 통해 Compute Method 조합으로 새로운 Compute Method를 만들 수 있도록 디자인 되었습니다. (이때에도 동일한 캐싱 기준(Key)는 메소드정보, 인스턴스, 인자가 됩니다)

즉, 인자 (“a”, “b”)와 (“b”, “a”)는 다른 캐싱 대상이 됩니다.

코드 주석을 풀고 결과를 확인해볼까요?

Nothing is cached (yet):
Sum(a, b)
Get(a)
Get(b)
0
Only GetAsync(a) and GetAsync(b) outputs are cached:
Sum(b, a)
0
Everything is cached:
0

실행해보시고 그 의미가 이해된다면, Fusion에서 제공하는 캐싱 기능의 막강함을 느낄 수 있고 Service-Client 모델에 성능을 향상시킬 수 있는 다양한 응용이 가능하다는 것을 느끼실 수 있습니다.

단지 캐싱부분만 분석했을 뿐인데, 다른 여러 굵직한 기능까지 해서 2020년10월부터 시작한 프로젝트라는게 믿겨지지가 않는군요!

2개의 좋아요

튜토리얼에 좀 더 상세한 내용이 있으니 Compute ServicesComputed Values and IComputed<T>를 자세히 볼 필요가 있습니다.

그다음으로 State에 대해 알아보겠습니다.

튜토리얼에 의해 Fusion의 두가지 핵심 개념에 대해 이제 알게 되었는데요,

  • Compute Service와 서비스의 조합을 통해 한번만 계산하면 조합된 값 중의 하나가 무효화가 될 때까지 계속 캐싱된다는 것을 알 수 있고요,
  • 이러한 모든 종속성을 추척하는 Fusion이 구현하는 IComputed 에 대해서 알게 되었습니다.

이제 IState<T>에 대해 알 필요가 있는데요, IState<T>는 즉 “상태” 입니다.

Fusion에서 계산된 값은 변경할 수 없습니다. 계산되었으므로 무효화 될 때까지 "불변성"을 가집니다.

IState<T>는 최신 버젼을 “추적” 합니다. 이에 따라 다르게 동작하는 유형을 살펴보겠습니다.

  • IIMutableState
  • IComputedState
  • ILiveState
  • ILiveState<T, TLocals>
2개의 좋아요
1개의 좋아요

아마도 Stl.Fusion의 사용자가 한국에 저 밖에 없을 수도 있겠습니다 ^^;

Stl.Fusion은 이해하고 쓰기에는 러닝커브가 있는 실시간 서비스를 만드는 라이브러리 입니다. 특히나 만드신 분의 가이드 문서가 대략 이해시키고 바로 쓸 수 있도록의 관점이 아니라 깊게 이해하고 활용할 수 있도록 하는 문서라 실시간 앱을 만들어본 경험이 없을 경우 되려 상당히 난해하기도 합니다.

그래서 저는 여러분에게 Stl.Fusion을 사용해서 ASP.NET Core Web API + 윈폼으로 채팅 프로그램 간단히 만들어서 소개해보려고 합니다.

3개의 좋아요

image

Stl.Fusion으로 간단히 만든 퓨전 챗

3개의 좋아요

관련 블로그는 곧 공유할 수 있도록 하겠습니다;;

1개의 좋아요

다음은 Uno를 이용해 다양한 플랫폼의 앱으로 (Webasembly 포함!) 캠퍼스를 공유해 같이 그림을 그리는 예제를 진행해보겠습니다.

2개의 좋아요

퓨전을 적용하기 앞서서 Uno로 윈도우 및 웹브라우저에서 동작하는 드로윙 앱을 만들었습니다.

펜, 펜 색 및 굵기 선택, 지우개, 전체 지우기 기능의 기본 기능만 구현하였습니다.

| 윈도
image

| 웹브라우저 (웹어셈블리)
image

3개의 좋아요