[MAUI 활용] BookStore 만들기 (6) – 의존성 주입

NET MAUI에서의 의존성 주입(Dependency Injection) 은 응용 프로그램의 컴포넌트 간 결합도를 줄이고, 객체 간 의존성을 효율적으로 관리하는 중요한 디자인 패턴입니다. .NET MAUI는 기본적으로 이 패턴을 지원하여, 서비스나 뷰 모델을 쉽게 주입하고 관리할 수 있게 해줍니다. 의존성 주입은 .NET Core와 동일한 방식으로 작동하며, 대부분의 .NET 애플리케이션에서 사용하는 서비스 등록과 동일한 메커니즘을 따릅니다.

의존성 주입의 핵심 개념

  1. 제어의 역전(Inversion of Control, IoC): 의존성 주입은 제어의 역전이라는 디자인 패턴의 구현입니다. 이는 객체가 스스로 다른 객체를 생성하지 않고, 외부에서 필요한 객체를 제공(주입)받는 방식입니다.
  2. 서비스 생명 주기:
    • Transient: 의존성이 주입될 때마다 등록된 타입의 새 인스턴스가 제공된다는 것을 의미합니다.  AddTransient<T>() 메서드로 등록할 수 있습니다. 
    • Singleton: 애플리케이션의 전체 수명 동안 유지될 단일 인스턴스를 등록합니다. 이는 AddSingleton<T>() 메서드를 통해
    • 이루어집니다.
    • Scoped:  이해하기 조금 까다로울 수 있는 방식입니다.. 기본적으로, 스코프된 서비스나 의존성은 페이지나 그것을 사용하는 객체와 동일한 수명을 공유합니다. 즉, 싱글톤이 아닌 페이지가 닫히거나 객체가 범위를 벗어나면 스코프된 의존성도 함께 사라집니다. 같은 범위 내에서 동일한 의존성을 여러 번 해결하면 항상 동일한 인스턴스를 얻게 되지만, 다른 범위에서 동일한 의존성을 주입받게 되면 다른 인스턴스가 생성됩니다. 이는 같은 페이지의 구성 요소(예: 뷰)가 동일한 의존성을 공유하는 것이 유용할 때나, 서로 다른 페이지가 각각 고유한 인스턴스를 가져야 할 때 특히 유용합니다. 스코프된 의존성은 AddScoped<T>() 메서드로 등록할 수 있습니다.”
  3. DI 컨테이너: .NET MAUI에서 DI 컨테이너는 객체를 자동으로 관리하고 주입해주는 역할을 합니다. DI 컨테이너는 MauiProgram.cs 파일에서 서비스를 구성할 때 사용됩니다. .NET에는 많은 의존성 주입 컨테이너가 있습니다. MAUI 에서는 기본으로 Microsoft.Extensions.DependencyInjection을 사용하여 앱 내에서 뷰, 뷰 모델, 서비스 클래스의 인스턴스를 관리합니다. Microsoft.Extensions.DependencyInjection은 느슨하게 결합된 애플리케이션을 구축하는 것을 용이하게 하며, 의존성 주입 컨테이너에서 일반적으로 제공되는 모든 기능을 제공합니다. 이러한 기능에는 타입 매핑 및 객체 인스턴스를 등록하는 메서드, 객체를 해결하는 메서드, 객체의 수명 관리를 처리하는 메서드, 그리고 해결된 객체의 생성자에 의존성을 주입하는 메서드가 포함됩니다.

의존성을 주입하는 방식은 크게 두가지로 나눌 수 있습니다. 생성자를 이용한 방식과 명시적으로 주입하는 방식입니다. 이에 대해서 간략하게 알아보겠습니다. 

생성자를 이용한 의존성 주입

의존성 주입의 가장 일반적인 방법 중 하나는 생성자를 이용한 의존정 주입입니다. 아래와 같이 구성할 수 있습니다.

[MainPage.xaml.cs]

namespace MyBookStore
{
    public partial class MainPage : ContentPage
    {
        public MainPage(BookListViewModel bookListViewModel)
        {
            InitializeComponent();
            BindingContext = bookListViewModel;
        }    
    }
}

BindingContext 와 BookListViewModel 에  대해서는 이어지는 강의를 통해서 자세히 배울 예정이고, 여기서는 BookListViewModel 의 생성자 부분만 간단히 살펴보겠습니다. 

[BookListViewModel.cs]

 public partial class BookListViewModel : BaseViewModel
 {
     private readonly BookService bookService;
     public ObservableCollection<Book> Books { get; set; } = new ObservableCollection<Book>();
     public BookListViewModel(BookService bookService) 
     {
         Title = "Book List";
         this.bookService = bookService;
     }
    ...
     
 }

위의 BookListViewModel 의 생성자를 보면 BookService 의 객체를 생성자를 통해서 의존성 주입을 받고 있는 것을 확인할 수 있습니다.  정리하면 MainPage 는 BookListViewModel 에 대한 의존성을, BookListViewModel 은 BookService 에 대한 의존성을 생성자를 통하여 주입받고 있는 것입니다. 

의존성 등록

MAUI에서는 빌더 패턴을 사용하며, 이는 현대 .NET 애플리케이션 및 기술에서 애플리케이션 구성을 위한 일반적인 접근 방식입니다. 이 패턴은 기능과 의존성 구성을 매우 간단하게 만들어 주며, 이를 모두 한 곳에서 관리할 수 있게 합니다.MauiProgram.cs는 애플리케이션이 보통 구성되고 빌드되는 장소입니다. 해당 소스를 아래와 같이 수정합니다.

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });

#if DEBUG
    		builder.Logging.AddDebug();
#endif            
            builder.Services.AddSingleton<BookListViewModel>();
            builder.Services.AddSingleton<MainPage>();
            builder.Services.AddSingleton<BookService>();

            return builder.Build();
        }
    }

실제 추가된 부분은 아래와 같습니다 .

           builder.Services.AddSingleton<BookListViewModel>();
           builder.Services.AddSingleton<MainPage>();
           builder.Services.AddSingleton<BookService>();

builder 객체는 모든 의존성이 등록되는 Services 컬렉션을 제공합니다. 이를 통해 MAUI는 우리가 의존성이 있는 클래스나 다른 클래스에 대한 의존성으로 사용될 클래스를 가지고 있다는 것을 알 수 있습니다. 새로운 의존성을 사용될 클래스를 등록하는 것은 위와 같이 아주 간단합니다. 

 항상 모든 의존성, 특히 이러한 의존성을 필요로 하는 클래스들을 등록해야 하는 것을 기억하십시요.  그렇지 않으면 이상한 예외나 오류가 발생할 수 있습니다. 사람들이 자주 범하는 일반적인 실수 중 하나는 ViewModel을 등록하지만, 그 ViewModel을 생성자에서 사용하는 페이지를 등록하지 않는 것입니다. 예를 들어 위의 코드에서

builder.Services.AddSingleton<MainPage>();

이 부분을 누락시키는 것이지요. 그러면 다음과 같은 런타임 오류가 발생하게 됩니다. 

System.MissingMethodException: 'MauiSamples.Views.MainPage 타입에 대해 매개변수가 없는 생성자가 정의되지 않았습니다.'

이 문제는 MainPageViewModel과 함께 MainPage를 등록함으로써 해결할 수 있습니다:

명시적 의존성 주입

어떤 경우에는 생성자 주입 방식을 사용하지 못하는 경우가 있을 수 있습니다. 이러한 경우에는 명시적으로 등록한 서비스를 명시적으로 갖고 올 수 있는 방식을 사용할 수 있습니다. 아래는 그러한 동작을 쉽게 구현하기 위한 helper class 입니다. 

public static class ServiceHelper
{
    public static IServiceProvider Services { get; private set; }

    public static void Initialize(IServiceProvider serviceProvider) =>
        Services = serviceProvider;

    public static T GetService<T>() => Services.GetService<T>();
}

그런 다음, MauiProgram.cs에서 Initialize 메서드를 호출합니다.

        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });

#if DEBUG
    		builder.Logging.AddDebug();
#endif            
            builder.Services.AddSingleton<BookListViewModel>();
            builder.Services.AddSingleton<MainPage>();
            builder.Services.AddSingleton<BookService>();
            var app = builder.Build();

            ServiceHelper.Initialize(app.Services);
            return app;            
        }

이에 MainPage 에서 ServiceHelper 를 통해서 의존성을 주입받아 보겠습니다.

public partial class MainPage : ContentPage
{
    int count = 0;
    private readonly MainPageViewModel mainPageViewModel;
    //public MainPage(MainPageViewModel _mainPageViewModel)
    //{
    //    InitializeComponent();
    //    this.mainPageViewModel = _mainPageViewModel;
    //}

    public MainPage()
    {
        InitializeComponent();
        this.mainPageViewModel = ServiceHelper.GetService<BookListViewModel>();
    }
    
}

생성자를 통한 주입부분을 주석 처리하고 기본 생성자에서 ServiceHelper 클래스를 통해서 의존성을 주입받고 있습니다.

의존성 주입은 .NET MAUI에서 제공하는 좋은 기능입니다.  프레임웍에 기본적으로 내장되어 있기 때문에, 내장된 의존성 주입 메커니즘의 제약과 제한을 넘어서지 않는 한, 제3자 프레임워크나 라이브러리를 사용할 필요가 없다는 점이 매우 훌륭합니다

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다