Dependency Injection Service Lifetimes

DI(Dependency Injection)을 구성할때 lifetime을 정의해야한다. lifetime은 DI가 동작할때 서비스 인스턴스가 생성되는 조건을 정의 하는 것이다.
(참고로, DI에 대한 개념 설명은 “Dependency Injection 개념과 Ninject 사용법”이란 포스팅에 설명해 두었다. 추가적인 내용이 필요 하다면 참고하길 바란다.)

lifetime에는 다음과 같이 3가지가 있다.

  1. Transient – 모든 요청에 대해서 매번 새로 인스턴스를 생성한다.
  2. Scoped – scope 당 1회 인스턴스를 생성한다.
  3. Singleton – DI가 구성되고 특정 시점에 1회 인스턴스를 생성되고 나면, 모든 호출에 대해서 동일한 인스턴스를 재활용한다.

글로만 읽으면 잘 이해가 가지 않는다. 좀 더 쉽게 이해를 하기 위해서 간단히 코딩을 하여 알아보자.

.Net Core Console 프로젝트를 생성하고 Progrma.cs에 다음과 같이 코드를 넣어 보자.

namespace DILifetimesTest
{
    public interface IService
    {
        void Info();
    }

    public interface ISingleton : IService { }
    public interface IScoped : IService { }
    public interface ITransient : IService { }

    public abstract class Operation : ISingleton, IScoped, ITransient
    {
        private Guid _operationId;
        private string _lifeTime;

        public Operation(string lifeTime)
        {
            _operationId = Guid.NewGuid();
            _lifeTime = lifeTime;

            Console.WriteLine($"{_lifeTime} Service Created.");
        }

        public void Info()
        {
            Console.WriteLine($"{_lifeTime}: {_operationId}");
        }
    }

    public class SingletonOperation : Operation
    {
        public SingletonOperation() : base("Singleton") { }
    }
    public class ScopedOperation : Operation
    {
        public ScopedOperation() : base("Scoped") { }
    }
    public class TransientOperation : Operation
    {
        public TransientOperation() : base("Transient") { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var serviceProvider = new ServiceCollection()
               .AddTransient<ITransient, TransientOperation>()
               .AddScoped<IScoped, ScopedOperation>()
               .AddSingleton<ISingleton, SingletonOperation>()
               .BuildServiceProvider();


            Console.WriteLine("========== Request 1 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");

            Console.WriteLine("========== Request 2 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");
                        
            using (var scope1 = serviceProvider.CreateScope())
            {
                Console.WriteLine("========== Request 3 (scope1)============");
                scope1.ServiceProvider.GetService<ITransient>().Info();
                scope1.ServiceProvider.GetService<IScoped>().Info();                
                scope1.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");

                Console.WriteLine("========== Request 4 (scope1)============");
                scope1.ServiceProvider.GetService<ITransient>().Info();
                scope1.ServiceProvider.GetService<IScoped>().Info();
                scope1.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");
            }

            using (var scope2 = serviceProvider.CreateScope())
            {
                Console.WriteLine("========== Request 5 (scope2)============");
                scope2.ServiceProvider.GetService<ITransient>().Info();
                scope2.ServiceProvider.GetService<IScoped>().Info();
                scope2.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");
            }

            Console.WriteLine("========== Request 6 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");

            Console.ReadKey();
        }
    }
}

코드를 실행해 보면 다음과 같이 결과를 얻을 수 있다.

========== Request 1 ============
Transient Service Created.
Transient: 2abfdf9f-5223-4a2c-953f-1c7269745073
Scoped Service Created.
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton Service Created.
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 2 ============
Transient Service Created.
Transient: 883343bd-0f24-4416-a56c-04b1a7cba89f
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 3 (scope1)============
Transient Service Created.
Transient: 5eba1fb5-f53b-49cc-bab4-5e1e103b4e17
Scoped Service Created.
Scoped: a0cdf863-e10b-4925-8731-3fe0f2a085ff
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 4 (scope1)============
Transient Service Created.
Transient: d6124182-1fbc-402d-997f-fd9024fa5187
Scoped: a0cdf863-e10b-4925-8731-3fe0f2a085ff
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 5 (scope2)============
Transient Service Created.
Transient: 8a200580-965e-4d77-bfad-08cc722e6a13
Scoped Service Created.
Scoped: 72c67b06-6296-49da-b162-853c6f16e1d0
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 6 ============
Transient Service Created.
Transient: 0885afac-3b12-4c8a-8e60-0fb68b0f32a4
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

출력된 결과를 살펴 보면 Transit은 매 서비스 요청마다 새로 인스턴스가 생성된 것을 확인 할 수 있다.

다음으로 Scoped는 1번 요청에서 생성된것이 2번 6번에서 재활용되었고, 그 외 Scope가 새로 할당 될 때는 새 인스턴스가 생성되고 해당 Scope안에서는 Scope가 종료될때까지 동일한 인스턴스가 재활용되는 것을 확인 할 수 있다.

마지막으로 Sigleton은 1회 인스턴스가 생성되면, 요청이나 Scope 할당과 상관없이 동일한 인스턴스가 재활용 되는 것을 확인 할 수 있다.

위와 같이 각각 lifetime 옵션에 따라 서비스 처리 특성이 다르기 때문에 서비스 요청 특성에 맞춰 알맞는 lifetime을 설정하는 것이 중요할 것 같다.

[출처] : https://www.c-sharpcorner.com/article/dependency-injection-service-lifetimes/

Dependency Injection 개념과 Ninject 사용법

소프트웨어를 잘 만들기 위해서 많은 디자인 패턴들이 사용되는데, 그중에서 DI(Dependency Injection, 의존성 주입)에 대한 개념과 .NET MVC에서 많이 사용되는 DI Framework들 중 하나인 Ninject(Open Source Project)에 대해서 알아보겠다.

DI(Dependency Injection) 란?

프로그래밍에서 사용되는 객체들 사이의 의존관계를 소스코드가 아닌 외부 설정파일 등을 통해 정의하게 하는 디자인 패턴이다. 개발자는 각 객체의 의존관계를 일일이 소스코드에 작성할 필요 없이 설정파일에 의존관계가 필요하다는 정보만 된다. 그러면 객체들이 생성될때, 외부로부터 의존관계를 주입 받아 관계가 설정 된다.

DI를 적용하면, 의존관계 설정이 컴파일시 고정 되는 것이 아니라 실행시 설정파일을 통해 이루어져 모듈간의 결합도(Coupling)을 낮출 수 있다.
결합도가 낮아지면, 코드의 재사용성이 높아져서 모듈을 여러 곳에서 수정 없이 사용할 수 있게 되고, 모의 객체를 구성하기 쉬워지기 때문에 단위 테스트에 용이해 진다.

이제, .NET에서 MVC 프로젝트를 만들때 DI를 구현하기 위해 가장 많이 사용하는 Open Source인 Ninject에 대해서 알아보자.

NInject 알아보기

(공식 페이지 : http://www.ninject.org/)

간단히 이름부터 살펴보면, NInject는, N(Ninja) + Inject로 대표 이미지로도 Ninja로 되어 있다.

홈페이지 대문에 보면 “Be fast, be agile, be precise”라는 슬로건이 있는데 닌자처럼 빠르고 민첩하며 정확하게 프로그램을 만들수 있게 하겠다는 정신이 담겨있는 것 같다.

(Nate Kohari라는 소프트웨어 엔지니어가 최초 개발을 했는데, 개인적인 생각으로는 N을 중의적인 의미로 사용한게 아닌가 싶다. 참고 : https://www.infoq.com/articles/ninject10-released/)

NInject는 Open Source 라이브러리로 Apache License 2.0에 따라 배포되었으며,
2007년 부터 .NET 어플리케이션의 DI를 구현하기 쉽게 해주도록 지원하고 있다.

이제, 실제로 사용해 보자

NInject 사용해 보기

  1. Package Install
    • Visual Studio에서 제공하는 Nuget Package Installer를 사용하여 다음 Package들을 설치한다.
      – Ninject
      – Ninject.Web.WebApi
      – Ninject.Web.Common
      – Ninject.Web.Common.WebHost
  2. Edit Ninject.Web.Common.cs
    • 위 Package 설치가 완료되면, 프로젝트 최상단에서 App_Start 폴더에 Ninject.Web.Common.cs 파일이 생성된 것을 확인 할 수 있다.
    • 해당 파일을 열어 보면, CreateKernel이라는 method가 있는데 다음 코드를 추가한다. 그러면 NInject가 controller의 의존성 주입을 구성해 줄 수 있게 된다.
RegisterServices(kernel);
GlobalConfiguration.Configuration.DependencyResolver = new NinjectDependencyResolver(kernel);
return kernel;
  1. Register Service
    • 스크롤을 조금 내려 보면, RegisterServices라는 method를 확인 할 수 있는데, 실제로 의존관계를 설정(bind)하는 곳이다. 의존성을 주입할 객체들의 관계를 다음과 같이 추가한다.
private static void RegisterServices(IKernel kernel)
{
    kernel.Bind<ICommonStore>().To<CommonStore>();
}
  1. Use it on controller
    • 이제 의존성 주입을 위한 IoC 설정과, 의존관계 설정(bind)작업을 모두 하였으니 Controller에서 사용해보자.
public class CommonStoreController : Controller
{    
    public CommonStoreController(ICommonStore common)
    {
        this.commonStore = common;
    }

    private ICommonStore commonStore;

    public int GetItemCount(string id)
    {
        return commonStore.Add(id);
    }
}

Design Pattern 중 DI(Dependency Injection, 의존성 주입)이라는 패턴에은 표준 프로그래밍을 할 때 중요한 요소이긴하지만 무조건 사용해야 하는 것은 아니다. 간단한 프로그램이나 객체간의 결합이 명확하여 구분하지 않아도 되는 경우 굳이 프로젝트를 무겁게(?) 만들 필요없다.

그리고 Ninject는 .NET에서 많이 사용되는 Open Source DI Framework 중 하나로 DI를 할 때 쉽게 구현할 수 있어서 선호되는 편이지만 다양한 DI Open Source Framework들이 있으며, 성능 면에서도 훨씬 더 좋은 것들이 있으니 확인하고 사용하길 바란다.
(참고 : https://www.claudiobernasconi.ch/2019/01/24/the-ultimate-list-of-net-dependency-injection-frameworks/)