[Stl.Fusion] Replica ์„œ๋น„์Šค (feat. HelloBlazorHybrid)

Stl.Fusion(์ดํ•˜ Fusion)์€ ์‹ค์‹œ๊ฐ„์„ฑ ์•ฑ์„ ํšจ์œจ์ ์œผ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

์˜ค๋Š˜ ์‹œ๊ฐ„์€ ํด๋ผ์ด์–ธํŠธ์—์„œ Replica ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•ด Compute ์„œ๋น„์Šค์™€ ์œ ์‚ฌํ•˜๊ฒŒ ๋ฌดํšจํ™” ๋ฐ ์บ์‹œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ HelloBlazorHybrid ์ƒ˜ํ”Œ์„ ๋ถ„์„ํ•˜๋ฉด์„œ ์ง„ํ–‰ํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

Replica ์„œ๋น„์Šค์˜ ๊ธฐ๋ณธ์ ์ธ ์ดํ•ด๋Š” Fusion ํŠœํ† ๋ฆฌ์–ผ - 4๋ถ€: Replica ์„œ๋น„์Šค๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

๊ฐœ์š”

Fusion์€ ๋ถ„์‚ฐ ๋ฐ˜์‘ํ˜• ๋ฉ”๋ชจ์ด์ œ์ด์…˜ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ ์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์„ Fusion์—์„œ๋Š” DREAM์ด๋ผ๊ณ  ์ค„์—ฌ์„œ ๋งํ•ฉ๋‹ˆ๋‹ค.

Fusion ๊ฐœ์š” ๋ฌธ์„œ๋Š” ๋ฒˆ์—ญ์„ ์™„๋ฃŒํ•˜๋ฉด ์ด๊ณณ์— ๋งํฌํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

๋˜ํ•œ Fusion์€ Blazor Server์™€ Webassembly๊ฐ„ ๋‹จ์ผ ์ฝ”๋“œ๋ฒ ์ด์Šค๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ด Blazor๋ฅผ ์ด์šฉํ•ด ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“œ๋Š”๋ฐ ๋‘˜ ์ค‘ ์–ด๋Š ๊ฒƒ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€ ๊ณ ๋ฏผํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

Fusion ํŠœํ† ๋ฆฌ์–ผ - 6๋ถ€: Blazor ์•ฑ์˜ ์‹ค์‹œ๊ฐ„ UI ๋ฒˆ์—ญ์„ ์™„๋ฃŒํ•˜๋ฉด ์ด๊ณณ์— ๋งํฌํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

Replica ์„œ๋น„์Šค (๋ณต์ œ ์„œ๋น„์Šค)๋ฅผ ์ด์šฉํ•˜๋ฉด ํด๋ผ์ด์–ธํŠธ๋กœ ๋…ธ์ถœํ•ด์•ผ ํ•  API๋ฅผ Web API ํ˜•ํƒœ๋กœ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ณ  ๋ช‡ ๊ฐ€์ง€ ํ™˜๊ฒฝ ๊ตฌ์„ฑ์„ ํ†ตํ•ด ์„œ๋ฒ„์˜ Compute ์„œ๋น„์Šค๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ DREAM์˜ ์ด์ ์„ ๊ทธ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ฉด์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ƒ˜ํ”Œ ์ •๋ณด

Replica ์„œ๋น„์Šค

Fusion์˜ Replica ์„œ๋น„์Šค๋Š” Blazor Webassembly, ๋ชจ๋ฐ”์ผ ์•ฑ, ๋ฐ์Šคํฌํ†ฑ ์•ฑ, ์ฝ˜์†” ๋“ฑ์—์„œ Fusion์œผ๋กœ ๊ตฌ์„ฑ๋œ Compute ์„œ๋น„์Šค๋ฅผ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์ผํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณต์ œํ•˜๋Š” ์„œ๋น„์Šค ์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ํ™˜๊ฒฝ ๊ตฌ์„ฑ์ด ๋˜๋ฉด ์‚ฌ์šฉ๋ฒ•์€ ๊ฑฐ์˜ ๋™์ผํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

HelloBlazorHybrid ์ƒ˜ํ”Œ์„ ๋ถ„์„ํ•˜๋ฉด์„œ Replica ์„œ๋น„์Šค๋ฅผ ์ดํ•ดํ•˜๋„๋ก ํ•ฉ์‹œ๋‹ค.

HelloBlazorHybrid ์ƒ˜ํ”Œ

HelloBlazorHybrid ํ”„๋กœ์ ํŠธ๋Š” Fusion์ด ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์„ Blazor์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์„ฑ๋œ ์ƒ˜ํ”Œ์ž…๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ๋Š” Blazor Server ๋ฐ Webassembly์—์„œ ๋ชจ๋‘ ๋™์ž‘ํ•˜๋„๋ก ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ ๊ตฌ์„ฑ

  • Abstrations

    ์ธํ„ฐํŽ˜์ด์Šค ๋ฐ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. Blazor Server ๋ฐ Webassembly์—์„œ ๊ณตํ†ต์œผ๋กœ ํ•„์š” ํ•ฉ๋‹ˆ๋‹ค.

  • Server
    Blazor Server ๋ฐ Webassembly๋กœ ์ƒ˜ํ”Œ์„ ํ˜ธ์ŠคํŒ…ํ•ฉ๋‹ˆ๋‹ค.

  • Services
    ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. ํ˜ธ์ŠคํŒ… ์„œ๋น„์Šค ๋ฐ Compute ์„œ๋น„์Šค๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  • UI
    Blazor์˜ UI ์ž…๋‹ˆ๋‹ค. Blazor Server ๋ฐ Webassembly์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

Fusion ๊ตฌ์„ฑ

Fusion์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด Fusion ์„œ๋น„์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

| Server/Startup.cs, ConfigureServices()

...
        // Fusion
        var fusion = services.AddFusion();
        var fusionServer = fusion.AddWebServer();
        fusion.AddFusionTime(); // IFusionTime is one of built-in compute services you can use
        services.AddScoped<BlazorModeHelper>();

        // Fusion services
        fusion.AddComputeService<ICounterService, CounterService>();
        fusion.AddComputeService<IWeatherForecastService, WeatherForecastService>();
        fusion.AddComputeService<IChatService, ChatService>();
        fusion.AddComputeService<ChatBotService>();
        // This is just to make sure ChatBotService.StartAsync is called on startup
        services.AddHostedService(c => c.GetRequiredService<ChatBotService>());
...

์œ„์˜ ๊ตฌ์„ฑ์œผ๋กœ ์„œ๋ฒ„ ์ธก์˜ Compute ์„œ๋น„์Šค๊ฐ€ ๋“ฑ๋ก์ด ๋ฉ๋‹ˆ๋‹ค.

Compute ์„œ๋น„์Šค ๊ตฌํ˜„ ๋ฐ ์‚ฌ์šฉ๋ฒ•์€ Fusion ํŠœํ† ๋ฆฌ์–ผ - 4๋ถ€: Replica ์„œ๋น„์Šค๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

        // Shared UI services
        UI.Program.ConfigureSharedServices(services);

UI ๊ด€๋ จ ์„œ๋น„์Šค๋ฅผ ์ดˆ๊ธฐํ™” ํ•ฉ๋‹ˆ๋‹ค. ์œ„์˜ ๊ฒฝ์šฐ๋Š” Blazor Server์—์„œ ์‚ฌ์šฉ ํ•ฉ๋‹ˆ๋‹ค. Blazor Webassembly์˜ ๊ฒฝ์šฐ UI ํ”„๋กœ์ ํŠธ์˜ Program.cs์˜ Main()์—์„œ ConfigureServices() ์—์„œ ๋™์ผํ•œ UI.Program.ConfigureSharedServices(services)๋ฅผ ํ˜ธ์ถœํ•จ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ(์—ฌ๊ธฐ์„œ๋Š” ์›น๋ธŒ๋ผ์šฐ์ €์—์„œ ๋™์ž‘ํ•˜๋Š” Webassembly)์—์„œ ์„œ๋ฒ„์˜ Compute ์„œ๋น„์Šค๋ฅผ ํ˜ธ์ถœํ•˜๋ ค๋ฉด Proxy ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด Web API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

| UI/Program.cs, ConfigureServices()

...
        // Fusion service clients
        fusionClient.AddReplicaService<ICounterService, ICounterClientDef>();
        fusionClient.AddReplicaService<IWeatherForecastService, IWeatherForecastClientDef>();
        fusionClient.AddReplicaService<IChatService, IChatClientDef>();
...

AddReplicaService()๋ฅผ ํ†ตํ•ด ์„œ๋น„์Šค ์ธํ„ฐํŽ˜์ด์Šค์™€ Web API๊ฐ€ ์—ฐ๊ฒฐ์ด ๋ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  ์‹ค์ œ๋กœ ์ € ์ธํ„ฐํŽ˜์ด์Šค๋กœ ํ˜ธ์ถœํ•˜๋ ค๋ฉด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ๊ตฌํ˜„์ฒด๊ฐ€ ์žˆ์–ด์•ผ ํ•˜๋Š”๋ฐ Fusion์€ ์ด Proxy ๊ตฌํ˜„์ฒด๋ฅผ ๋Ÿฐํƒ€์ž„์—์„œ ๋งŒ๋“ค์–ด ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ด์ค๋‹ˆ๋‹ค. ์ด์ œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์›๊ฒฉ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ๋‹จ์ˆœํžˆ Proxy๋ฅผ ํ†ตํ•ด Web API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ๊ณผ ์–ด๋–ค ์ฐจ์ด๊ฐ€ ์žˆ์„๊นŒ์š”?

Replica ์„œ๋น„์Šค์˜ ๋™์ž‘

fusionClient.AddReplicaService<TService, TClient>์— ์˜ํ•ด Replica ์„œ๋น„์Šค๊ฐ€ ๋“ฑ๋ก๋˜๋ฉด ์ด์ œ ๋งˆ์น˜ ์„œ๋น„์Šค ๊ตฌํ˜„์ฒด์˜ ์ธ์Šคํ„ด์Šค๊ฐ€ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

| UI/Counter.razor

@inject ICounterService CounterService

razor์—์„œ @inject์œผ๋กœ ์›ํ•˜๋Š” Replica ์„œ๋น„์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CounterService.Increment();

์ธํ„ฐํŽ˜์ด์Šค์— ๋งž๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ Fusion์— ์˜ํ•ด ์ƒ์„ฑ๋œ Proxy ๊ตฌํ˜„์ฒด์˜ ์ธ์Šคํ„ด์Šค์— ์˜ํ•ด ICounterClientDef์ธํ„ฐํŽ˜์ด์Šค์— ์ •์˜๋œ ๊ทœ์น™์œผ๋กœ Web API๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

| Abstractions/Clients.cs

[BasePath("counter")]
public interface ICounterClientDef
{
    [Post("increment")]
    Task Increment(CancellationToken cancellationToken = default);

    [Get("get")]
    Task<int> Get(CancellationToken cancellationToken = default);
}

ํŠน์„ฑ์— ์˜ํ•ด /api/counter/increment์˜ API๊ฐ€ ํ˜ธ์ถœ์ด ๋˜๋Š”๋ฐ ์ด๊ฒƒ์€ ๋‹ค์Œ์˜ ์ปจํŠธ๋กค๋Ÿฌ์— ์˜ํ•ด ์„œ๋ฒ„์˜ Compute ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

| Server/CounterController.cs

[Route("api/[controller]/[action]")]
[ApiController, JsonifyErrors, UseDefaultSession]
public class CounterController : ControllerBase, ICounterService
{
    private readonly ICounterService _counter;

    public CounterController(ICounterService counter) 
        => _counter = counter;

    [HttpGet, Publish]
    public Task<int> Get(CancellationToken cancellationToken = default)
        => _counter.Get(cancellationToken);

    [HttpPost]
    public Task Increment(CancellationToken cancellationToken = default)
        => _counter.Increment(cancellationToken);
}

์„œ๋น„์Šค์˜ Get()์˜ ํŠน์„ฑ ์ค‘ Publish๊ฐ€ ์žˆ๋Š”๋ฐ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ฒŒ์‹œ๋ฅผ ์š”์ฒญํ•  ๊ฒฝ์šฐ Get ์ถœ๋ ฅ์ด ๊ฒŒ์‹œ ๋˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

Replica ์„œ๋น„์Šค์˜ ํฅ๋ฏธ๋กœ์šด ์ ์€ Replica ์„œ๋น„์Šค ๋˜ํ•œ ์ƒํƒœ๊ฐ€ ๋ฌดํšจํ™” ๋˜๊ธฐ ์ „๊นŒ์ง€ ๊ฐ’์„ ์บ์‹œ ํ•œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„์˜ ์ƒํƒœ๊ฐ€ ๋ฌดํšจํ™”๋˜์ง€ ์•Š๋Š” ํ•œ ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ฐ’์„ ์ฝ์€ ํ›„ ๋‹ค์‹œ ์ฝ์œผ๋ ค ํ•  ๋•Œ ์„œ๋ฒ„์— ์š”์ฒญํ•˜์ง€ ์•Š๊ณ  ์บ์‹œ๋œ ๊ฐ’์„ ์‚ฌ์šฉํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ๋™์ผํ•œ ํŽ˜์ด์ง€๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ๋งŒ๋“ค๊ณ  Increment ๋ฒ„ํŠผ์„ ๋ˆŒ๋ €์„ ๋•Œ Count๊ฐ€ ๋™์‹œ์— ์˜ฌ๋ผ๊ฐ€๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ์—๋Š” IWeatherForecastService๋ฅผ ๋ณด์‹œ์ฃ .

| Abstractions/IWeatherForcastService.cs

public interface IWeatherForecastService
{
    [ComputeMethod]
    Task<WeatherForecast[]> GetForecast(DateTime startDate, CancellationToken cancellationToken = default);
}

์‹œ์ž‘ ๋‚ ์งœ๋กœ ์˜ˆ๋ณด๋ฅผ ๊ตฌํ•˜๋Š” Compute ์„œ๋น„์Šค ์ž…๋‹ˆ๋‹ค.

| Services/WeatherForecastService.cs

public class WeatherForecastService : IWeatherForecastService
{
    private static readonly string[] Summaries = {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    [ComputeMethod(AutoInvalidationDelay = 1)]
    public virtual Task<WeatherForecast[]> GetForecast(
        DateTime startDate, CancellationToken cancellationToken = default)
    {
        var rng = new Random();
        return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        }).ToArray());
    }
}

ํ•œ ๊ฐ€์ง€ ๋‹ค๋ฅธ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ComputeMethod() ํŠน์„ฑ์—์„œ AutoInvalidationDelay๊ฐ€ 1๋กœ ์„ค์ •๋˜์–ด ์žˆ๋Š”๋ฐ 1์ดˆ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ๋ฌดํšจํ™”๋ฅผ ํ•ด์ฃผ๋Š” ์„ค์ •์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์œผ๋กœ ์ธํ•ด ๋งค ์ดˆ๋งˆ๋‹ค ์˜ˆ๋ณด ์ •๋ณด๊ฐ€ ๊ฐฑ์‹  ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ์ปจํŠธ๋กค๋Ÿฌ ์ž…๋‹ˆ๋‹ค.

| Server/Controllers/WeatherForecastController.cs

[Route("api/[controller]/[action]")]
[ApiController, JsonifyErrors, UseDefaultSession]
public class WeatherForecastController : ControllerBase, IWeatherForecastService
{
    private readonly IWeatherForecastService _forecast;

    public WeatherForecastController(IWeatherForecastService forecast) 
        => _forecast = forecast;

    [HttpGet, Publish]
    public Task<WeatherForecast[]> GetForecast(DateTime startDate,
        CancellationToken cancellationToken = default)
        => _forecast.GetForecast(startDate, cancellationToken);
}

์ด ์ปจํŠธ๋กค๋Ÿฌ์— ์˜ํ•ด /api/WeatherForecast/getForecast์˜ Web API๊ฐ€ ๋…ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

[BasePath("weatherForecast")]
public interface IWeatherForecastClientDef
{
    [Get("getForecast")]
    Task<WeatherForecast[]> GetForecast(DateTime startDate, CancellationToken cancellationToken = default);
}

fusionClient.AddReplicaService<IWeatherForecastService, IWeatherForecastClientDef>();์— ์˜ํ•ด ํ•ด๋‹น Web API๊ฐ€ Replica ์„œ๋น„์Šค๋กœ ์—ฐ๊ฒฐ์ด ๋ฉ๋‹ˆ๋‹ค.

์ •๋ฆฌ

์˜ค๋Š˜์€ Stl.Fusion์—์„œ ์ œ๊ณตํ•˜๋Š” HelloBlazorHybrid ์ƒ˜ํ”Œ์„ ํ†ตํ•ด Fusion์˜ Replica ์„œ๋น„์Šค์— ๋Œ€ํ•ด ์•Œ์•„ ๋ณด์•˜์Šต๋‹ˆ๋‹ค. Replica ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๋ฉด ๋งˆ์น˜ ๋‚ด๋ถ€ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ์ฒ˜๋Ÿผ ์„œ๋ฒ„์˜ Compute ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉฐ ์ƒํƒœ ๋ฌดํšจํ™”์‹œ ํด๋ผ์ด์–ธํŠธ๊นŒ์ง€ ๋ณ€๊ฒฝ๋œ ๊ฐ’์ด ์ž˜ ์ ์šฉ๋จ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

1๊ฐœ์˜ ์ข‹์•„์š”