🦮 Blazor 입문
지난 시간에는 Blazor의 Templated Component의 개념에 대해서 학습하는 시간을 가져보았다. Templated Component는 말 그대로 템플릿 컴포넌트 인데, 여기서 말하는 템플릿은 C#의 제네릭과 동일하다는 의미를 가진다. 따라서 Templated Component를 사용하여 붕어빵처럼 컴포넌트를 찍어낼 수 있다. 이 때에 RenderFragment를 사용하고, <T> 타입을 선언하는 것을 razor에서는 @typeparam을 통해 대체하는 것 까지 알아보았다. 이번 시간에는 Dependency Injection 라는 개념에 대해서 학습하는 시간을 가져보며, 이 또한 직접 실습하며 페이지를 제작해보도록 한다.
🏄♀️ Dependency Injection
Dependency Injection. 단어 그대로 직역하자면 '의존성 주입'이다. 이번 예제는 기존 코드와 연관성이 없기 때문에 새로운 파일을 하나 만들어주도록 하자. 'Fodd Service'라는 파일을 Data 폴더에서 만들자.
웹 개발은 일전에도 말했다 싶이 테크크아웃 포장 전문 식당이라고 얘기하였다. 이를 고속도로 휴게소로 비유하여 생각한다면 한식집, 분식집, 일식집, 양식집 등 다양한 식당이 있는데, 이를 모두 각자 '서비스'라고 생각하도록 하자. 일단은 Dependency Injection이 왜 필요한지 알아보기 위해 하나하씩 만들어보도록 하자.
using System.Collections;
namespace BlazorApp.Data
{
public class Food
{
public string Name { get; set; }
public int Price { get; set; }
}
public class FoodService
{
public List<Food> GetFoods()
{
List<Food> foods = new List<Food>
{
new Food() { Name = "Bibimbap", Price = 7000 },
new Food() { Name = "Kimbap", Price = 3000 },
new Food() { Name = "Bossam", Price = 9000 }
};
return foods;
}
}
}
먼저 새로운 클래스 Food를 만들어주고, FoodService 클래스에서 이를 리스트화 하고, 간단한 내용물을 채워주도록 하자. 이 후 새로운 컴포넌트를 만들기 귀찮으므로 🤣 index.razor 컴포넌트 안에 내용을 지우고 index.razor에 FoodSerive를 사용할 수 있게끔 만들어주도록 하자.
@page "/"
@using BlazorApp.Data
<div>
@foreach (var food in foodService.GetFoods())
{
<div>@food.Name</div>
<div>@food.Price</div>
}
</div>
@code
{
FoodService _foodService = new FoodService();
}
이와 같이 간단하게 만들어 준 다음, 정상적으로 실행이 되는지 확인해보도록 하자.
걀과를 살펴보면 home 탭, 즉 index 페이지에서 비빔밥, 김밥, 보쌈이 정상적으로 잘 출력되는 것을 알 수 있다. 그러나 여기서 문제점이 있다.
@page "/"
@using BlazorApp.Data
...
@code
{
// 이렇게 구현 할 경우, 의존성이 강해진다.
FoodService _foodService = new FoodService();
}
바로 index 컴포넌트에 작성한 FoodService가 문제가 된다. 바로 코드의 의존성이 너무 강해진다라는 뜻이다. 예를 들어 나중에 FoodSerive가 1개만 있는 것이 아닌 수십, 수백개가 있게 될텐데 하필이면 index 컴포넌트에 있는 FoodSeriver가 콕 박혀있기 때문에 FoodService를 수정하게 되면 마찬가지로 index 컴포넌트도 영향을 받게 되기 때문이다. 따라서 이는 좋지 않은 방법이다.
그렇다면 이를 어떻게 해결해야 할까? 바로 인터페이스를 이용하는 방법이 있다. 인덱스를 통해 이를 해결해보도록 하자.
using System.Collections;
namespace BlazorApp.Data
{
public class Food
{
public string Name { get; set; }
public int Price { get; set; }
}
public interface IFoodService
{
// 다양한 자료구조를 사용할 수 있으므로, IEnumerable를 사용한다.
IEnumerable<Food> GetFoods();
}
public class FoodService : IFoodService
{
...
}
}
먼저 interface 형식으로 IFoodService를 구현하도록 하자. IFoodService는 GetFoods를 무조건 갖고 있어야 할 테니 들고 있어주고, 다양한 자료구조를 이용할 수 있으므로 IEnumerable 형태로 구현하도록 하자. 이 후 IEnumerable 형태를 사용하므로 FoodService에서 List가 아닌 IEnumerable<Food> 형태로 GetFoods로 바꿔주고, 마찬가지로 FoodService 뒤에도 인터페이스를 사용하기 위해 IFoodService를 붙여주도록 하자.
@page "/"
@using BlazorApp.Data
@inject IFoodService foodService
<div>
@foreach (var food in foodService.GetFoods())
{
<div>@food.Name</div>
<div>@food.Price</div>
}
</div>
@code
{
// 이렇게 구현 할 경우, 의존성이 강해진다.
// FoodService _foodService = new FoodService();
// new FoodService가 문제가 된다.
IFoodService _foodService = new FoodService();
}
그러나 인터페이스를 통해 활용하여 변경 할 경우, index 컴포넌트에서 결국 new FoodSeriver()를 하게 되어 문제가 발생한다. 사용자는 푸드 서비스 뿐만 아니라 다른 푸드 서비스를 사용할 수 있는데 이와 같이 new FoodServie로 구현 할경우 문제가 생긴다. 예를 들면 아래와 같이 FastFood가 새롭게 생기는 것이다.
public class FastFoodService : IFoodService
{
public IEnumerable<Food> GetFoods()
{
List<Food> foods = new List<Food>
{
new Food() { Name = "Burger", Price = 7000 },
new Food() { Name = "Fries", Price = 3000 },
};
return foods;
}
}
위와 같이 FastFoodService를 새롭게 구현한다면 결국 new를 쓰는 단점과 문제점은 interface를 사용해도 여전히 남이있는 것이다. 따라서 이를 해결하기 위해 나온 개념이 바로 Dependency Injection이다. Dependency Injection를 통해 위 문제를 해결해보도록 하자.
먼저 Program.cs를 열어주자. 예전 버전에서는 ConfigureServie 함수 내에서 처리되었지만 신 버전에서는 해당 함수가 존재하지 않고 그냥 작성할 수 있다. 따라서 여기에 AddSingleton을 통해 IFoodServier를 넣어주도록 하자.
using BlazorApp.Data;
...
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
// Dependency Injection
builder.Services.AddSingleton<IFoodService, FastFoodService>();
var app = builder.Build();
...
app.Run();
Program에 작성하는 코드들은 우리가 실제 운영할 식당의 정보를 작성하는 것과 마찬가지라고 일전에 얘기한 적이 있다. 위 코드는 우리가 IFoodService의 인터페이스를 FastFoodService로 구현할 것이라고 선포한 것이다. 따라서 이제 인덱스 페이지 최상단에 @inject IFoodService foodService를 작성하여 의존성을 주입해주면 된다. 이 후 코드를 실행하면 FastFoodService의 서비스가 실행되는 것을 볼 수 있다.
Dependency Injection의 특징은 이것이 다가 아니다. 예를들어 Payment Service라고 해서 계산을 할 때 사용되는 서비스가 있다고 가정해보자. 해당 함수를 통해 마지막에 계산을 하기 위해서는 결국 사용자가 어떤 푸드 서비스를 사용했는제 알아야 한다. 따라서 서비스끼리도 서로 연관성으로 물릴 일이 생기는 것이다. 이제 Payment Service를 구현해보도록 하자.
using System.Collections;
namespace BlazorApp.Data
{
public class Food
{
public string Name { get; set; }
public int Price { get; set; }
}
public interface IFoodService
{
// 다양한 자료구조를 사용할 수 있으므로, IEnumerable를 사용한다.
IEnumerable<Food> GetFoods();
}
public class FoodService : IFoodService
{
...
}
public class FastFoodService : IFoodService
{
...
}
public class PaymentService
{
IFoodService _service;
public PaymentService(IFoodService service)
{
_service = service;
}
}
}
실제로 내가 사용한 서비스를 알기 위해 IFoodService를 Payment Sevice에서 정의하도록 하자. 이 후 생성자를 만들어 줄 것인데 생성자는 IFoodService를 인자 값으로 받아 정의한다. 여기서 놀라운 사실은 생성자에서 이런식으로 IFoodService를 정의만 해준다면 실질적으로 ASP.NET 차원에서 이를 주입해주어 연동을 해주게 된다. 사실 얼핏들으면 잘 이해가 되지 않으므로 실습을 통해 알아가도록 하자.
Program.cs 함수로 돌아가 Dependency Injection 밑에 AddSingleton을 통해 해당 함수를 사용한다고 명시시켜 주도록 하자.
...
// Dependency Injection
builder.Services.AddSingleton<IFoodService, FastFoodService>();
// 생성자에서 알아서 연결해준다.
builder.Services.AddSingleton<PaymentService>();
...
이 후, index 컴포넌트로 돌아가 마찬가지로 Payment Service를 주입시키도록 하자.
@page "/"
@using BlazorApp.Data
@* IFoodService와 PaymentService 의존성 주입. 😎 *@
@inject IFoodService foodService
@inject PaymentService paymentService;
...
@code
{
// 이렇게 구현 할 경우, 의존성이 강해진다.
// FoodService _foodService = new FoodService();
// new FoodService가 문제가 된다.
// IFoodService _foodService = new FoodService();
protected override void OnInitialized()
{
}
}
이 후 브레이크 포인트를 잡기 위해 OnInitialized를 함수를 구현하여 브레이크 포인트를 잡아준다. 이 후 실행해 PaymentService를 살펴보면 FastFoodService 서비스가 걸려있는 것을 알 수 있다.
따라서 이를 통해 우리가 직접 PaymentService 내부를 구현해주지 않더라도 Dependency Injection의 특징에 따라 PaymentService 내부에 FoodService의 내용이 들어가는 것을 알 수 있다.
🏄♀️ Service의 종류
사실 Program.cs의 서비스를 추가할 때에는 3가지 종류가 있다. FoodService에 서비스 몇 개를 더 만들어 이를 구현해보도록 하자.
using System.Collections;
namespace BlazorApp.Data
{
public class Food
{
public string Name { get; set; }
public int Price { get; set; }
}
...
public class SingletonService : IDisposable
{
public Guid ID { get; set; }
public SingletonService()
{
ID = Guid.NewGuid();
}
public void Dispose()
{
Console.WriteLine("SingletonServices Disposed");
}
}
public class TransientService : IDisposable
{
public Guid ID { get; set; }
public TransientService()
{
ID = Guid.NewGuid();
}
public void Dispose()
{
Console.WriteLine("TransientService Disposed");
}
}
public class ScopedService : IDisposable
{
public Guid ID { get; set; }
public ScopedService()
{
ID = Guid.NewGuid();
}
public void Dispose()
{
Console.WriteLine("ScopedService Disposed");
}
}
}
Guid는 Globally Unique Identifier를 뜻하며, 아이디를 만들 때 우용한 함수이다. 따라서 이를 통해 NewGuid를 사용하여 아이디를 랜덤으로 만들어주도록 하자. 이 후 이를 사용하기 위해 Program.cs에 추가해주도록 하자.
...
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
// Dependency Injection
builder.Services.AddSingleton<IFoodService, FastFoodService>();
// 생성자에서 알아서 연결해준다.
builder.Services.AddSingleton<PaymentService>();
// 3가지 모드
builder.Services.AddSingleton<SingletonService>();
builder.Services.AddTransient<TransientService>();
builder.Services.AddScoped<ScopedService>();
var app = builder.Build();
...
모두 AddSingleton을 사용하는 것이 아닌 각자의 기능 Transient, Scoped를 사용한다. 이 후 해당 함수의 실행 주기를 파악하기 위해 index 컴포넌트에서 이를 표시해주도록 하자.
@page "/"
@using BlazorApp.Data
@inject IFoodService foodService
@inject PaymentService paymentService;
@* 생명주기가 모두 다르다. *@
@inject SingletonService singleton;
@inject TransientService transient;
@inject ScopedService scoped;
...
<div>
<h1>Singleton</h1>
Guid: @singleton.ID;
<h1>Transient</h1>
Guid: @transient.ID;
<h1>Scoped</h1>
Guid: @scoped.ID;
</div>
@code
{
...
}
위와 같이 작성하여 실행해 줄 경우 아래와 같이 나오는 것을 볼 수 있다.
결론부터 요약하자면, Singleton의 경우 서버를 한번 띄울 때 생성이되고 변화가 없다. 따라서 이를 언제 활용하냐면 Wearther forecast 서비스와 같이 아예 변동이 없고, 누구에게나 동일한 정보를 보여줘야 하는 서비스를 운영한다면 싱글톤을 통해 만들어 주는 것이 현명하다. 만약 그게 아니라 유저마다 정보가 바뀌어야 한다면 Transient와 Scoped를 사용해야 한다.
Transient의 경우, Singleton과 정반대라고 생각하면 된다. Transient는 페이지를 요청/갱신 할 때마다 바뀌게 된다. 마지막으로 Scoped의 경우 Singleton과 Transient의 그 사이라고 생각하면 된다. 따라서 유저가 처음에 로그인 할 경우 접속하는 그 순간에 설정되고, 이 때 변경된다. 따라서 각각의 함수들이 생명주기가 모두 다르다는 걸 알 수 있다.
Singleton의 경우 서버를 띄울 때 생성이 되므로 매우 긴 생명주기를 가지고 있으며, Transient의 경우 페이지를 갱신, 접속할 때마다 바뀌기 때문에 매우 짧은 것을 알 수 있다. Scoped의 경우 Singleton과 Transient 그 사이 어딘가라고 생각하면 되겠다.
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 6-5-9. Blazor 입문 : Form, Validation (0) | 2024.09.15 |
---|---|
Part 6-5-8. Blazor 입문 : SPA 구조, Router (0) | 2024.09.09 |
Part 6-5-6. Blazor 입문 : Templated Component (0) | 2024.09.07 |
Part 6-5-5. Blazor 입문 : Cascading Parameter (0) | 2024.09.06 |
Part 6-5-4. Blazor 입문 : Parameter, Ref, EventCallback (0) | 2024.09.04 |